diff --git a/.coveragerc b/.coveragerc index b5939bafec6..e96895429a6 100644 --- a/.coveragerc +++ b/.coveragerc @@ -5,7 +5,6 @@ omit = homeassistant/__main__.py homeassistant/helpers/signal.py homeassistant/helpers/typing.py - homeassistant/monkey_patch.py homeassistant/scripts/*.py homeassistant/util/async.py @@ -62,6 +61,7 @@ omit = homeassistant/components/asterisk_cdr/mailbox.py homeassistant/components/asterisk_mbox/* homeassistant/components/asuswrt/device_tracker.py + homeassistant/components/aten_pe/* homeassistant/components/atome/* homeassistant/components/august/* homeassistant/components/aurora_abb_powerone/sensor.py @@ -83,9 +83,9 @@ omit = homeassistant/components/blinkt/light.py homeassistant/components/blockchain/sensor.py homeassistant/components/bloomsky/* - homeassistant/components/bluesound/media_player.py + homeassistant/components/bluesound/* homeassistant/components/bluetooth_le_tracker/device_tracker.py - homeassistant/components/bluetooth_tracker/device_tracker.py + homeassistant/components/bluetooth_tracker/* homeassistant/components/bme280/sensor.py homeassistant/components/bme680/sensor.py homeassistant/components/bmw_connected_drive/* @@ -93,6 +93,7 @@ omit = homeassistant/components/bom/sensor.py homeassistant/components/bom/weather.py homeassistant/components/braviatv/media_player.py + homeassistant/components/broadlink/remote.py homeassistant/components/broadlink/sensor.py homeassistant/components/broadlink/switch.py homeassistant/components/brottsplatskartan/sensor.py @@ -109,7 +110,7 @@ omit = homeassistant/components/cast/* homeassistant/components/cert_expiry/sensor.py homeassistant/components/cert_expiry/helper.py - homeassistant/components/channels/media_player.py + homeassistant/components/channels/* homeassistant/components/cisco_ios/device_tracker.py homeassistant/components/cisco_mobility_express/device_tracker.py homeassistant/components/cisco_webex_teams/notify.py @@ -163,6 +164,7 @@ omit = homeassistant/components/doorbird/* homeassistant/components/dovado/* homeassistant/components/downloader/* + homeassistant/components/dsmr_reader/* homeassistant/components/dte_energy_bridge/sensor.py homeassistant/components/dublin_bus_transport/sensor.py homeassistant/components/duke_energy/sensor.py @@ -178,7 +180,7 @@ omit = homeassistant/components/ecobee/notify.py homeassistant/components/ecobee/sensor.py homeassistant/components/ecobee/weather.py - homeassistant/components/econet/water_heater.py + homeassistant/components/econet/* homeassistant/components/ecovacs/* homeassistant/components/eddystone_temperature/sensor.py homeassistant/components/edimax/switch.py @@ -199,6 +201,7 @@ omit = homeassistant/components/envirophat/sensor.py homeassistant/components/envisalink/* homeassistant/components/ephember/climate.py + homeassistant/components/epson/const.py homeassistant/components/epson/media_player.py homeassistant/components/epsonworkforce/sensor.py homeassistant/components/eq3btsmart/climate.py @@ -229,6 +232,7 @@ omit = homeassistant/components/flexit/climate.py homeassistant/components/flic/binary_sensor.py homeassistant/components/flock/notify.py + homeassistant/components/flume/* homeassistant/components/flunearyou/sensor.py homeassistant/components/flux_led/light.py homeassistant/components/folder/sensor.py @@ -254,6 +258,9 @@ omit = homeassistant/components/geniushub/* homeassistant/components/gearbest/sensor.py homeassistant/components/geizhals/sensor.py + homeassistant/components/gios/__init__.py + homeassistant/components/gios/air_quality.py + homeassistant/components/gios/consts.py homeassistant/components/github/sensor.py homeassistant/components/gitlab_ci/sensor.py homeassistant/components/gitter/sensor.py @@ -282,7 +289,7 @@ omit = homeassistant/components/hangouts/hangouts_bot.py homeassistant/components/hangouts/hangups_utils.py homeassistant/components/harman_kardon_avr/media_player.py - homeassistant/components/harmony/remote.py + homeassistant/components/harmony/* homeassistant/components/haveibeenpwned/sensor.py homeassistant/components/hdmi_cec/* homeassistant/components/heatmiser/climate.py @@ -314,7 +321,9 @@ omit = homeassistant/components/iaqualink/light.py homeassistant/components/iaqualink/sensor.py homeassistant/components/iaqualink/switch.py + homeassistant/components/icloud/__init__.py homeassistant/components/icloud/device_tracker.py + homeassistant/components/icloud/sensor.py homeassistant/components/izone/climate.py homeassistant/components/izone/discovery.py homeassistant/components/izone/__init__.py @@ -327,6 +336,7 @@ omit = homeassistant/components/influxdb/sensor.py homeassistant/components/insteon/* homeassistant/components/incomfort/* + homeassistant/components/intesishome/* homeassistant/components/ios/* homeassistant/components/iota/* homeassistant/components/iperf3/* @@ -410,6 +420,7 @@ omit = homeassistant/components/miflora/sensor.py homeassistant/components/mikrotik/* homeassistant/components/mill/climate.py + homeassistant/components/mill/const.py homeassistant/components/minio/* homeassistant/components/mitemp_bt/sensor.py homeassistant/components/mjpeg/camera.py @@ -526,6 +537,7 @@ omit = homeassistant/components/proliphix/climate.py homeassistant/components/prometheus/* homeassistant/components/prowl/notify.py + homeassistant/components/proxmoxve/* homeassistant/components/proxy/camera.py homeassistant/components/ptvsd/* homeassistant/components/pulseaudio_loopback/switch.py @@ -603,6 +615,8 @@ omit = homeassistant/components/shodan/sensor.py homeassistant/components/sht31/sensor.py homeassistant/components/sigfox/sensor.py + homeassistant/components/signal_messenger/__init__.py + homeassistant/components/signal_messenger/notify.py homeassistant/components/simplepush/notify.py homeassistant/components/simplisafe/__init__.py homeassistant/components/simplisafe/alarm_control_panel.py @@ -634,7 +648,7 @@ omit = homeassistant/components/somfy/* homeassistant/components/somfy_mylink/* homeassistant/components/sonarr/sensor.py - homeassistant/components/songpal/media_player.py + homeassistant/components/songpal/* homeassistant/components/sonos/* homeassistant/components/sony_projector/switch.py homeassistant/components/spc/* @@ -642,7 +656,8 @@ omit = homeassistant/components/spider/* homeassistant/components/spotcrime/sensor.py homeassistant/components/spotify/media_player.py - homeassistant/components/squeezebox/media_player.py + homeassistant/components/squeezebox/* + homeassistant/components/starline/* homeassistant/components/starlingbank/sensor.py homeassistant/components/steam_online/sensor.py homeassistant/components/stiebel_eltron/* @@ -676,7 +691,14 @@ omit = homeassistant/components/telnet/switch.py homeassistant/components/temper/sensor.py homeassistant/components/tensorflow/image_processing.py - homeassistant/components/tesla/* + homeassistant/components/tesla/__init__.py + homeassistant/components/tesla/binary_sensor.py + homeassistant/components/tesla/climate.py + homeassistant/components/tesla/const.py + homeassistant/components/tesla/device_tracker.py + homeassistant/components/tesla/lock.py + homeassistant/components/tesla/sensor.py + homeassistant/components/tesla/switch.py homeassistant/components/tfiac/climate.py homeassistant/components/thermoworks_smoke/sensor.py homeassistant/components/thethingsnetwork/* @@ -688,6 +710,7 @@ omit = homeassistant/components/tile/device_tracker.py homeassistant/components/time_date/sensor.py homeassistant/components/todoist/calendar.py + homeassistant/components/todoist/const.py homeassistant/components/tof/sensor.py homeassistant/components/tomato/device_tracker.py homeassistant/components/toon/* @@ -695,7 +718,6 @@ omit = homeassistant/components/totalconnect/* homeassistant/components/touchline/climate.py homeassistant/components/tplink/device_tracker.py - homeassistant/components/tplink/light.py homeassistant/components/tplink/switch.py homeassistant/components/tplink_lte/* homeassistant/components/traccar/device_tracker.py @@ -736,12 +758,13 @@ omit = homeassistant/components/velbus/climate.py homeassistant/components/velbus/const.py homeassistant/components/velbus/cover.py + homeassistant/components/velbus/light.py homeassistant/components/velbus/sensor.py homeassistant/components/velbus/switch.py homeassistant/components/velux/* homeassistant/components/venstar/climate.py - homeassistant/components/vera/* homeassistant/components/verisure/* + homeassistant/components/versasense/* homeassistant/components/vesync/__init__.py homeassistant/components/vesync/common.py homeassistant/components/vesync/const.py @@ -763,7 +786,6 @@ omit = homeassistant/components/waze_travel_time/sensor.py homeassistant/components/webostv/* homeassistant/components/wemo/* - homeassistant/components/wemo/fan.py homeassistant/components/whois/sensor.py homeassistant/components/wink/* homeassistant/components/wirelesstag/* @@ -783,7 +805,6 @@ omit = homeassistant/components/xmpp/notify.py homeassistant/components/xs1/* homeassistant/components/yale_smart_alarm/alarm_control_panel.py - homeassistant/components/yamaha/media_player.py homeassistant/components/yamaha_musiccast/media_player.py homeassistant/components/yandex_transport/* homeassistant/components/yeelight/* diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 5bfd37fab36..7b56b66d0b5 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,4 +1,3 @@ -// See https://aka.ms/vscode-remote/devcontainer.json for format details. { "name": "Home Assistant Dev", "context": "..", diff --git a/.pre-commit-config-all.yaml b/.pre-commit-config-all.yaml index 3910835ae9d..1eabfcb0017 100644 --- a/.pre-commit-config-all.yaml +++ b/.pre-commit-config-all.yaml @@ -18,14 +18,31 @@ repos: - --safe - --quiet files: ^((homeassistant|script|tests)/.+)?[^/]+\.py$ -- repo: https://gitlab.com/pycqa/flake8 +- repo: https://github.com/PyCQA/flake8 rev: 3.7.9 hooks: - id: flake8 additional_dependencies: - flake8-docstrings==1.5.0 - - pydocstyle==4.0.1 + - pydocstyle==5.0.1 files: ^(homeassistant|script|tests)/.+\.py$ +- repo: https://github.com/PyCQA/bandit + rev: 1.6.2 + hooks: + - id: bandit + args: + - --quiet + - --format=custom + - --configfile=tests/bandit.yaml + files: ^(homeassistant|script|tests)/.+\.py$ +- repo: https://github.com/pre-commit/mirrors-isort + rev: v4.3.21 + hooks: + - id: isort +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.4.0 + hooks: + - id: check-json # Using a local "system" mypy instead of the mypy hook, because its # results depend on what is installed. And the mypy hook runs in a # virtualenv of its own, meaning we'd need to install and maintain diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3220ac84866..226708bb947 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,5 +20,22 @@ repos: - id: flake8 additional_dependencies: - flake8-docstrings==1.5.0 - - pydocstyle==4.0.1 + - pydocstyle==5.0.1 files: ^(homeassistant|script|tests)/.+\.py$ +- repo: https://github.com/PyCQA/bandit + rev: 1.6.2 + hooks: + - id: bandit + args: + - --quiet + - --format=custom + - --configfile=tests/bandit.yaml + files: ^(homeassistant|script|tests)/.+\.py$ +- repo: https://github.com/pre-commit/mirrors-isort + rev: v4.3.21 + hooks: + - id: isort +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.4.0 + hooks: + - id: check-json diff --git a/.readthedocs.yml b/.readthedocs.yml index 923a03f03dd..0303f84d51c 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -4,7 +4,7 @@ build: image: latest python: - version: 3.6 + version: 3.7 setup_py_install: true requirements_file: requirements_docs.txt diff --git a/.travis.yml b/.travis.yml index c9638b02a2f..6add8c15bfc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,7 @@ sudo: false -dist: xenial +dist: bionic addons: apt: - sources: - - sourceline: "ppa:jonathonf/ffmpeg-4" packages: - libudev-dev - libavformat-dev @@ -16,15 +14,13 @@ addons: matrix: fast_finish: true include: - - python: "3.6.1" + - python: "3.7.0" env: TOXENV=lint - - python: "3.6.1" + - python: "3.7.0" env: TOXENV=pylint PYLINT_ARGS=--jobs=0 TRAVIS_WAIT=30 - - python: "3.6.1" + - python: "3.7.0" env: TOXENV=typing - - python: "3.6.1" - env: TOXENV=py36 - - python: "3.7" + - python: "3.7.0" env: TOXENV=py37 cache: diff --git a/CODEOWNERS b/CODEOWNERS index ee6a8cd169c..23005cb5273 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -17,7 +17,6 @@ homeassistant/components/abode/* @shred86 homeassistant/components/adguard/* @frenck homeassistant/components/airly/* @bieniu homeassistant/components/airvisual/* @bachya -homeassistant/components/alarm_control_panel/* @colinodell homeassistant/components/alexa/* @home-assistant/cloud @ochlocracy homeassistant/components/almond/* @gcampax @balloob homeassistant/components/alpha_vantage/* @fabaff @@ -33,6 +32,7 @@ homeassistant/components/arcam_fmj/* @elupus homeassistant/components/arduino/* @fabaff homeassistant/components/arest/* @fabaff homeassistant/components/asuswrt/* @kennedyshead +homeassistant/components/aten_pe/* @mtdcr homeassistant/components/atome/* @baqs homeassistant/components/aurora_abb_powerone/* @davet2001 homeassistant/components/auth/* @home-assistant/core @@ -50,7 +50,7 @@ homeassistant/components/bizkaibus/* @UgaitzEtxebarria homeassistant/components/blink/* @fronzbot homeassistant/components/bmw_connected_drive/* @gerard33 homeassistant/components/braviatv/* @robbiet480 -homeassistant/components/broadlink/* @danielhiversen +homeassistant/components/broadlink/* @danielhiversen @felipediel homeassistant/components/brunt/* @eavanvalkenburg homeassistant/components/bt_smarthub/* @jxwolstenholme homeassistant/components/buienradar/* @mjj4791 @ties @@ -79,15 +79,19 @@ homeassistant/components/device_automation/* @home-assistant/core homeassistant/components/digital_ocean/* @fabaff homeassistant/components/discogs/* @thibmaek homeassistant/components/doorbird/* @oblogic7 +homeassistant/components/dsmr_reader/* @depl0y homeassistant/components/dweet/* @fabaff homeassistant/components/ecobee/* @marthoc homeassistant/components/ecovacs/* @OverloadUT homeassistant/components/egardia/* @jeroenterheerdt homeassistant/components/eight_sleep/* @mezz64 +homeassistant/components/elgato/* @frenck homeassistant/components/elv/* @majuss homeassistant/components/emby/* @mezz64 +homeassistant/components/emulated_hue/* @NobleKangaroo homeassistant/components/enigma2/* @fbradyirl homeassistant/components/enocean/* @bdurrer +homeassistant/components/entur_public_transport/* @hfurubotten homeassistant/components/environment_canada/* @michaeldavie homeassistant/components/ephember/* @ttroy50 homeassistant/components/epsonworkforce/* @ThaStealth @@ -95,11 +99,13 @@ homeassistant/components/eq3btsmart/* @rytilahti homeassistant/components/esphome/* @OttoWinter homeassistant/components/essent/* @TheLastProject homeassistant/components/evohome/* @zxdavb +homeassistant/components/fastdotcom/* @rohankapoorcom homeassistant/components/file/* @fabaff homeassistant/components/filter/* @dgomes homeassistant/components/fitbit/* @robbiet480 homeassistant/components/fixer/* @fabaff homeassistant/components/flock/* @fabaff +homeassistant/components/flume/* @ChrisMandich homeassistant/components/flunearyou/* @bachya homeassistant/components/fortigate/* @kifeo homeassistant/components/fortios/* @kimfrellsen @@ -112,6 +118,8 @@ homeassistant/components/gearbest/* @HerrHofrat homeassistant/components/geniushub/* @zxdavb homeassistant/components/geo_rss_events/* @exxamalte homeassistant/components/geonetnz_quakes/* @exxamalte +homeassistant/components/geonetnz_volcano/* @exxamalte +homeassistant/components/gios/* @bieniu homeassistant/components/gitter/* @fabaff homeassistant/components/glances/* @fabaff @engrbm87 homeassistant/components/gntp/* @robbiet480 @@ -125,6 +133,7 @@ homeassistant/components/growatt_server/* @indykoning homeassistant/components/gtfs/* @robbiet480 homeassistant/components/harmony/* @ehendrix23 homeassistant/components/hassio/* @home-assistant/hass-io +homeassistant/components/heatmiser/* @andylockran homeassistant/components/heos/* @andrewsayre homeassistant/components/here_travel_time/* @eifinger homeassistant/components/hikvision/* @mezz64 @@ -144,6 +153,7 @@ homeassistant/components/huawei_lte/* @scop homeassistant/components/huawei_router/* @abmantis homeassistant/components/hue/* @balloob homeassistant/components/iaqualink/* @flz +homeassistant/components/icloud/* @Quentame homeassistant/components/ign_sismologia/* @exxamalte homeassistant/components/incomfort/* @zxdavb homeassistant/components/influxdb/* @fabaff @@ -153,7 +163,10 @@ homeassistant/components/input_number/* @home-assistant/core homeassistant/components/input_select/* @home-assistant/core homeassistant/components/input_text/* @home-assistant/core homeassistant/components/integration/* @dgomes +homeassistant/components/intent/* @home-assistant/core +homeassistant/components/intesishome/* @jnimmo homeassistant/components/ios/* @robbiet480 +homeassistant/components/iperf3/* @rohankapoorcom homeassistant/components/ipma/* @dgomes homeassistant/components/iqvia/* @bachya homeassistant/components/irish_rail_transport/* @ttroy50 @@ -174,11 +187,13 @@ homeassistant/components/life360/* @pnbruckner homeassistant/components/linky/* @Quentame homeassistant/components/linux_battery/* @fabaff homeassistant/components/liveboxplaytv/* @pschmitt +homeassistant/components/local_ip/* @issacg homeassistant/components/logger/* @home-assistant/core homeassistant/components/logi_circle/* @evanjd homeassistant/components/lovelace/* @home-assistant/frontend homeassistant/components/luci/* @fbradyirl @mzdrale homeassistant/components/luftdaten/* @fabaff +homeassistant/components/lupusec/* @majuss homeassistant/components/lutron/* @JonGilmore homeassistant/components/mastodon/* @fabaff homeassistant/components/matrix/* @tinloaf @@ -193,6 +208,7 @@ homeassistant/components/mill/* @danielhiversen homeassistant/components/min_max/* @fabaff homeassistant/components/minio/* @tkislan homeassistant/components/mobile_app/* @robbiet480 +homeassistant/components/modbus/* @adamchengtkc homeassistant/components/monoprice/* @etsinko homeassistant/components/moon/* @fabaff homeassistant/components/mpd/* @fabaff @@ -206,6 +222,7 @@ homeassistant/components/ness_alarm/* @nickw444 homeassistant/components/nest/* @awarecan homeassistant/components/netdata/* @fabaff homeassistant/components/nextbus/* @vividboarder +homeassistant/components/nilu/* @hfurubotten homeassistant/components/nissan_leaf/* @filcole homeassistant/components/nmbs/* @thibmaek homeassistant/components/no_ip/* @fabaff @@ -220,6 +237,7 @@ homeassistant/components/obihai/* @dshokouhi homeassistant/components/ohmconnect/* @robbiet480 homeassistant/components/ombi/* @larssont homeassistant/components/onboarding/* @home-assistant/core +homeassistant/components/onewire/* @garbled1 homeassistant/components/opentherm_gw/* @mvn23 homeassistant/components/openuv/* @bachya homeassistant/components/openweathermap/* @fabaff @@ -237,6 +255,7 @@ homeassistant/components/plant/* @ChristianKuehnel homeassistant/components/plex/* @jjlawren homeassistant/components/plugwise/* @laetificat @CoMPaTech @bouwew homeassistant/components/point/* @fredrike +homeassistant/components/proxmoxve/* @k4ds3 homeassistant/components/ps4/* @ktnrg45 homeassistant/components/ptvsd/* @swamp-ig homeassistant/components/push/* @dgomes @@ -266,6 +285,7 @@ homeassistant/components/seventeentrack/* @bachya homeassistant/components/shell_command/* @home-assistant/core homeassistant/components/shiftr/* @fabaff homeassistant/components/shodan/* @fabaff +homeassistant/components/signal_messenger/* @bbernhard homeassistant/components/simplisafe/* @bachya homeassistant/components/sinch/* @bendikrb homeassistant/components/slide/* @ualex73 @@ -281,8 +301,10 @@ homeassistant/components/soma/* @ratsept homeassistant/components/somfy/* @tetienne homeassistant/components/songpal/* @rytilahti homeassistant/components/spaceapi/* @fabaff +homeassistant/components/speedtestdotnet/* @rohankapoorcom homeassistant/components/spider/* @peternijssen homeassistant/components/sql/* @dgomes +homeassistant/components/starline/* @anonym-tsk homeassistant/components/statistics/* @fabaff homeassistant/components/stiebel_eltron/* @fucm homeassistant/components/stream/* @hunterjm @@ -298,11 +320,12 @@ homeassistant/components/switchmate/* @danielhiversen homeassistant/components/syncthru/* @nielstron homeassistant/components/synology_srm/* @aerialls homeassistant/components/syslog/* @fabaff +homeassistant/components/tado/* @michaelarnauts homeassistant/components/tahoma/* @philklei homeassistant/components/tautulli/* @ludeeus homeassistant/components/tellduslive/* @fredrike homeassistant/components/template/* @PhracturedBlue -homeassistant/components/tesla/* @zabuldon +homeassistant/components/tesla/* @zabuldon @alandtse homeassistant/components/tfiac/* @fredrike @mellado homeassistant/components/thethingsnetwork/* @fabaff homeassistant/components/threshold/* @fabaff @@ -331,6 +354,7 @@ homeassistant/components/usgs_earthquakes_feed/* @exxamalte homeassistant/components/utility_meter/* @dgomes homeassistant/components/velbus/* @cereal2nd homeassistant/components/velux/* @Julius2342 +homeassistant/components/versasense/* @flamm3blemuff1n homeassistant/components/version/* @fabaff homeassistant/components/vesync/* @markperdue @webdjoe homeassistant/components/vicare/* @oischinger @@ -345,6 +369,7 @@ homeassistant/components/websocket_api/* @home-assistant/core homeassistant/components/wemo/* @sqldiablo homeassistant/components/withings/* @vangorra homeassistant/components/wled/* @frenck +homeassistant/components/workday/* @fabaff homeassistant/components/worldclock/* @fabaff homeassistant/components/wwlln/* @bachya homeassistant/components/xbox_live/* @MartinHjelmare diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fbe77c7756f..1921e5d38dd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,7 +4,7 @@ Everybody is invited and welcome to contribute to Home Assistant. There is a lot The process is straight-forward. - - Read [How to get faster PR reviews](https://github.com/kubernetes/community/blob/master/contributors/guide/pull-requests.md#best-practices-for-faster-reviews) by Kubernetes (but skip step 0) + - Read [How to get faster PR reviews](https://github.com/kubernetes/community/blob/master/contributors/guide/pull-requests.md#best-practices-for-faster-reviews) by Kubernetes (but skip step 0 and 1) - Fork the Home Assistant [git repository](https://github.com/home-assistant/home-assistant). - Write the code for your device, notification service, sensor, or IoT thing. - Ensure tests work. @@ -12,3 +12,7 @@ The process is straight-forward. Still interested? Then you should take a peek at the [developer documentation](https://developers.home-assistant.io/) to get more details. +## Feature suggestions + +If you want to suggest a new feature for Home Assistant (e.g., new integrations), please open a thread in our [Community Forum: Feature Requests](https://community.home-assistant.io/c/feature-requests). +We use [GitHub for tracking issues](https://github.com/home-assistant/home-assistant/issues), not for tracking feature requests. \ No newline at end of file diff --git a/README.rst b/README.rst index ae9531456fd..0de30d43c65 100644 --- a/README.rst +++ b/README.rst @@ -1,14 +1,7 @@ Home Assistant |Chat Status| ================================================================================= -Home Assistant is a home automation platform running on Python 3. It is able to track and control all devices at home and offer a platform for automating control. - -To get started: - -.. code:: bash - - python3 -m pip install homeassistant - hass --open-ui +Open source home automation that puts local control and privacy first. Powered by a worldwide community of tinkerers and DIY enthusiasts. Perfect to run on a Raspberry Pi or a local server. Check out `home-assistant.io `__ for `a demo `__, `installation instructions `__, diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index 37473b92620..546b63950fe 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -14,8 +14,6 @@ pr: resources: containers: - - container: 36 - image: homeassistant/ci-azure:3.6 - container: 37 image: homeassistant/ci-azure:3.7 repositories: @@ -25,7 +23,7 @@ resources: endpoint: 'home-assistant' variables: - name: PythonMain - value: '36' + value: '37' - group: codecov stages: @@ -50,6 +48,18 @@ stages: . venv/bin/activate pre-commit run flake8 --all-files displayName: 'Run flake8' + - script: | + . venv/bin/activate + pre-commit run bandit --all-files + displayName: 'Run bandit' + - script: | + . venv/bin/activate + pre-commit run isort --all-files --show-diff-on-failure + displayName: 'Run isort' + - script: | + . venv/bin/activate + pre-commit run check-json --all-files + displayName: 'Run check-json' - job: 'Validate' pool: vmImage: 'ubuntu-latest' @@ -87,7 +97,7 @@ stages: pre-commit install-hooks --config .pre-commit-config-all.yaml - script: | . venv/bin/activate - pre-commit run black --all-files + pre-commit run black --all-files --show-diff-on-failure displayName: 'Check Black formatting' - stage: 'Tests' @@ -100,8 +110,6 @@ stages: strategy: maxParallel: 3 matrix: - Python36: - python.container: '36' Python37: python.container: '37' container: $[ variables['python.container'] ] @@ -158,7 +166,7 @@ stages: python -m venv venv . venv/bin/activate - pip install -U pip setuptools + pip install -U pip setuptools wheel pip install -r requirements_all.txt -c homeassistant/package_constraints.txt pip install -r requirements_test.txt -c homeassistant/package_constraints.txt - script: | diff --git a/docs/source/_ext/edit_on_github.py b/docs/source/_ext/edit_on_github.py index eef249a3f01..a31fb13ebf1 100644 --- a/docs/source/_ext/edit_on_github.py +++ b/docs/source/_ext/edit_on_github.py @@ -8,7 +8,6 @@ Loosely based on https://github.com/astropy/astropy/pull/347 import os import warnings - __licence__ = 'BSD (3 clause)' diff --git a/docs/source/conf.py b/docs/source/conf.py index b5428ede8fa..f36b5b8124a 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -17,11 +17,11 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # -import sys -import os import inspect +import os +import sys -from homeassistant.const import __version__, __short_version__ +from homeassistant.const import __short_version__, __version__ PROJECT_NAME = 'Home Assistant' PROJECT_PACKAGE_NAME = 'homeassistant' diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index b416b1f98d3..a0243e2dd8c 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -1,26 +1,23 @@ """Start Home Assistant.""" import argparse +import asyncio import os import platform import subprocess import sys import threading -from typing import List, Dict, Any, TYPE_CHECKING +from typing import TYPE_CHECKING, Any, Dict, List -from homeassistant import monkey_patch -from homeassistant.const import __version__, REQUIRED_PYTHON_VER, RESTART_EXIT_CODE +from homeassistant.const import REQUIRED_PYTHON_VER, RESTART_EXIT_CODE, __version__ if TYPE_CHECKING: from homeassistant import core def set_loop() -> None: - """Attempt to use uvloop.""" - import asyncio + """Attempt to use different loop.""" from asyncio.events import BaseDefaultEventLoopPolicy - policy = None - if sys.platform == "win32": if hasattr(asyncio, "WindowsProactorEventLoopPolicy"): # pylint: disable=no-member @@ -33,15 +30,7 @@ def set_loop() -> None: _loop_factory = asyncio.ProactorEventLoop policy = ProactorPolicy() - else: - try: - import uvloop - except ImportError: - pass - else: - policy = uvloop.EventLoopPolicy() - if policy is not None: asyncio.set_event_loop_policy(policy) @@ -89,11 +78,7 @@ def ensure_config_path(config_dir: str) -> None: try: os.mkdir(lib_dir) except OSError: - print( - ("Fatal Error: Unable to create library " "directory {} ").format( - lib_dir - ) - ) + print("Fatal Error: Unable to create library directory {}".format(lib_dir)) sys.exit(1) @@ -158,7 +143,7 @@ def get_arguments() -> argparse.Namespace: "--log-file", type=str, default=None, - help="Log file to write to. If not set, CONFIG/home-assistant.log " "is used", + help="Log file to write to. If not set, CONFIG/home-assistant.log is used", ) parser.add_argument( "--log-no-color", action="store_true", help="Disable color logs" @@ -272,7 +257,6 @@ def cmdline() -> List[str]: async def setup_and_run_hass(config_dir: str, args: argparse.Namespace) -> int: """Set up HASS and run.""" - # pylint: disable=redefined-outer-name from homeassistant import bootstrap, core hass = core.HomeAssistant() @@ -356,11 +340,6 @@ def main() -> int: """Start Home Assistant.""" validate_python() - monkey_patch_needed = sys.version_info[:3] < (3, 6, 3) - if monkey_patch_needed and os.environ.get("HASS_NO_MONKEY") != "1": - monkey_patch.disable_c_asyncio() - monkey_patch.patch_weakref_tasks() - set_loop() # Run a simple daemon runner process on Windows to handle restarts @@ -394,13 +373,11 @@ def main() -> int: if args.pid_file: write_pid(args.pid_file) - from homeassistant.util.async_ import asyncio_run - - exit_code = asyncio_run(setup_and_run_hass(config_dir, args)) + exit_code = asyncio.run(setup_and_run_hass(config_dir, args)) if exit_code == RESTART_EXIT_CODE and not args.runner: try_to_restart() - return exit_code # type: ignore + return exit_code if __name__ == "__main__": diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index 3f7dd570400..e4437bea840 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -1,21 +1,21 @@ """Provide an authentication layer for Home Assistant.""" import asyncio -import logging from collections import OrderedDict from datetime import timedelta +import logging from typing import Any, Dict, List, Optional, Tuple, cast import jwt from homeassistant import data_entry_flow from homeassistant.auth.const import ACCESS_TOKEN_EXPIRATION -from homeassistant.core import callback, HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.util import dt as dt_util from . import auth_store, models from .const import GROUP_ID_ADMIN -from .mfa_modules import auth_mfa_module_from_config, MultiFactorAuthModule -from .providers import auth_provider_from_config, AuthProvider, LoginFlow +from .mfa_modules import MultiFactorAuthModule, auth_mfa_module_from_config +from .providers import AuthProvider, LoginFlow, auth_provider_from_config EVENT_USER_ADDED = "user_added" EVENT_USER_REMOVED = "user_removed" diff --git a/homeassistant/auth/auth_store.py b/homeassistant/auth/auth_store.py index 4c64730edda..57ec9ee63dc 100644 --- a/homeassistant/auth/auth_store.py +++ b/homeassistant/auth/auth_store.py @@ -11,7 +11,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.util import dt as dt_util from . import models -from .const import GROUP_ID_ADMIN, GROUP_ID_USER, GROUP_ID_READ_ONLY +from .const import GROUP_ID_ADMIN, GROUP_ID_READ_ONLY, GROUP_ID_USER from .permissions import PermissionLookup, system_policies from .permissions.types import PolicyType diff --git a/homeassistant/auth/mfa_modules/__init__.py b/homeassistant/auth/mfa_modules/__init__.py index 9d49f67df82..fd9e61b9d17 100644 --- a/homeassistant/auth/mfa_modules/__init__.py +++ b/homeassistant/auth/mfa_modules/__init__.py @@ -7,7 +7,7 @@ from typing import Any, Dict, Optional import voluptuous as vol from voluptuous.humanize import humanize_error -from homeassistant import requirements, data_entry_flow +from homeassistant import data_entry_flow, requirements from homeassistant.const import CONF_ID, CONF_NAME, CONF_TYPE from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -42,7 +42,7 @@ class MultiFactorAuthModule: self.config = config @property - def id(self) -> str: # pylint: disable=invalid-name + def id(self) -> str: """Return id of the auth module. Default is same as type diff --git a/homeassistant/auth/mfa_modules/insecure_example.py b/homeassistant/auth/mfa_modules/insecure_example.py index a3f0d58c6b3..45cc07ae581 100644 --- a/homeassistant/auth/mfa_modules/insecure_example.py +++ b/homeassistant/auth/mfa_modules/insecure_example.py @@ -7,9 +7,9 @@ import voluptuous as vol from homeassistant.core import HomeAssistant from . import ( - MultiFactorAuthModule, - MULTI_FACTOR_AUTH_MODULES, MULTI_FACTOR_AUTH_MODULE_SCHEMA, + MULTI_FACTOR_AUTH_MODULES, + MultiFactorAuthModule, SetupFlow, ) diff --git a/homeassistant/auth/mfa_modules/notify.py b/homeassistant/auth/mfa_modules/notify.py index b14f5fedc22..46cc634bcae 100644 --- a/homeassistant/auth/mfa_modules/notify.py +++ b/homeassistant/auth/mfa_modules/notify.py @@ -3,9 +3,9 @@ Sending HOTP through notify service """ import asyncio -import logging from collections import OrderedDict -from typing import Any, Dict, Optional, List +import logging +from typing import Any, Dict, List, Optional import attr import voluptuous as vol @@ -16,9 +16,9 @@ from homeassistant.exceptions import ServiceNotFound from homeassistant.helpers import config_validation as cv from . import ( - MultiFactorAuthModule, - MULTI_FACTOR_AUTH_MODULES, MULTI_FACTOR_AUTH_MODULE_SCHEMA, + MULTI_FACTOR_AUTH_MODULES, + MultiFactorAuthModule, SetupFlow, ) diff --git a/homeassistant/auth/mfa_modules/totp.py b/homeassistant/auth/mfa_modules/totp.py index 9b0f3910e92..6abddd2123f 100644 --- a/homeassistant/auth/mfa_modules/totp.py +++ b/homeassistant/auth/mfa_modules/totp.py @@ -1,7 +1,7 @@ """Time-based One Time Password auth module.""" import asyncio -import logging from io import BytesIO +import logging from typing import Any, Dict, Optional, Tuple import voluptuous as vol @@ -10,9 +10,9 @@ from homeassistant.auth.models import User from homeassistant.core import HomeAssistant from . import ( - MultiFactorAuthModule, - MULTI_FACTOR_AUTH_MODULES, MULTI_FACTOR_AUTH_MODULE_SCHEMA, + MULTI_FACTOR_AUTH_MODULES, + MultiFactorAuthModule, SetupFlow, ) diff --git a/homeassistant/auth/models.py b/homeassistant/auth/models.py index 6889d17a25f..08f2f375b41 100644 --- a/homeassistant/auth/models.py +++ b/homeassistant/auth/models.py @@ -1,5 +1,6 @@ """Auth models.""" from datetime import datetime, timedelta +import secrets from typing import Dict, List, NamedTuple, Optional import uuid @@ -9,7 +10,6 @@ from homeassistant.util import dt as dt_util from . import permissions as perm_mdl from .const import GROUP_ID_ADMIN -from .util import generate_secret TOKEN_TYPE_NORMAL = "normal" TOKEN_TYPE_SYSTEM = "system" @@ -96,8 +96,8 @@ class RefreshToken: ) id = attr.ib(type=str, factory=lambda: uuid.uuid4().hex) created_at = attr.ib(type=datetime, factory=dt_util.utcnow) - token = attr.ib(type=str, factory=lambda: generate_secret(64)) - jwt_key = attr.ib(type=str, factory=lambda: generate_secret(64)) + token = attr.ib(type=str, factory=lambda: secrets.token_hex(64)) + jwt_key = attr.ib(type=str, factory=lambda: secrets.token_hex(64)) last_used_at = attr.ib(type=Optional[datetime], default=None) last_used_ip = attr.ib(type=Optional[str], default=None) diff --git a/homeassistant/auth/permissions/__init__.py b/homeassistant/auth/permissions/__init__.py index f0e093953e1..92d02c75b91 100644 --- a/homeassistant/auth/permissions/__init__.py +++ b/homeassistant/auth/permissions/__init__.py @@ -5,13 +5,12 @@ from typing import Any, Callable, Optional import voluptuous as vol from .const import CAT_ENTITIES -from .models import PermissionLookup -from .types import PolicyType from .entities import ENTITY_POLICY_SCHEMA, compile_entities from .merge import merge_policies # noqa: F401 +from .models import PermissionLookup +from .types import PolicyType from .util import test_all - POLICY_SCHEMA = vol.Schema({vol.Optional(CAT_ENTITIES): ENTITY_POLICY_SCHEMA}) _LOGGER = logging.getLogger(__name__) @@ -58,15 +57,12 @@ class PolicyPermissions(AbstractPermissions): def __eq__(self, other: Any) -> bool: """Equals check.""" - # pylint: disable=protected-access return isinstance(other, PolicyPermissions) and other._policy == self._policy class _OwnerPermissions(AbstractPermissions): """Owner permissions.""" - # pylint: disable=no-self-use - def access_all_entities(self, key: str) -> bool: """Check if we have a certain access to all entities.""" return True diff --git a/homeassistant/auth/permissions/entities.py b/homeassistant/auth/permissions/entities.py index add9913abf3..be30c7bf69a 100644 --- a/homeassistant/auth/permissions/entities.py +++ b/homeassistant/auth/permissions/entities.py @@ -4,11 +4,10 @@ from typing import Callable, Optional import voluptuous as vol -from .const import SUBCAT_ALL, POLICY_READ, POLICY_CONTROL, POLICY_EDIT +from .const import POLICY_CONTROL, POLICY_EDIT, POLICY_READ, SUBCAT_ALL from .models import PermissionLookup from .types import CategoryType, SubCategoryDict, ValueType - -from .util import SubCatLookupType, lookup_all, compile_policy +from .util import SubCatLookupType, compile_policy, lookup_all SINGLE_ENTITY_SCHEMA = vol.Any( True, diff --git a/homeassistant/auth/permissions/merge.py b/homeassistant/auth/permissions/merge.py index 3cf02e05771..fad98b3f22a 100644 --- a/homeassistant/auth/permissions/merge.py +++ b/homeassistant/auth/permissions/merge.py @@ -1,7 +1,7 @@ """Merging of policies.""" -from typing import cast, Dict, List, Set +from typing import Dict, List, Set, cast -from .types import PolicyType, CategoryType +from .types import CategoryType, PolicyType def merge_policies(policies: List[PolicyType]) -> PolicyType: diff --git a/homeassistant/auth/permissions/system_policies.py b/homeassistant/auth/permissions/system_policies.py index b40400304cc..b45984653fb 100644 --- a/homeassistant/auth/permissions/system_policies.py +++ b/homeassistant/auth/permissions/system_policies.py @@ -1,5 +1,5 @@ """System policies.""" -from .const import CAT_ENTITIES, SUBCAT_ALL, POLICY_READ +from .const import CAT_ENTITIES, POLICY_READ, SUBCAT_ALL ADMIN_POLICY = {CAT_ENTITIES: True} diff --git a/homeassistant/auth/permissions/util.py b/homeassistant/auth/permissions/util.py index 4d38e0a639c..11bbd878eb2 100644 --- a/homeassistant/auth/permissions/util.py +++ b/homeassistant/auth/permissions/util.py @@ -1,6 +1,5 @@ """Helpers to deal with permissions.""" from functools import wraps - from typing import Callable, Dict, List, Optional, cast from .const import SUBCAT_ALL diff --git a/homeassistant/auth/providers/__init__.py b/homeassistant/auth/providers/__init__.py index 3e25003ad00..bb0fc55b5c4 100644 --- a/homeassistant/auth/providers/__init__.py +++ b/homeassistant/auth/providers/__init__.py @@ -8,8 +8,8 @@ import voluptuous as vol from voluptuous.humanize import humanize_error from homeassistant import data_entry_flow, requirements -from homeassistant.core import callback, HomeAssistant from homeassistant.const import CONF_ID, CONF_NAME, CONF_TYPE +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.util import dt as dt_util from homeassistant.util.decorator import Registry @@ -48,7 +48,7 @@ class AuthProvider: self.config = config @property - def id(self) -> Optional[str]: # pylint: disable=invalid-name + def id(self) -> Optional[str]: """Return id of the auth provider. Optional, can be None. diff --git a/homeassistant/auth/providers/command_line.py b/homeassistant/auth/providers/command_line.py index 58a2cac1fc5..12e27c01504 100644 --- a/homeassistant/auth/providers/command_line.py +++ b/homeassistant/auth/providers/command_line.py @@ -1,20 +1,18 @@ """Auth provider that validates credentials via an external command.""" -from typing import Any, Dict, Optional, cast - import asyncio.subprocess import collections import logging import os +from typing import Any, Dict, Optional, cast import voluptuous as vol from homeassistant.exceptions import HomeAssistantError -from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow +from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow from ..models import Credentials, UserMeta - CONF_COMMAND = "command" CONF_ARGS = "args" CONF_META = "meta" @@ -78,7 +76,7 @@ class CommandLineAuthProvider(AuthProvider): if process.returncode != 0: _LOGGER.error( - "User %r failed to authenticate, command exited " "with code %d.", + "User %r failed to authenticate, command exited with code %d.", username, process.returncode, ) diff --git a/homeassistant/auth/providers/homeassistant.py b/homeassistant/auth/providers/homeassistant.py index 265a24a4b28..9ddbf4189f7 100644 --- a/homeassistant/auth/providers/homeassistant.py +++ b/homeassistant/auth/providers/homeassistant.py @@ -3,21 +3,18 @@ import asyncio import base64 from collections import OrderedDict import logging - from typing import Any, Dict, List, Optional, Set, cast import bcrypt import voluptuous as vol from homeassistant.const import CONF_ID -from homeassistant.core import callback, HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow - +from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow from ..models import Credentials, UserMeta - STORAGE_VERSION = 1 STORAGE_KEY = "auth_provider.homeassistant" diff --git a/homeassistant/auth/providers/insecure_example.py b/homeassistant/auth/providers/insecure_example.py index 37859f5ed0e..70014a236cd 100644 --- a/homeassistant/auth/providers/insecure_example.py +++ b/homeassistant/auth/providers/insecure_example.py @@ -5,13 +5,12 @@ from typing import Any, Dict, Optional, cast import voluptuous as vol -from homeassistant.exceptions import HomeAssistantError from homeassistant.core import callback +from homeassistant.exceptions import HomeAssistantError -from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow +from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow from ..models import Credentials, UserMeta - USER_SCHEMA = vol.Schema( { vol.Required("username"): str, diff --git a/homeassistant/auth/providers/legacy_api_password.py b/homeassistant/auth/providers/legacy_api_password.py index 018886388df..15ba1dfc14c 100644 --- a/homeassistant/auth/providers/legacy_api_password.py +++ b/homeassistant/auth/providers/legacy_api_password.py @@ -12,9 +12,9 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv -from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow +from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow from .. import AuthManager -from ..models import Credentials, UserMeta, User +from ..models import Credentials, User, UserMeta AUTH_PROVIDER_TYPE = "legacy_api_password" CONF_API_PASSWORD = "api_password" diff --git a/homeassistant/auth/providers/trusted_networks.py b/homeassistant/auth/providers/trusted_networks.py index f71be436acf..bc995368fec 100644 --- a/homeassistant/auth/providers/trusted_networks.py +++ b/homeassistant/auth/providers/trusted_networks.py @@ -3,15 +3,16 @@ It shows list of users if access from trusted network. Abort login flow if not access from trusted network. """ -from ipaddress import ip_network, IPv4Address, IPv6Address, IPv4Network, IPv6Network +from ipaddress import IPv4Address, IPv4Network, IPv6Address, IPv6Network, ip_network from typing import Any, Dict, List, Optional, Union, cast import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError -from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow +import homeassistant.helpers.config_validation as cv + +from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow from ..models import Credentials, UserMeta IPAddress = Union[IPv4Address, IPv6Address] diff --git a/homeassistant/auth/util.py b/homeassistant/auth/util.py deleted file mode 100644 index 83834fa7683..00000000000 --- a/homeassistant/auth/util.py +++ /dev/null @@ -1,13 +0,0 @@ -"""Auth utils.""" -import binascii -import os - - -def generate_secret(entropy: int = 32) -> str: - """Generate a secret. - - Backport of secrets.token_hex from Python 3.6 - - Event loop friendly. - """ - return binascii.hexlify(os.urandom(entropy)).decode("ascii") diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 312c739cd72..48ca96c7254 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -1,22 +1,26 @@ """Provide methods to bootstrap a Home Assistant instance.""" import asyncio +from collections import OrderedDict import logging import logging.handlers import os import sys from time import time -from collections import OrderedDict -from typing import Any, Optional, Dict, Set +from typing import Any, Dict, Optional, Set import voluptuous as vol -from homeassistant import core, config as conf_util, config_entries, loader -from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE +from homeassistant import config as conf_util, config_entries, core, loader +from homeassistant.const import ( + EVENT_HOMEASSISTANT_CLOSE, + REQUIRED_NEXT_PYTHON_DATE, + REQUIRED_NEXT_PYTHON_VER, +) +from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component from homeassistant.util.logging import AsyncHandler from homeassistant.util.package import async_get_user_site, is_virtual_env from homeassistant.util.yaml import clear_secret_cache -from homeassistant.exceptions import HomeAssistantError _LOGGER = logging.getLogger(__name__) @@ -62,7 +66,7 @@ async def async_from_config_dict( hass.config.skip_pip = skip_pip if skip_pip: _LOGGER.warning( - "Skipping pip installation of required modules. " "This may cause issues" + "Skipping pip installation of required modules. This may cause issues" ) core_config = config.get(core.DOMAIN, {}) @@ -95,11 +99,14 @@ async def async_from_config_dict( stop = time() _LOGGER.info("Home Assistant initialized in %.2fs", stop - start) - if sys.version_info[:3] < (3, 7, 0): + if REQUIRED_NEXT_PYTHON_DATE and sys.version_info[:3] < REQUIRED_NEXT_PYTHON_VER: msg = ( - "Python 3.6 support is deprecated and will " - "be removed in the first release after December 15, 2019. Please " - "upgrade Python to 3.7.0 or higher." + "Support for the running Python version " + f"{'.'.join(str(x) for x in sys.version_info[:3])} is deprecated and will " + f"be removed in the first release after {REQUIRED_NEXT_PYTHON_DATE}. " + "Please upgrade Python to " + f"{'.'.join(str(x) for x in REQUIRED_NEXT_PYTHON_VER)} or " + "higher." ) _LOGGER.warning(msg) hass.components.persistent_notification.async_create( @@ -161,7 +168,7 @@ def async_enable_logging( This method must be run in the event loop. """ - fmt = "%(asctime)s %(levelname)s (%(threadName)s) " "[%(name)s] %(message)s" + fmt = "%(asctime)s %(levelname)s (%(threadName)s) [%(name)s] %(message)s" datefmt = "%Y-%m-%d %H:%M:%S" if not log_no_color: diff --git a/homeassistant/components/__init__.py b/homeassistant/components/__init__.py index 7bb572dcf6b..90e0f32226c 100644 --- a/homeassistant/components/__init__.py +++ b/homeassistant/components/__init__.py @@ -11,7 +11,6 @@ import logging from homeassistant.core import split_entity_id - # mypy: allow-untyped-defs _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/abode/.translations/da.json b/homeassistant/components/abode/.translations/da.json index 3f094cb93bd..4a5fa763ea1 100644 --- a/homeassistant/components/abode/.translations/da.json +++ b/homeassistant/components/abode/.translations/da.json @@ -12,7 +12,7 @@ "user": { "data": { "password": "Adgangskode", - "username": "Email adresse" + "username": "Email-adresse" }, "title": "Udfyld dine Abode-loginoplysninger" } diff --git a/homeassistant/components/abode/.translations/ru.json b/homeassistant/components/abode/.translations/ru.json index f39e6b1443b..590f7662731 100644 --- a/homeassistant/components/abode/.translations/ru.json +++ b/homeassistant/components/abode/.translations/ru.json @@ -5,8 +5,8 @@ }, "error": { "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a Abode.", - "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_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435." + "identifier_exists": "\u0423\u0447\u0451\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_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435." }, "step": { "user": { diff --git a/homeassistant/components/abode/__init__.py b/homeassistant/components/abode/__init__.py index 1689576bc7f..6d23701d088 100644 --- a/homeassistant/components/abode/__init__.py +++ b/homeassistant/components/abode/__init__.py @@ -20,14 +20,14 @@ from homeassistant.const import ( CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.entity import Entity from .const import ( ATTRIBUTION, - DOMAIN, DEFAULT_CACHEDB, + DOMAIN, SIGNAL_CAPTURE_IMAGE, SIGNAL_TRIGGER_QUICK_ACTION, ) diff --git a/homeassistant/components/abode/alarm_control_panel.py b/homeassistant/components/abode/alarm_control_panel.py index f1ff08f3a0a..88a072bd79c 100644 --- a/homeassistant/components/abode/alarm_control_panel.py +++ b/homeassistant/components/abode/alarm_control_panel.py @@ -2,6 +2,10 @@ import logging import homeassistant.components.alarm_control_panel as alarm +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, +) from homeassistant.const import ( ATTR_ATTRIBUTION, STATE_ALARM_ARMED_AWAY, @@ -51,6 +55,11 @@ class AbodeAlarm(AbodeDevice, alarm.AlarmControlPanel): state = None return state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY + def alarm_disarm(self, code=None): """Send disarm command.""" self._device.set_standby() diff --git a/homeassistant/components/abode/config_flow.py b/homeassistant/components/abode/config_flow.py index bf48e4546b3..89b389798f6 100644 --- a/homeassistant/components/abode/config_flow.py +++ b/homeassistant/components/abode/config_flow.py @@ -10,7 +10,7 @@ from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback -from .const import DOMAIN, DEFAULT_CACHEDB # pylint: disable=W0611 +from .const import DEFAULT_CACHEDB, DOMAIN # pylint: disable=unused-import CONF_POLLING = "polling" diff --git a/homeassistant/components/abode/sensor.py b/homeassistant/components/abode/sensor.py index d84bfe52441..573df6d49b4 100644 --- a/homeassistant/components/abode/sensor.py +++ b/homeassistant/components/abode/sensor.py @@ -72,19 +72,19 @@ class AbodeSensor(AbodeDevice): @property def state(self): """Return the state of the sensor.""" - if self._sensor_type == "temp": + if self._sensor_type == CONST.TEMP_STATUS_KEY: return self._device.temp - if self._sensor_type == "humidity": + if self._sensor_type == CONST.HUMI_STATUS_KEY: return self._device.humidity - if self._sensor_type == "lux": + if self._sensor_type == CONST.LUX_STATUS_KEY: return self._device.lux @property def unit_of_measurement(self): """Return the units of measurement.""" - if self._sensor_type == "temp": + if self._sensor_type == CONST.TEMP_STATUS_KEY: return self._device.temp_unit - if self._sensor_type == "humidity": + if self._sensor_type == CONST.HUMI_STATUS_KEY: return self._device.humidity_unit - if self._sensor_type == "lux": + if self._sensor_type == CONST.LUX_STATUS_KEY: return self._device.lux_unit diff --git a/homeassistant/components/acer_projector/switch.py b/homeassistant/components/acer_projector/switch.py index 39a79636c93..b28d67562d4 100644 --- a/homeassistant/components/acer_projector/switch.py +++ b/homeassistant/components/acer_projector/switch.py @@ -1,17 +1,17 @@ """Use serial protocol of Acer projector to obtain state of the projector.""" import logging import re -import serial +import serial import voluptuous as vol -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import ( - STATE_ON, - STATE_OFF, - STATE_UNKNOWN, - CONF_NAME, CONF_FILENAME, + CONF_NAME, + STATE_OFF, + STATE_ON, + STATE_UNKNOWN, ) import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/actiontec/device_tracker.py b/homeassistant/components/actiontec/device_tracker.py index e07dd2622be..302a8d56173 100644 --- a/homeassistant/components/actiontec/device_tracker.py +++ b/homeassistant/components/actiontec/device_tracker.py @@ -1,18 +1,19 @@ """Support for Actiontec MI424WR (Verizon FIOS) routers.""" +from collections import namedtuple import logging import re import telnetlib -from collections import namedtuple + import voluptuous as vol -import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt_util from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/adguard/.translations/da.json b/homeassistant/components/adguard/.translations/da.json index 813405cec62..e9e6415518d 100644 --- a/homeassistant/components/adguard/.translations/da.json +++ b/homeassistant/components/adguard/.translations/da.json @@ -1,16 +1,18 @@ { "config": { "abort": { + "adguard_home_addon_outdated": "Denne integration kr\u00e6ver AdGuard Home {minimal_version} eller h\u00f8jere, du har {current_version}. Opdater venligst din Hass.io AdGuard Home-tilf\u00f8jelse.", + "adguard_home_outdated": "Denne integration kr\u00e6ver AdGuard Home {minimal_version} eller h\u00f8jere, du har {current_version}.", "existing_instance_updated": "Opdaterede eksisterende konfiguration.", - "single_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning af AdGuard Home." + "single_instance_allowed": "Kun en enkelt konfiguration af AdGuard Home er tilladt." }, "error": { "connection_error": "Forbindelse mislykkedes." }, "step": { "hassio_confirm": { - "description": "Vil du konfigurere Home Assistant til at oprette forbindelse til AdGuard Home, der leveres af Hass.io add-on: {addon}?", - "title": "AdGuard Home via Hass.io add-on" + "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" }, "user": { "data": { @@ -21,8 +23,8 @@ "username": "Brugernavn", "verify_ssl": "AdGuard Home bruger et korrekt certifikat" }, - "description": "Konfigurer din AdGuard Home instans for at tillade overv\u00e5gning og kontrol.", - "title": "Link AdGuard Home." + "description": "Konfigurer din AdGuard Home-instans for at tillade overv\u00e5gning og kontrol.", + "title": "Forbind din AdGuard Home." } }, "title": "AdGuard Home" diff --git a/homeassistant/components/adguard/.translations/it.json b/homeassistant/components/adguard/.translations/it.json index 1b3ce014d90..6dc6ae18d81 100644 --- a/homeassistant/components/adguard/.translations/it.json +++ b/homeassistant/components/adguard/.translations/it.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "adguard_home_addon_outdated": "Questa integrazione richiede AdGuard Home {minimal_version} o versione successiva, si dispone di {current_version}. Aggiorna il componente aggiuntivo Hass.io AdGuard Home.", + "adguard_home_addon_outdated": "Questa integrazione richiede AdGuard Home {minimal_version} o versione successiva, si dispone di {current_version}. Aggiorna il componente aggiuntivo AdGuard Home di Hass.io.", "adguard_home_outdated": "Questa integrazione richiede AdGuard Home {minimal_version} o versione successiva, si dispone di {current_version}.", "existing_instance_updated": "Configurazione esistente aggiornata.", "single_instance_allowed": "\u00c8 consentita solo una singola configurazione di AdGuard Home." @@ -11,7 +11,7 @@ }, "step": { "hassio_confirm": { - "description": "Vuoi configurare Home Assistant per connettersi alla AdGuard Home fornita dal componente aggiuntivo di Hass.io: {addon} ?", + "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" }, "user": { diff --git a/homeassistant/components/adguard/sensor.py b/homeassistant/components/adguard/sensor.py index e0c86e42d26..c818752ad2f 100644 --- a/homeassistant/components/adguard/sensor.py +++ b/homeassistant/components/adguard/sensor.py @@ -178,7 +178,7 @@ class AdGuardHomeReplacedSafeSearchSensor(AdGuardHomeSensor): """Initialize AdGuard Home sensor.""" super().__init__( adguard, - "Searches Safe Search Enforced", + "AdGuard Safe Searches Enforced", "mdi:shield-search", "enforced_safesearch", "requests", diff --git a/homeassistant/components/ads/__init__.py b/homeassistant/components/ads/__init__.py index ba4762da84a..adaaaa08b7f 100644 --- a/homeassistant/components/ads/__init__.py +++ b/homeassistant/components/ads/__init__.py @@ -1,14 +1,13 @@ """Support for Automation Device Specification (ADS).""" -import threading -import struct -import logging -import ctypes -from collections import namedtuple import asyncio +from collections import namedtuple +import ctypes +import logging +import struct +import threading + import async_timeout - import pyads - import voluptuous as vol from homeassistant.const import ( diff --git a/homeassistant/components/ads/binary_sensor.py b/homeassistant/components/ads/binary_sensor.py index 2afb163fc32..fd6d77873b5 100644 --- a/homeassistant/components/ads/binary_sensor.py +++ b/homeassistant/components/ads/binary_sensor.py @@ -11,7 +11,7 @@ from homeassistant.components.binary_sensor import ( from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME import homeassistant.helpers.config_validation as cv -from . import CONF_ADS_VAR, DATA_ADS, AdsEntity, STATE_KEY_STATE +from . import CONF_ADS_VAR, DATA_ADS, STATE_KEY_STATE, AdsEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/ads/cover.py b/homeassistant/components/ads/cover.py index b21c064e941..0fdcbc16ef8 100644 --- a/homeassistant/components/ads/cover.py +++ b/homeassistant/components/ads/cover.py @@ -4,25 +4,25 @@ import logging import voluptuous as vol from homeassistant.components.cover import ( - PLATFORM_SCHEMA, - SUPPORT_OPEN, - SUPPORT_CLOSE, - SUPPORT_STOP, - SUPPORT_SET_POSITION, ATTR_POSITION, DEVICE_CLASSES_SCHEMA, + PLATFORM_SCHEMA, + SUPPORT_CLOSE, + SUPPORT_OPEN, + SUPPORT_SET_POSITION, + SUPPORT_STOP, CoverDevice, ) -from homeassistant.const import CONF_NAME, CONF_DEVICE_CLASS +from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME import homeassistant.helpers.config_validation as cv from . import ( CONF_ADS_VAR, CONF_ADS_VAR_POSITION, DATA_ADS, - AdsEntity, - STATE_KEY_STATE, STATE_KEY_POSITION, + STATE_KEY_STATE, + AdsEntity, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/ads/light.py b/homeassistant/components/ads/light.py index f1e78ea132e..b9626b9e969 100644 --- a/homeassistant/components/ads/light.py +++ b/homeassistant/components/ads/light.py @@ -16,9 +16,9 @@ from . import ( CONF_ADS_VAR, CONF_ADS_VAR_BRIGHTNESS, DATA_ADS, - AdsEntity, STATE_KEY_BRIGHTNESS, STATE_KEY_STATE, + AdsEntity, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/ads/sensor.py b/homeassistant/components/ads/sensor.py index 1d956b1fba2..3bdcbfc95f8 100644 --- a/homeassistant/components/ads/sensor.py +++ b/homeassistant/components/ads/sensor.py @@ -8,7 +8,7 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT import homeassistant.helpers.config_validation as cv -from . import CONF_ADS_FACTOR, CONF_ADS_TYPE, CONF_ADS_VAR, AdsEntity, STATE_KEY_STATE +from . import CONF_ADS_FACTOR, CONF_ADS_TYPE, CONF_ADS_VAR, STATE_KEY_STATE, AdsEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/ads/switch.py b/homeassistant/components/ads/switch.py index 1b5a40debb6..3590b6af88e 100644 --- a/homeassistant/components/ads/switch.py +++ b/homeassistant/components/ads/switch.py @@ -3,11 +3,11 @@ import logging import voluptuous as vol -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import CONF_NAME import homeassistant.helpers.config_validation as cv -from . import CONF_ADS_VAR, DATA_ADS, AdsEntity, STATE_KEY_STATE +from . import CONF_ADS_VAR, DATA_ADS, STATE_KEY_STATE, AdsEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/aftership/sensor.py b/homeassistant/components/aftership/sensor.py index c41e5aec7b5..ff5edc92ba0 100644 --- a/homeassistant/components/aftership/sensor.py +++ b/homeassistant/components/aftership/sensor.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +from pyaftership.tracker import Tracking import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -11,6 +12,7 @@ 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 _LOGGER = logging.getLogger(__name__) @@ -56,8 +58,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the AfterShip sensor platform.""" - from pyaftership.tracker import Tracking - apikey = config[CONF_API_KEY] name = config[CONF_NAME] diff --git a/homeassistant/components/air_quality/__init__.py b/homeassistant/components/air_quality/__init__.py index 00308c40b36..29c6756260c 100644 --- a/homeassistant/components/air_quality/__init__.py +++ b/homeassistant/components/air_quality/__init__.py @@ -2,12 +2,12 @@ from datetime import timedelta import logging -from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_component import EntityComponent _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/airly/.translations/da.json b/homeassistant/components/airly/.translations/da.json index 652cc46a7b3..c2c14d1d101 100644 --- a/homeassistant/components/airly/.translations/da.json +++ b/homeassistant/components/airly/.translations/da.json @@ -3,7 +3,7 @@ "error": { "auth": "API-n\u00f8glen er ikke korrekt.", "name_exists": "Navnet findes allerede.", - "wrong_location": "Ingen Airly m\u00e5lestationer i dette omr\u00e5de." + "wrong_location": "Ingen Airly-m\u00e5lestationer i dette omr\u00e5de." }, "step": { "user": { @@ -13,7 +13,7 @@ "longitude": "L\u00e6ngdegrad", "name": "Integrationens navn" }, - "description": "Konfigurer Airly luftkvalitet integration. For at generere API-n\u00f8gle, g\u00e5 til https://developer.airly.eu/register", + "description": "Konfigurer Airly luftkvalitetsintegration. For at generere API-n\u00f8gle, g\u00e5 til https://developer.airly.eu/register", "title": "Airly" } }, diff --git a/homeassistant/components/airly/__init__.py b/homeassistant/components/airly/__init__.py index ce165918ac2..17e1d27e571 100644 --- a/homeassistant/components/airly/__init__.py +++ b/homeassistant/components/airly/__init__.py @@ -10,7 +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.util import Throttle @@ -48,9 +47,6 @@ async def async_setup_entry(hass, config_entry): await airly.async_update() - if not airly.data: - raise ConfigEntryNotReady() - hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = airly hass.async_create_task( diff --git a/homeassistant/components/airly/air_quality.py b/homeassistant/components/airly/air_quality.py index 082344c14e3..b48a360da28 100644 --- a/homeassistant/components/airly/air_quality.py +++ b/homeassistant/components/airly/air_quality.py @@ -1,11 +1,11 @@ """Support for the Airly air_quality service.""" from homeassistant.components.air_quality import ( - AirQualityEntity, ATTR_AQI, - ATTR_PM_10, ATTR_PM_2_5, + ATTR_PM_10, + AirQualityEntity, ) -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from .const import ( ATTR_API_ADVICE, @@ -35,10 +35,13 @@ LABEL_PM_10_PERCENT = f"{ATTR_PM_10}_percent_of_limit" async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Airly air_quality entity based on a config entry.""" name = config_entry.data[CONF_NAME] + latitude = config_entry.data[CONF_LATITUDE] + longitude = config_entry.data[CONF_LONGITUDE] + unique_id = f"{latitude}-{longitude}" data = hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] - async_add_entities([AirlyAirQuality(data, name)], True) + async_add_entities([AirlyAirQuality(data, name, unique_id)], True) def round_state(func): @@ -56,11 +59,12 @@ def round_state(func): class AirlyAirQuality(AirQualityEntity): """Define an Airly air quality.""" - def __init__(self, airly, name): + def __init__(self, airly, name, unique_id): """Initialize.""" self.airly = airly self.data = airly.data self._name = name + self._unique_id = unique_id self._pm_2_5 = None self._pm_10 = None self._aqi = None @@ -108,12 +112,12 @@ class AirlyAirQuality(AirQualityEntity): @property def unique_id(self): """Return a unique_id for this entity.""" - return f"{self.airly.latitude}-{self.airly.longitude}" + return self._unique_id @property def available(self): """Return True if entity is available.""" - return bool(self.airly.data) + return bool(self.data) @property def device_state_attributes(self): @@ -132,7 +136,6 @@ class AirlyAirQuality(AirQualityEntity): if self.airly.data: self.data = self.airly.data - - self._pm_10 = self.data[ATTR_API_PM10] - self._pm_2_5 = self.data[ATTR_API_PM25] - self._aqi = self.data[ATTR_API_CAQI] + self._pm_10 = self.data[ATTR_API_PM10] + self._pm_2_5 = self.data[ATTR_API_PM25] + self._aqi = self.data[ATTR_API_CAQI] diff --git a/homeassistant/components/airly/config_flow.py b/homeassistant/components/airly/config_flow.py index b361930fa7d..31cfec7e7aa 100644 --- a/homeassistant/components/airly/config_flow.py +++ b/homeassistant/components/airly/config_flow.py @@ -1,14 +1,14 @@ """Adds config flow for Airly.""" -import async_timeout -import voluptuous as vol from airly import Airly from airly.exceptions import AirlyError +import async_timeout +import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant import config_entries from homeassistant.const import CONF_API_KEY, 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 .const import DEFAULT_NAME, DOMAIN, NO_AIRLY_SENSORS diff --git a/homeassistant/components/airly/sensor.py b/homeassistant/components/airly/sensor.py index bce32d64041..af0eac39cdc 100644 --- a/homeassistant/components/airly/sensor.py +++ b/homeassistant/components/airly/sensor.py @@ -2,6 +2,8 @@ from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_DEVICE_CLASS, + CONF_LATITUDE, + CONF_LONGITUDE, CONF_NAME, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_PRESSURE, @@ -60,12 +62,16 @@ SENSOR_TYPES = { async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Airly sensor entities based on a config entry.""" name = config_entry.data[CONF_NAME] + latitude = config_entry.data[CONF_LATITUDE] + longitude = config_entry.data[CONF_LONGITUDE] data = hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] sensors = [] for sensor in SENSOR_TYPES: - sensors.append(AirlySensor(data, name, sensor)) + unique_id = f"{latitude}-{longitude}-{sensor.lower()}" + sensors.append(AirlySensor(data, name, sensor, unique_id)) + async_add_entities(sensors, True) @@ -84,11 +90,12 @@ def round_state(func): class AirlySensor(Entity): """Define an Airly sensor.""" - def __init__(self, airly, name, kind): + def __init__(self, airly, name, kind, unique_id): """Initialize.""" self.airly = airly self.data = airly.data self._name = name + self._unique_id = unique_id self.kind = kind self._device_class = None self._state = None @@ -130,7 +137,7 @@ class AirlySensor(Entity): @property def unique_id(self): """Return a unique_id for this entity.""" - return f"{self.airly.latitude}-{self.airly.longitude}-{self.kind.lower()}" + return self._unique_id @property def unit_of_measurement(self): @@ -140,7 +147,7 @@ class AirlySensor(Entity): @property def available(self): """Return True if entity is available.""" - return bool(self.airly.data) + return bool(self.data) async def async_update(self): """Update the sensor.""" diff --git a/homeassistant/components/alarm_control_panel/.translations/bg.json b/homeassistant/components/alarm_control_panel/.translations/bg.json index 29700793770..a9342c8c477 100644 --- a/homeassistant/components/alarm_control_panel/.translations/bg.json +++ b/homeassistant/components/alarm_control_panel/.translations/bg.json @@ -6,6 +6,13 @@ "arm_night": "\u0421\u043b\u043e\u0436\u0438 {entity_name} \u043f\u043e\u0434 \u043e\u0445\u0440\u0430\u043d\u0430 \u0432 \u043d\u043e\u0449\u0435\u043d \u0440\u0435\u0436\u0438\u043c", "disarm": "\u0414\u0435\u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u0439 {entity_name}", "trigger": "\u0417\u0430\u0434\u0435\u0439\u0441\u0442\u0432\u0430\u043d\u0435 {entity_name}" + }, + "trigger_type": { + "armed_away": "{entity_name} \u043f\u043e\u0434 \u043e\u0445\u0440\u0430\u043d\u0430", + "armed_home": "{entity_name} \u043f\u043e\u0434 \u043e\u0445\u0440\u0430\u043d\u0430 - \u0432\u043a\u044a\u0449\u0438", + "armed_night": "{entity_name} \u043f\u043e\u0434 \u043e\u0445\u0440\u0430\u043d\u0430 - \u043d\u043e\u0449", + "disarmed": "{entity_name} \u0434\u0435\u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u0430", + "triggered": "{entity_name} \u0437\u0430\u0434\u0435\u0439\u0441\u0442\u0432\u0430\u043d\u0430" } } } \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/ca.json b/homeassistant/components/alarm_control_panel/.translations/ca.json index 8d95d5f6485..d60cf3173c7 100644 --- a/homeassistant/components/alarm_control_panel/.translations/ca.json +++ b/homeassistant/components/alarm_control_panel/.translations/ca.json @@ -6,6 +6,13 @@ "arm_night": "Activa {entity_name} nocturn", "disarm": "Desactiva {entity_name}", "trigger": "Dispara {entity_name}" + }, + "trigger_type": { + "armed_away": "{entity_name} activada en mode a fora", + "armed_home": "{entity_name} activada en mode a casa", + "armed_night": "{entity_name} activada en mode nocturn", + "disarmed": "{entity_name} desactivada", + "triggered": "{entity_name} disparat/ada" } } } \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/da.json b/homeassistant/components/alarm_control_panel/.translations/da.json index 74e02e10de4..220034d23e1 100644 --- a/homeassistant/components/alarm_control_panel/.translations/da.json +++ b/homeassistant/components/alarm_control_panel/.translations/da.json @@ -1,7 +1,18 @@ { "device_automation": { "action_type": { + "arm_away": "Tilkobl {entity_name} ude", + "arm_home": "Tilkobl {entity_name} hjemme", + "arm_night": "Tilkobl {entity_name} nat", + "disarm": "Frakobl {entity_name}", "trigger": "Udl\u00f8s {entity_name}" + }, + "trigger_type": { + "armed_away": "{entity_name} tilkoblet ude", + "armed_home": "{entity_name} tilkoblet hjemme", + "armed_night": "{entity_name} tilkoblet nat", + "disarmed": "{entity_name} frakoblet", + "triggered": "{entity_name} udl\u00f8st" } } } \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/en.json b/homeassistant/components/alarm_control_panel/.translations/en.json index b8eeb1d2e8c..a00e81feb92 100644 --- a/homeassistant/components/alarm_control_panel/.translations/en.json +++ b/homeassistant/components/alarm_control_panel/.translations/en.json @@ -6,6 +6,13 @@ "arm_night": "Arm {entity_name} night", "disarm": "Disarm {entity_name}", "trigger": "Trigger {entity_name}" + }, + "trigger_type": { + "armed_away": "{entity_name} armed away", + "armed_home": "{entity_name} armed home", + "armed_night": "{entity_name} armed night", + "disarmed": "{entity_name} disarmed", + "triggered": "{entity_name} triggered" } } } \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/es.json b/homeassistant/components/alarm_control_panel/.translations/es.json index 273efeeaba5..8200755de0f 100644 --- a/homeassistant/components/alarm_control_panel/.translations/es.json +++ b/homeassistant/components/alarm_control_panel/.translations/es.json @@ -6,6 +6,13 @@ "arm_night": "Armar {entity_name} por la noche", "disarm": "Desarmar {entity_name}", "trigger": "Lanzar {entity_name}" + }, + "trigger_type": { + "armed_away": "{entity_name} armado fuera", + "armed_home": "{entity_name} armado en casa", + "armed_night": "{entity_name} armado modo noche", + "disarmed": "{entity_name} desarmado", + "triggered": "{entity_name} activado" } } } \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/fr.json b/homeassistant/components/alarm_control_panel/.translations/fr.json index c3ba6db0c62..fbdc6a5605f 100644 --- a/homeassistant/components/alarm_control_panel/.translations/fr.json +++ b/homeassistant/components/alarm_control_panel/.translations/fr.json @@ -1,11 +1,18 @@ { "device_automation": { "action_type": { - "arm_away": "Armer {entity_name} mode sortie", - "arm_home": "Armer {entity_name} mode \u00e0 la maison", - "arm_night": "Armer {entity_name} mode nuit", + "arm_away": "Armer {entity_name} en mode \"sortie\"", + "arm_home": "Armer {entity_name} en mode \"maison\"", + "arm_night": "Armer {entity_name} en mode \"nuit\"", "disarm": "D\u00e9sarmer {entity_name}", "trigger": "D\u00e9clencheur {entity_name}" + }, + "trigger_type": { + "armed_away": "Armer {entity_name} en mode \"sortie\"", + "armed_home": "Armer {entity_name} en mode \"maison\"", + "armed_night": "Armer {entity_name} en mode \"nuit\"", + "disarmed": "{entity_name} d\u00e9sarm\u00e9", + "triggered": "{entity_name} d\u00e9clench\u00e9" } } } \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/hu.json b/homeassistant/components/alarm_control_panel/.translations/hu.json new file mode 100644 index 00000000000..b249a16c9f1 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/.translations/hu.json @@ -0,0 +1,18 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "{entity_name} \u00e9les\u00edt\u00e9se t\u00e1voz\u00f3 m\u00f3dban", + "arm_home": "{entity_name} \u00e9les\u00edt\u00e9se otthon marad\u00f3 m\u00f3dban", + "arm_night": "{entity_name} \u00e9les\u00edt\u00e9se \u00e9jszakai m\u00f3dban", + "disarm": "{entity_name} hat\u00e1stalan\u00edt\u00e1sa", + "trigger": "{entity_name} riaszt\u00e1si esem\u00e9ny ind\u00edt\u00e1sa" + }, + "trigger_type": { + "armed_away": "{entity_name} t\u00e1voz\u00f3 m\u00f3dban lett \u00e9les\u00edtve", + "armed_home": "{entity_name} otthon marad\u00f3 m\u00f3dban lett \u00e9les\u00edtve", + "armed_night": "{entity_name} \u00e9jszakai m\u00f3dban lett \u00e9les\u00edtve", + "disarmed": "{entity_name} hat\u00e1stalan\u00edtva lett", + "triggered": "{entity_name} riaszt\u00e1sba ker\u00fclt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/it.json b/homeassistant/components/alarm_control_panel/.translations/it.json index e39967e9dac..78a3f0b07e5 100644 --- a/homeassistant/components/alarm_control_panel/.translations/it.json +++ b/homeassistant/components/alarm_control_panel/.translations/it.json @@ -6,6 +6,13 @@ "arm_night": "Armare {entity_name} notte", "disarm": "Disarmare {entity_name}", "trigger": "Attivazione {entity_name}" + }, + "trigger_type": { + "armed_away": "{entity_name} armata modalit\u00e0 fuori casa", + "armed_home": "{entity_name} armata modalit\u00e0 a casa", + "armed_night": "{entity_name} armata modalit\u00e0 notte", + "disarmed": "{entity_name} disarmato", + "triggered": "{entity_name} attivato" } } } \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/ko.json b/homeassistant/components/alarm_control_panel/.translations/ko.json index 5d6caa5fe12..b70ae8dc025 100644 --- a/homeassistant/components/alarm_control_panel/.translations/ko.json +++ b/homeassistant/components/alarm_control_panel/.translations/ko.json @@ -6,6 +6,13 @@ "arm_night": "{entity_name} \uc57c\uac04\uacbd\ube44", "disarm": "{entity_name} \uacbd\ube44\ud574\uc81c", "trigger": "{entity_name} \ud2b8\ub9ac\uac70" + }, + "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" } } } \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/lb.json b/homeassistant/components/alarm_control_panel/.translations/lb.json index ff265a52c38..add11f5b8fe 100644 --- a/homeassistant/components/alarm_control_panel/.translations/lb.json +++ b/homeassistant/components/alarm_control_panel/.translations/lb.json @@ -6,6 +6,13 @@ "arm_night": "{entity_name} fir Nuecht uschalten", "disarm": "{entity_name} entsch\u00e4rfen", "trigger": "{entity_name} ausl\u00e9isen" + }, + "trigger_type": { + "armed_away": "{entity_name} ugeschalt fir Ennerwee", + "armed_home": "{entity_name} ugeschalt fir Doheem", + "armed_night": "{entity_name} ugeschalt fir Nuecht", + "disarmed": "{entity_name} entsch\u00e4rft", + "triggered": "{entity_name} ausgel\u00e9ist" } } } \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/nl.json b/homeassistant/components/alarm_control_panel/.translations/nl.json index 9329a089d32..5081ae992b4 100644 --- a/homeassistant/components/alarm_control_panel/.translations/nl.json +++ b/homeassistant/components/alarm_control_panel/.translations/nl.json @@ -6,6 +6,13 @@ "arm_night": "Inschakelen {entity_name} nacht", "disarm": "Uitschakelen {entity_name}", "trigger": "Trigger {entity_name}" + }, + "trigger_type": { + "armed_away": "{entity_name} afwezig ingeschakeld", + "armed_home": "{entity_name} thuis ingeschakeld", + "armed_night": "{entity_name} nachtstand ingeschakeld", + "disarmed": "{entity_name} uitgeschakeld", + "triggered": "{entity_name} geactiveerd" } } } \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/no.json b/homeassistant/components/alarm_control_panel/.translations/no.json index 93833f33d41..108d273a0f0 100644 --- a/homeassistant/components/alarm_control_panel/.translations/no.json +++ b/homeassistant/components/alarm_control_panel/.translations/no.json @@ -6,6 +6,13 @@ "arm_night": "Aktiver {entity_name} natt", "disarm": "Deaktiver {entity_name}", "trigger": "Utl\u00f8ser {entity_name}" + }, + "trigger_type": { + "armed_away": "{entity_name} borte sikkring ", + "armed_home": "{entity_name} hjemme sikkring", + "armed_night": "{entity_name} natt sikkring", + "disarmed": "{entity_name} deaktivert", + "triggered": "{entity_name} utl\u00f8st" } } } \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/pl.json b/homeassistant/components/alarm_control_panel/.translations/pl.json index a5dc326c267..024a0861c1c 100644 --- a/homeassistant/components/alarm_control_panel/.translations/pl.json +++ b/homeassistant/components/alarm_control_panel/.translations/pl.json @@ -6,6 +6,13 @@ "arm_night": "uzbr\u00f3j (noc) {entity_name}", "disarm": "rozbr\u00f3j {entity_name}", "trigger": "wyzw\u00f3l {entity_name}" + }, + "trigger_type": { + "armed_away": "{entity_name} zostanie uzbrojony (poza domem)", + "armed_home": "{entity_name} zostanie uzbrojony (w domu)", + "armed_night": "{entity_name} zostanie uzbrojony (noc)", + "disarmed": "{entity_name} zostanie rozbrojony", + "triggered": "{entity_name} zostanie wyzwolony" } } } \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/pt-BR.json b/homeassistant/components/alarm_control_panel/.translations/pt-BR.json index 156ede8851b..032756f48f2 100644 --- a/homeassistant/components/alarm_control_panel/.translations/pt-BR.json +++ b/homeassistant/components/alarm_control_panel/.translations/pt-BR.json @@ -6,6 +6,13 @@ "arm_night": "Armar {entity_name} noite", "disarm": "Desarmar {entity_name}", "trigger": "Disparar {entidade_nome}" + }, + "trigger_type": { + "armed_away": "{entity_name} armado modo longe", + "armed_home": "{entidade_nome} armadado modo casa ", + "armed_night": "{entity_name} armadado para noite", + "disarmed": "{entity_name} desarmado", + "triggered": "{entity_name} acionado" } } } \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/ru.json b/homeassistant/components/alarm_control_panel/.translations/ru.json index e573ce70918..f9a0e859e11 100644 --- a/homeassistant/components/alarm_control_panel/.translations/ru.json +++ b/homeassistant/components/alarm_control_panel/.translations/ru.json @@ -6,6 +6,13 @@ "arm_night": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u0440\u0430\u043d\u044b \"\u041d\u043e\u0447\u044c\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", "disarm": "\u041e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043e\u0445\u0440\u0430\u043d\u0443 \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", "trigger": "{entity_name} \u0441\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u0442" + }, + "trigger_type": { + "armed_away": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u0440\u0430\u043d\u044b \"\u041d\u0435 \u0434\u043e\u043c\u0430\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", + "armed_home": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u0440\u0430\u043d\u044b \"\u0414\u043e\u043c\u0430\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", + "armed_night": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u0440\u0430\u043d\u044b \"\u041d\u043e\u0447\u044c\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", + "disarmed": "\u041e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0430 \u043e\u0445\u0440\u0430\u043d\u0430 \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", + "triggered": "{entity_name} \u0441\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u0442" } } } \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/sl.json b/homeassistant/components/alarm_control_panel/.translations/sl.json index 9bf01fc62de..855c50ab827 100644 --- a/homeassistant/components/alarm_control_panel/.translations/sl.json +++ b/homeassistant/components/alarm_control_panel/.translations/sl.json @@ -6,6 +6,13 @@ "arm_night": "Vklju\u010di {entity_name} no\u010d", "disarm": "Razoro\u017ei {entity_name}", "trigger": "Spro\u017ei {entity_name}" + }, + "trigger_type": { + "armed_away": "{entity_name} oboro\u017een - zdoma", + "armed_home": "{entity_name} oboro\u017een - dom", + "armed_night": "{entity_name} oboro\u017een - no\u010d", + "disarmed": "{entity_name} razoro\u017een", + "triggered": "{entity_name} spro\u017een" } } } \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/zh-Hant.json b/homeassistant/components/alarm_control_panel/.translations/zh-Hant.json index c52288802d1..72c0b65436d 100644 --- a/homeassistant/components/alarm_control_panel/.translations/zh-Hant.json +++ b/homeassistant/components/alarm_control_panel/.translations/zh-Hant.json @@ -6,6 +6,13 @@ "arm_night": "\u8a2d\u5b9a {entity_name} \u591c\u9593\u6a21\u5f0f", "disarm": "\u89e3\u9664 {entity_name}", "trigger": "\u89f8\u767c {entity_name}" + }, + "trigger_type": { + "armed_away": "{entity_name} \u8a2d\u5b9a\u5916\u51fa", + "armed_home": "{entity_name} \u8a2d\u5b9a\u5728\u5bb6", + "armed_night": "{entity_name} \u8a2d\u5b9a\u591c\u9593", + "disarmed": "{entity_name} \u5df2\u89e3\u9664", + "triggered": "{entity_name} \u5df2\u89f8\u767c" } } } \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index 6faad5dd51f..5fb44a18a0b 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -1,4 +1,5 @@ """Component to interface with an alarm control panel.""" +from abc import abstractmethod from datetime import timedelta import logging @@ -7,22 +8,30 @@ import voluptuous as vol from homeassistant.const import ( ATTR_CODE, ATTR_CODE_FORMAT, - SERVICE_ALARM_TRIGGER, - SERVICE_ALARM_DISARM, - SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY, - SERVICE_ALARM_ARM_NIGHT, SERVICE_ALARM_ARM_CUSTOM_BYPASS, -) -from homeassistant.helpers.config_validation import ( # noqa: F401 - ENTITY_SERVICE_SCHEMA, - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, + SERVICE_ALARM_ARM_HOME, + SERVICE_ALARM_ARM_NIGHT, + SERVICE_ALARM_DISARM, + SERVICE_ALARM_TRIGGER, ) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.config_validation import ( # noqa: F401 + PLATFORM_SCHEMA, + PLATFORM_SCHEMA_BASE, + make_entity_service_schema, +) from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent +from .const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_CUSTOM_BYPASS, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, + SUPPORT_ALARM_TRIGGER, +) + DOMAIN = "alarm_control_panel" SCAN_INTERVAL = timedelta(seconds=30) ATTR_CHANGED_BY = "changed_by" @@ -32,9 +41,7 @@ ATTR_CODE_ARM_REQUIRED = "code_arm_required" ENTITY_ID_FORMAT = DOMAIN + ".{}" -ALARM_SERVICE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Optional(ATTR_CODE): cv.string} -) +ALARM_SERVICE_SCHEMA = make_entity_service_schema({vol.Optional(ATTR_CODE): cv.string}) async def async_setup(hass, config): @@ -49,21 +56,34 @@ async def async_setup(hass, config): SERVICE_ALARM_DISARM, ALARM_SERVICE_SCHEMA, "async_alarm_disarm" ) component.async_register_entity_service( - SERVICE_ALARM_ARM_HOME, ALARM_SERVICE_SCHEMA, "async_alarm_arm_home" + SERVICE_ALARM_ARM_HOME, + ALARM_SERVICE_SCHEMA, + "async_alarm_arm_home", + [SUPPORT_ALARM_ARM_HOME], ) component.async_register_entity_service( - SERVICE_ALARM_ARM_AWAY, ALARM_SERVICE_SCHEMA, "async_alarm_arm_away" + SERVICE_ALARM_ARM_AWAY, + ALARM_SERVICE_SCHEMA, + "async_alarm_arm_away", + [SUPPORT_ALARM_ARM_AWAY], ) component.async_register_entity_service( - SERVICE_ALARM_ARM_NIGHT, ALARM_SERVICE_SCHEMA, "async_alarm_arm_night" + SERVICE_ALARM_ARM_NIGHT, + ALARM_SERVICE_SCHEMA, + "async_alarm_arm_night", + [SUPPORT_ALARM_ARM_NIGHT], ) component.async_register_entity_service( SERVICE_ALARM_ARM_CUSTOM_BYPASS, ALARM_SERVICE_SCHEMA, "async_alarm_arm_custom_bypass", + [SUPPORT_ALARM_ARM_CUSTOM_BYPASS], ) component.async_register_entity_service( - SERVICE_ALARM_TRIGGER, ALARM_SERVICE_SCHEMA, "async_alarm_trigger" + SERVICE_ALARM_TRIGGER, + ALARM_SERVICE_SCHEMA, + "async_alarm_trigger", + [SUPPORT_ALARM_TRIGGER], ) return True @@ -79,7 +99,6 @@ async def async_unload_entry(hass, entry): return await hass.data[DOMAIN].async_unload_entry(entry) -# pylint: disable=no-self-use class AlarmControlPanel(Entity): """An abstract class for alarm control devices.""" @@ -164,6 +183,11 @@ class AlarmControlPanel(Entity): """ return self.hass.async_add_executor_job(self.alarm_arm_custom_bypass, code) + @property + @abstractmethod + def supported_features(self) -> int: + """Return the list of supported features.""" + @property def state_attributes(self): """Return the state attributes.""" diff --git a/homeassistant/components/alarm_control_panel/const.py b/homeassistant/components/alarm_control_panel/const.py new file mode 100644 index 00000000000..77f7846fc34 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/const.py @@ -0,0 +1,7 @@ +"""Provides the constants needed for component.""" + +SUPPORT_ALARM_ARM_HOME = 1 +SUPPORT_ALARM_ARM_AWAY = 2 +SUPPORT_ALARM_ARM_NIGHT = 4 +SUPPORT_ALARM_TRIGGER = 8 +SUPPORT_ALARM_ARM_CUSTOM_BYPASS = 16 diff --git a/homeassistant/components/alarm_control_panel/device_action.py b/homeassistant/components/alarm_control_panel/device_action.py index a3c2b482261..81e444ae16f 100644 --- a/homeassistant/components/alarm_control_panel/device_action.py +++ b/homeassistant/components/alarm_control_panel/device_action.py @@ -1,5 +1,6 @@ """Provides device automations for Alarm control panel.""" -from typing import Optional, List +from typing import List, Optional + import voluptuous as vol from homeassistant.const import ( @@ -16,10 +17,17 @@ from homeassistant.const import ( SERVICE_ALARM_DISARM, SERVICE_ALARM_TRIGGER, ) -from homeassistant.core import HomeAssistant, Context +from homeassistant.core import Context, HomeAssistant from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv + from . import ATTR_CODE_ARM_REQUIRED, DOMAIN +from .const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, + SUPPORT_ALARM_TRIGGER, +) ACTION_TYPES = {"arm_away", "arm_home", "arm_night", "disarm", "trigger"} @@ -42,31 +50,42 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: if entry.domain != DOMAIN: continue + state = hass.states.get(entry.entity_id) + + # We need a state or else we can't populate the HVAC and preset modes. + if state is None: + continue + + supported_features = state.attributes["supported_features"] + # Add actions for each entity that belongs to this integration - actions.append( - { - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "arm_away", - } - ) - actions.append( - { - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "arm_home", - } - ) - actions.append( - { - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "arm_night", - } - ) + if supported_features & SUPPORT_ALARM_ARM_AWAY: + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "arm_away", + } + ) + if supported_features & SUPPORT_ALARM_ARM_HOME: + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "arm_home", + } + ) + if supported_features & SUPPORT_ALARM_ARM_NIGHT: + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "arm_night", + } + ) actions.append( { CONF_DEVICE_ID: device_id, @@ -75,14 +94,15 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: CONF_TYPE: "disarm", } ) - actions.append( - { - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "trigger", - } - ) + if supported_features & SUPPORT_ALARM_TRIGGER: + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "trigger", + } + ) return actions diff --git a/homeassistant/components/alarm_control_panel/device_trigger.py b/homeassistant/components/alarm_control_panel/device_trigger.py new file mode 100644 index 00000000000..95ae17aaaf5 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/device_trigger.py @@ -0,0 +1,151 @@ +"""Provides device automations for Alarm control panel.""" +from typing import List + +import voluptuous as vol + +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, +) +from homeassistant.components.automation import AutomationActionType, state +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from homeassistant.const import ( + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_ENTITY_ID, + CONF_PLATFORM, + CONF_TYPE, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, + STATE_ALARM_PENDING, + STATE_ALARM_TRIGGERED, +) +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 = { + "triggered", + "disarmed", + "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), + } +) + + +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 = [] + + # 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 + + entity_state = hass.states.get(entry.entity_id) + + # We need a state or else we can't populate the HVAC and preset modes. + if entity_state is None: + continue + + supported_features = entity_state.attributes["supported_features"] + + # 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: "disarmed", + }, + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "triggered", + }, + ] + 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, + 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, + 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, + CONF_TYPE: "armed_night", + } + ) + + return triggers + + +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: dict, +) -> CALLBACK_TYPE: + """Attach a trigger.""" + config = TRIGGER_SCHEMA(config) + + if config[CONF_TYPE] == "triggered": + from_state = STATE_ALARM_PENDING + to_state = STATE_ALARM_TRIGGERED + elif config[CONF_TYPE] == "disarmed": + from_state = STATE_ALARM_TRIGGERED + to_state = STATE_ALARM_DISARMED + elif config[CONF_TYPE] == "armed_home": + from_state = STATE_ALARM_PENDING + to_state = STATE_ALARM_ARMED_HOME + elif config[CONF_TYPE] == "armed_away": + from_state = STATE_ALARM_PENDING + to_state = STATE_ALARM_ARMED_AWAY + elif config[CONF_TYPE] == "armed_night": + from_state = STATE_ALARM_PENDING + to_state = STATE_ALARM_ARMED_NIGHT + + 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) + return await state.async_attach_trigger( + hass, state_config, action, automation_info, platform_type="device" + ) diff --git a/homeassistant/components/alarm_control_panel/manifest.json b/homeassistant/components/alarm_control_panel/manifest.json index 04ef58769da..e877fe90a17 100644 --- a/homeassistant/components/alarm_control_panel/manifest.json +++ b/homeassistant/components/alarm_control_panel/manifest.json @@ -4,7 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/alarm_control_panel", "requirements": [], "dependencies": [], - "codeowners": [ - "@colinodell" - ] + "codeowners": [] } diff --git a/homeassistant/components/alarm_control_panel/services.yaml b/homeassistant/components/alarm_control_panel/services.yaml index 9abf2189ed3..b31cb718b3f 100644 --- a/homeassistant/components/alarm_control_panel/services.yaml +++ b/homeassistant/components/alarm_control_panel/services.yaml @@ -59,85 +59,3 @@ alarm_trigger: code: description: An optional code to trigger the alarm control panel with. example: 1234 - -envisalink_alarm_keypress: - description: Send custom keypresses to the alarm. - fields: - entity_id: - description: Name of the alarm control panel to trigger. - example: 'alarm_control_panel.downstairs' - keypress: - description: 'String to send to the alarm panel (1-6 characters).' - example: '*71' - -alarmdecoder_alarm_toggle_chime: - description: Send the alarm the toggle chime command. - fields: - entity_id: - description: Name of the alarm control panel to trigger. - example: 'alarm_control_panel.downstairs' - code: - description: A required code to toggle the alarm control panel chime with. - example: 1234 - -ifttt_push_alarm_state: - description: Update the alarm state to the specified value. - fields: - entity_id: - description: Name of the alarm control panel which state has to be updated. - example: 'alarm_control_panel.downstairs' - state: - description: The state to which the alarm control panel has to be set. - example: 'armed_night' - -elkm1_alarm_arm_vacation: - description: Arm the ElkM1 in vacation mode. - fields: - entity_id: - description: Name of alarm control panel to arm. - example: 'alarm_control_panel.main' - code: - description: An code to arm the alarm control panel. - example: 1234 - -elkm1_alarm_arm_home_instant: - description: Arm the ElkM1 in home instant mode. - fields: - entity_id: - description: Name of alarm control panel to arm. - example: 'alarm_control_panel.main' - code: - description: An code to arm the alarm control panel. - example: 1234 - -elkm1_alarm_arm_night_instant: - description: Arm the ElkM1 in night instant mode. - fields: - entity_id: - description: Name of alarm control panel to arm. - example: 'alarm_control_panel.main' - code: - description: An code to arm the alarm control panel. - example: 1234 - -elkm1_alarm_display_message: - description: Display a message on all of the ElkM1 keypads for an area. - fields: - entity_id: - description: Name of alarm control panel to display messages on. - example: 'alarm_control_panel.main' - clear: - description: 0=clear message, 1=clear message with * key, 2=Display until timeout; default 2 - example: 1 - beep: - description: 0=no beep, 1=beep; default 0 - example: 1 - timeout: - description: Time to display message, 0=forever, max 65535, default 0 - example: 4242 - line1: - description: Up to 16 characters of text (truncated if too long). Default blank. - example: The answer to life, - line2: - description: Up to 16 characters of text (truncated if too long). Default blank. - example: the universe, and everything. diff --git a/homeassistant/components/alarm_control_panel/strings.json b/homeassistant/components/alarm_control_panel/strings.json index f67635776dd..cbca15c8cf6 100644 --- a/homeassistant/components/alarm_control_panel/strings.json +++ b/homeassistant/components/alarm_control_panel/strings.json @@ -6,6 +6,13 @@ "arm_night": "Arm {entity_name} night", "disarm": "Disarm {entity_name}", "trigger": "Trigger {entity_name}" + }, + "trigger_type": { + "triggered": "{entity_name} triggered", + "disarmed": "{entity_name} disarmed", + "armed_home": "{entity_name} armed home", + "armed_away": "{entity_name} armed away", + "armed_night": "{entity_name} armed night" } } } \ No newline at end of file diff --git a/homeassistant/components/alarmdecoder/__init__.py b/homeassistant/components/alarmdecoder/__init__.py index 61cb0effe53..93c0746a812 100644 --- a/homeassistant/components/alarmdecoder/__init__.py +++ b/homeassistant/components/alarmdecoder/__init__.py @@ -1,14 +1,17 @@ """Support for AlarmDecoder devices.""" +from datetime import timedelta import logging -from datetime import timedelta +from alarmdecoder import AlarmDecoder +from alarmdecoder.devices import SerialDevice, SocketDevice, USBDevice +from alarmdecoder.util import NoDeviceError import voluptuous as vol +from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA +from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STOP import homeassistant.helpers.config_validation as cv -from homeassistant.const import EVENT_HOMEASSISTANT_STOP, CONF_HOST from homeassistant.helpers.discovery import load_platform from homeassistant.util import dt as dt_util -from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA _LOGGER = logging.getLogger(__name__) @@ -109,9 +112,6 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): """Set up for the AlarmDecoder devices.""" - from alarmdecoder import AlarmDecoder - from alarmdecoder.devices import SocketDevice, SerialDevice, USBDevice - conf = config.get(DOMAIN) restart = False @@ -134,8 +134,6 @@ def setup(hass, config): def open_connection(now=None): """Open a connection to AlarmDecoder.""" - from alarmdecoder.util import NoDeviceError - nonlocal restart try: controller.open(baud) diff --git a/homeassistant/components/alarmdecoder/alarm_control_panel.py b/homeassistant/components/alarmdecoder/alarm_control_panel.py index 288c1dfd1c7..66960ca3034 100644 --- a/homeassistant/components/alarmdecoder/alarm_control_panel.py +++ b/homeassistant/components/alarmdecoder/alarm_control_panel.py @@ -3,7 +3,15 @@ import logging import voluptuous as vol -import homeassistant.components.alarm_control_panel as alarm +from homeassistant.components.alarm_control_panel import ( + FORMAT_NUMBER, + AlarmControlPanel, +) +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, +) from homeassistant.const import ( ATTR_CODE, STATE_ALARM_ARMED_AWAY, @@ -13,11 +21,11 @@ from homeassistant.const import ( ) import homeassistant.helpers.config_validation as cv -from . import DATA_AD, DOMAIN as DOMAIN_ALARMDECODER, SIGNAL_PANEL_MESSAGE +from . import DATA_AD, DOMAIN, SIGNAL_PANEL_MESSAGE _LOGGER = logging.getLogger(__name__) -SERVICE_ALARM_TOGGLE_CHIME = "alarmdecoder_alarm_toggle_chime" +SERVICE_ALARM_TOGGLE_CHIME = "alarm_toggle_chime" ALARM_TOGGLE_CHIME_SCHEMA = vol.Schema({vol.Required(ATTR_CODE): cv.string}) SERVICE_ALARM_KEYPRESS = "alarm_keypress" @@ -36,7 +44,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): device.alarm_toggle_chime(code) hass.services.register( - alarm.DOMAIN, + DOMAIN, SERVICE_ALARM_TOGGLE_CHIME, alarm_toggle_chime_handler, schema=ALARM_TOGGLE_CHIME_SCHEMA, @@ -48,14 +56,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None): device.alarm_keypress(keypress) hass.services.register( - DOMAIN_ALARMDECODER, + DOMAIN, SERVICE_ALARM_KEYPRESS, alarm_keypress_handler, schema=ALARM_KEYPRESS_SCHEMA, ) -class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel): +class AlarmDecoderAlarmPanel(AlarmControlPanel): """Representation of an AlarmDecoder-based alarm panel.""" def __init__(self): @@ -115,13 +123,18 @@ class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel): @property def code_format(self): """Return one or more digits/characters.""" - return alarm.FORMAT_NUMBER + return FORMAT_NUMBER @property def state(self): """Return the state of the device.""" return self._state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT + @property def device_state_attributes(self): """Return the state attributes.""" diff --git a/homeassistant/components/alarmdecoder/manifest.json b/homeassistant/components/alarmdecoder/manifest.json index 5ab69d94cf2..0f41405d431 100644 --- a/homeassistant/components/alarmdecoder/manifest.json +++ b/homeassistant/components/alarmdecoder/manifest.json @@ -2,9 +2,7 @@ "domain": "alarmdecoder", "name": "Alarmdecoder", "documentation": "https://www.home-assistant.io/integrations/alarmdecoder", - "requirements": [ - "alarmdecoder==1.13.2" - ], + "requirements": ["alarmdecoder==1.13.9"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/alarmdecoder/services.yaml b/homeassistant/components/alarmdecoder/services.yaml index 55451d42f13..12268d48bb7 100644 --- a/homeassistant/components/alarmdecoder/services.yaml +++ b/homeassistant/components/alarmdecoder/services.yaml @@ -7,3 +7,13 @@ alarm_keypress: keypress: description: 'String to send to the alarm panel.' example: '*71' + +alarm_toggle_chime: + description: Send the alarm the toggle chime command. + fields: + entity_id: + description: Name of the alarm control panel to trigger. + example: 'alarm_control_panel.downstairs' + code: + description: A required code to toggle the alarm control panel chime with. + example: 1234 diff --git a/homeassistant/components/alarmdotcom/alarm_control_panel.py b/homeassistant/components/alarmdotcom/alarm_control_panel.py index 07d69960e0b..dd6b1272223 100644 --- a/homeassistant/components/alarmdotcom/alarm_control_panel.py +++ b/homeassistant/components/alarmdotcom/alarm_control_panel.py @@ -7,6 +7,10 @@ import voluptuous as vol import homeassistant.components.alarm_control_panel as alarm from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, +) from homeassistant.const import ( CONF_CODE, CONF_NAME, @@ -95,6 +99,11 @@ class AlarmDotCom(alarm.AlarmControlPanel): return STATE_ALARM_ARMED_AWAY return None + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY + @property def device_state_attributes(self): """Return the state attributes.""" diff --git a/homeassistant/components/alert/__init__.py b/homeassistant/components/alert/__init__.py index 420d730933c..09e2883c332 100644 --- a/homeassistant/components/alert/__init__.py +++ b/homeassistant/components/alert/__init__.py @@ -1,30 +1,30 @@ """Support for repeating alerts when conditions are met.""" import asyncio -import logging from datetime import timedelta +import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.notify import ( + ATTR_DATA, ATTR_MESSAGE, ATTR_TITLE, - ATTR_DATA, DOMAIN as DOMAIN_NOTIFY, ) from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_ENTITY_ID, - STATE_IDLE, CONF_NAME, CONF_STATE, - STATE_ON, - STATE_OFF, - SERVICE_TURN_ON, - SERVICE_TURN_OFF, SERVICE_TOGGLE, - ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_IDLE, + STATE_OFF, + STATE_ON, ) -from homeassistant.helpers import service, event +from homeassistant.helpers import event, service +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import ToggleEntity from homeassistant.util.dt import now diff --git a/homeassistant/components/alexa/__init__.py b/homeassistant/components/alexa/__init__.py index cb0d093bb48..e9bcccb3587 100644 --- a/homeassistant/components/alexa/__init__.py +++ b/homeassistant/components/alexa/__init__.py @@ -3,25 +3,24 @@ import logging import voluptuous as vol -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers import entityfilter from homeassistant.const import CONF_NAME +from homeassistant.helpers import config_validation as cv, entityfilter from . import flash_briefings, intent, smart_home_http from .const import ( CONF_AUDIO, CONF_CLIENT_ID, CONF_CLIENT_SECRET, + CONF_DESCRIPTION, + CONF_DISPLAY_CATEGORIES, CONF_DISPLAY_URL, CONF_ENDPOINT, + CONF_ENTITY_CONFIG, + CONF_FILTER, CONF_TEXT, CONF_TITLE, CONF_UID, DOMAIN, - CONF_FILTER, - CONF_ENTITY_CONFIG, - CONF_DESCRIPTION, - CONF_DISPLAY_CATEGORIES, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/alexa/auth.py b/homeassistant/components/alexa/auth.py index 9f87a6d954e..94789c33305 100644 --- a/homeassistant/components/alexa/auth.py +++ b/homeassistant/components/alexa/auth.py @@ -1,8 +1,9 @@ """Support for Alexa skill auth.""" import asyncio +from datetime import timedelta import json import logging -from datetime import timedelta + import aiohttp import async_timeout @@ -50,7 +51,7 @@ class Auth: "client_secret": self.client_secret, } _LOGGER.debug( - "Calling LWA to get the access token (first time), " "with: %s", + "Calling LWA to get the access token (first time), with: %s", json.dumps(lwa_params), ) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 7d74bb3f8cd..3fdad2b3c92 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -1,6 +1,10 @@ """Alexa capabilities.""" import logging +from homeassistant.components import cover, fan, image_processing, input_number, light +from homeassistant.components.alarm_control_panel import ATTR_CODE_FORMAT, FORMAT_NUMBER +import homeassistant.components.climate.const as climate +import homeassistant.components.media_player.const as media_player from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, @@ -18,23 +22,26 @@ from homeassistant.const import ( STATE_UNKNOWN, STATE_UNLOCKED, ) -import homeassistant.components.climate.const as climate -import homeassistant.components.media_player.const as media_player -from homeassistant.components.alarm_control_panel import ATTR_CODE_FORMAT, FORMAT_NUMBER -from homeassistant.components import light, fan, cover import homeassistant.util.color as color_util import homeassistant.util.dt as dt_util from .const import ( - Catalog, API_TEMP_UNITS, API_THERMOSTAT_MODES, API_THERMOSTAT_PRESETS, DATE_FORMAT, PERCENTAGE_FAN_MAP, RANGE_FAN_MAP, + Inputs, ) from .errors import UnsupportedProperty +from .resources import ( + AlexaCapabilityResource, + AlexaGlobalCatalog, + AlexaModeResource, + AlexaPresetResource, + AlexaSemantics, +) _LOGGER = logging.getLogger(__name__) @@ -105,12 +112,41 @@ class AlexaCapability: @staticmethod def capability_resources(): - """Applicable to ToggleController, RangeController, and ModeController interfaces.""" + """Return the capability object. + + Applicable to ToggleController, RangeController, and ModeController interfaces. + """ return [] @staticmethod def configuration(): - """Return the Configuration object.""" + """Return the configuration object. + + Applicable to the ThermostatController, SecurityControlPanel, ModeController, RangeController, + and EventDetectionSensor. + """ + return [] + + @staticmethod + def configurations(): + """Return the configurations object. + + The plural configurations object is different that the singular configuration object. + Applicable to EqualizerController interface. + """ + return [] + + @staticmethod + def inputs(): + """Applicable only to media players.""" + return [] + + @staticmethod + def semantics(): + """Return the semantics object. + + Applicable to ToggleController, RangeController, and ModeController interfaces. + """ return [] @staticmethod @@ -122,6 +158,10 @@ class AlexaCapability: """Serialize according to the Discovery API.""" result = {"type": "AlexaInterface", "interface": self.name(), "version": "3"} + instance = self.instance + if instance is not None: + result["instance"] = instance + properties_supported = self.properties_supported() if properties_supported: result["properties"] = { @@ -130,22 +170,19 @@ class AlexaCapability: "retrievable": self.properties_retrievable(), } - # pylint: disable=assignment-from-none proactively_reported = self.capability_proactively_reported() if proactively_reported is not None: result["proactivelyReported"] = proactively_reported - # pylint: disable=assignment-from-none non_controllable = self.properties_non_controllable() if non_controllable is not None: result["properties"]["nonControllable"] = non_controllable - # pylint: disable=assignment-from-none supports_deactivation = self.supports_deactivation() if supports_deactivation is not None: result["supportsDeactivation"] = supports_deactivation - capability_resources = self.serialize_capability_resources() + capability_resources = self.capability_resources() if capability_resources: result["capabilityResources"] = capability_resources @@ -153,15 +190,23 @@ class AlexaCapability: if configuration: result["configuration"] = configuration - # pylint: disable=assignment-from-none - instance = self.instance - if instance is not None: - result["instance"] = instance + # The plural configurations object is different than the singular configuration object above. + configurations = self.configurations() + if configurations: + result["configurations"] = configurations + + semantics = self.semantics() + if semantics: + result["semantics"] = semantics supported_operations = self.supported_operations() if supported_operations: result["supportedOperations"] = supported_operations + inputs = self.inputs() + if inputs: + result["inputs"] = inputs + return result def serialize_properties(self): @@ -184,35 +229,19 @@ class AlexaCapability: yield result - def serialize_capability_resources(self): - """Return capabilityResources friendlyNames serialized for an API response.""" - resources = self.capability_resources() - if resources: - return {"friendlyNames": self.serialize_friendly_names(resources)} - return None +class Alexa(AlexaCapability): + """Implements Alexa Interface. - @staticmethod - def serialize_friendly_names(resources): - """Return capabilityResources, ModeResources, or presetResources friendlyNames serialized for an API response.""" - friendly_names = [] - for resource in resources: - if resource["type"] == Catalog.LABEL_ASSET: - friendly_names.append( - { - "@type": Catalog.LABEL_ASSET, - "value": {"assetId": resource["value"]}, - } - ) - else: - friendly_names.append( - { - "@type": Catalog.LABEL_TEXT, - "value": {"text": resource["value"], "locale": "en-US"}, - } - ) + Although endpoints implement this interface implicitly, + The API suggests you should explicitly include this interface. - return friendly_names + https://developer.amazon.com/docs/device-apis/alexa-interface.html + """ + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa" class AlexaEndpointHealth(AlexaCapability): @@ -236,7 +265,7 @@ class AlexaEndpointHealth(AlexaCapability): def properties_proactively_reported(self): """Return True if properties asynchronously reported.""" - return False + return True def properties_retrievable(self): """Return True if properties can be retrieved.""" @@ -529,6 +558,23 @@ class AlexaInputController(AlexaCapability): """Return the Alexa API name of this interface.""" return "Alexa.InputController" + def inputs(self): + """Return the list of valid supported inputs.""" + source_list = self.entity.attributes.get( + media_player.ATTR_INPUT_SOURCE_LIST, [] + ) + input_list = [] + for source in source_list: + formatted_source = ( + source.lower().replace("-", "").replace("_", "").replace(" ", "") + ) + if formatted_source in Inputs.VALID_SOURCE_NAME_MAP.keys(): + input_list.append( + {"name": Inputs.VALID_SOURCE_NAME_MAP[formatted_source]} + ) + + return input_list + class AlexaTemperatureSensor(AlexaCapability): """Implements Alexa.TemperatureSensor. @@ -752,10 +798,11 @@ class AlexaThermostatController(AlexaCapability): supported_modes.append(thermostat_mode) preset_modes = self.entity.attributes.get(climate.ATTR_PRESET_MODES) - for mode in preset_modes: - thermostat_mode = API_THERMOSTAT_PRESETS.get(mode) - if thermostat_mode: - supported_modes.append(thermostat_mode) + if preset_modes: + for mode in preset_modes: + thermostat_mode = API_THERMOSTAT_PRESETS.get(mode) + if thermostat_mode: + supported_modes.append(thermostat_mode) # Return False for supportsScheduling until supported with event listener in handler. configuration = {"supportsScheduling": False} @@ -862,6 +909,8 @@ class AlexaModeController(AlexaCapability): def __init__(self, entity, instance, non_controllable=False): """Initialize the entity.""" super().__init__(entity, instance) + self._resource = None + self._semantics = None self.properties_non_controllable = lambda: non_controllable def name(self): @@ -878,73 +927,102 @@ class AlexaModeController(AlexaCapability): def properties_retrievable(self): """Return True if properties can be retrieved.""" + return True def get_property(self, name): """Read and return a property.""" if name != "mode": raise UnsupportedProperty(name) + # Fan Direction if self.instance == f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}": - return self.entity.attributes.get(fan.ATTR_DIRECTION) + mode = self.entity.attributes.get(fan.ATTR_DIRECTION, None) + if mode in (fan.DIRECTION_FORWARD, fan.DIRECTION_REVERSE, STATE_UNKNOWN): + return f"{fan.ATTR_DIRECTION}.{mode}" + + # Cover Position + if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": + # Return state instead of position when using ModeController. + mode = self.entity.state + if mode in ( + cover.STATE_OPEN, + cover.STATE_OPENING, + cover.STATE_CLOSED, + cover.STATE_CLOSING, + STATE_UNKNOWN, + ): + return f"{cover.ATTR_POSITION}.{mode}" return None def configuration(self): """Return configuration with modeResources.""" - return self.serialize_mode_resources() + if isinstance(self._resource, AlexaCapabilityResource): + return self._resource.serialize_configuration() + + return None def capability_resources(self): """Return capabilityResources object.""" - capability_resources = [] + # Fan Direction Resource if self.instance == f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}": - capability_resources = [ - {"type": Catalog.LABEL_ASSET, "value": Catalog.SETTING_DIRECTION} - ] + self._resource = AlexaModeResource( + [AlexaGlobalCatalog.SETTING_DIRECTION], False + ) + self._resource.add_mode( + f"{fan.ATTR_DIRECTION}.{fan.DIRECTION_FORWARD}", [fan.DIRECTION_FORWARD] + ) + self._resource.add_mode( + f"{fan.ATTR_DIRECTION}.{fan.DIRECTION_REVERSE}", [fan.DIRECTION_REVERSE] + ) + return self._resource.serialize_capability_resources() - return capability_resources + # Cover Position Resources + if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": + self._resource = AlexaModeResource( + ["Position", AlexaGlobalCatalog.SETTING_OPENING], False + ) + self._resource.add_mode( + f"{cover.ATTR_POSITION}.{cover.STATE_OPEN}", + [AlexaGlobalCatalog.VALUE_OPEN], + ) + self._resource.add_mode( + f"{cover.ATTR_POSITION}.{cover.STATE_CLOSED}", + [AlexaGlobalCatalog.VALUE_CLOSE], + ) + self._resource.add_mode(f"{cover.ATTR_POSITION}.custom", ["Custom"]) + return self._resource.serialize_capability_resources() - def mode_resources(self): - """Return modeResources object.""" - mode_resources = None - if self.instance == f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}": - mode_resources = { - "ordered": False, - "resources": [ - { - "value": f"{fan.ATTR_DIRECTION}.{fan.DIRECTION_FORWARD}", - "friendly_names": [ - {"type": Catalog.LABEL_TEXT, "value": fan.DIRECTION_FORWARD} - ], - }, - { - "value": f"{fan.ATTR_DIRECTION}.{fan.DIRECTION_REVERSE}", - "friendly_names": [ - {"type": Catalog.LABEL_TEXT, "value": fan.DIRECTION_REVERSE} - ], - }, - ], - } + return None - return mode_resources + def semantics(self): + """Build and return semantics object.""" - def serialize_mode_resources(self): - """Return ModeResources, friendlyNames serialized for an API response.""" - mode_resources = [] - resources = self.mode_resources() - ordered = resources["ordered"] - for resource in resources["resources"]: - mode_value = resource["value"] - friendly_names = resource["friendly_names"] - result = { - "value": mode_value, - "modeResources": { - "friendlyNames": self.serialize_friendly_names(friendly_names) - }, - } - mode_resources.append(result) + # Cover Position + if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": + self._semantics = AlexaSemantics() + self._semantics.add_action_to_directive( + [AlexaSemantics.ACTION_CLOSE, AlexaSemantics.ACTION_LOWER], + "SetMode", + {"mode": f"{cover.ATTR_POSITION}.{cover.STATE_CLOSED}"}, + ) + self._semantics.add_action_to_directive( + [AlexaSemantics.ACTION_OPEN, AlexaSemantics.ACTION_RAISE], + "SetMode", + {"mode": f"{cover.ATTR_POSITION}.{cover.STATE_OPEN}"}, + ) + self._semantics.add_states_to_value( + [AlexaSemantics.STATES_CLOSED], + f"{cover.ATTR_POSITION}.{cover.STATE_CLOSED}", + ) + self._semantics.add_states_to_value( + [AlexaSemantics.STATES_OPEN], + f"{cover.ATTR_POSITION}.{cover.STATE_OPEN}", + ) + return self._semantics.serialize_semantics() - return {"ordered": ordered, "supportedModes": mode_resources} + return None class AlexaRangeController(AlexaCapability): @@ -956,6 +1034,8 @@ class AlexaRangeController(AlexaCapability): def __init__(self, entity, instance, non_controllable=False): """Initialize the entity.""" super().__init__(entity, instance) + self._resource = None + self._semantics = None self.properties_non_controllable = lambda: non_controllable def name(self): @@ -979,88 +1059,137 @@ class AlexaRangeController(AlexaCapability): if name != "rangeValue": raise UnsupportedProperty(name) + # Fan Speed if self.instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}": speed = self.entity.attributes.get(fan.ATTR_SPEED) return RANGE_FAN_MAP.get(speed, 0) + # Cover Position + if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": + return self.entity.attributes.get(cover.ATTR_CURRENT_POSITION) + + # Cover Tilt Position + if self.instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}": + return self.entity.attributes.get(cover.ATTR_CURRENT_TILT_POSITION) + + # Input Number Value + if self.instance == f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}": + return float(self.entity.state) + return None def configuration(self): """Return configuration with presetResources.""" - return self.serialize_preset_resources() + if isinstance(self._resource, AlexaCapabilityResource): + return self._resource.serialize_configuration() + + return None def capability_resources(self): """Return capabilityResources object.""" - capability_resources = [] + # Fan Speed Resources if self.instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}": - return [{"type": Catalog.LABEL_ASSET, "value": Catalog.SETTING_FANSPEED}] - - return capability_resources - - def preset_resources(self): - """Return presetResources object.""" - preset_resources = [] - - if self.instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}": - preset_resources = { - "minimumValue": 1, - "maximumValue": 3, - "precision": 1, - "presets": [ - { - "rangeValue": 1, - "names": [ - { - "type": Catalog.LABEL_ASSET, - "value": Catalog.VALUE_MINIMUM, - }, - {"type": Catalog.LABEL_ASSET, "value": Catalog.VALUE_LOW}, - ], - }, - { - "rangeValue": 2, - "names": [ - {"type": Catalog.LABEL_ASSET, "value": Catalog.VALUE_MEDIUM} - ], - }, - { - "rangeValue": 3, - "names": [ - { - "type": Catalog.LABEL_ASSET, - "value": Catalog.VALUE_MAXIMUM, - }, - {"type": Catalog.LABEL_ASSET, "value": Catalog.VALUE_HIGH}, - ], - }, - ], - } - - return preset_resources - - def serialize_preset_resources(self): - """Return PresetResources, friendlyNames serialized for an API response.""" - preset_resources = [] - resources = self.preset_resources() - for preset in resources["presets"]: - preset_resources.append( - { - "rangeValue": preset["rangeValue"], - "presetResources": { - "friendlyNames": self.serialize_friendly_names(preset["names"]) - }, - } + self._resource = AlexaPresetResource( + labels=[AlexaGlobalCatalog.SETTING_FAN_SPEED], + min_value=1, + max_value=3, + precision=1, ) + self._resource.add_preset( + value=1, + labels=[AlexaGlobalCatalog.VALUE_LOW, AlexaGlobalCatalog.VALUE_MINIMUM], + ) + self._resource.add_preset(value=2, labels=[AlexaGlobalCatalog.VALUE_MEDIUM]) + self._resource.add_preset( + value=3, + labels=[ + AlexaGlobalCatalog.VALUE_HIGH, + AlexaGlobalCatalog.VALUE_MAXIMUM, + ], + ) + return self._resource.serialize_capability_resources() - return { - "supportedRange": { - "minimumValue": resources["minimumValue"], - "maximumValue": resources["maximumValue"], - "precision": resources["precision"], - }, - "presets": preset_resources, - } + # Cover Position Resources + if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": + self._resource = AlexaPresetResource( + ["Position", AlexaGlobalCatalog.SETTING_OPENING], + min_value=0, + max_value=100, + precision=1, + unit=AlexaGlobalCatalog.UNIT_PERCENT, + ) + return self._resource.serialize_capability_resources() + + # Cover Tilt Position Resources + if self.instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}": + self._resource = AlexaPresetResource( + ["Tilt Position", AlexaGlobalCatalog.SETTING_OPENING], + min_value=0, + max_value=100, + precision=1, + unit=AlexaGlobalCatalog.UNIT_PERCENT, + ) + return self._resource.serialize_capability_resources() + + # Input Number Value + if self.instance == f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}": + min_value = float(self.entity.attributes[input_number.ATTR_MIN]) + max_value = float(self.entity.attributes[input_number.ATTR_MAX]) + precision = float(self.entity.attributes.get(input_number.ATTR_STEP, 1)) + unit = self.entity.attributes.get(input_number.ATTR_UNIT_OF_MEASUREMENT) + + self._resource = AlexaPresetResource( + ["Value"], + min_value=min_value, + max_value=max_value, + precision=precision, + unit=unit, + ) + self._resource.add_preset( + value=min_value, labels=[AlexaGlobalCatalog.VALUE_MINIMUM] + ) + self._resource.add_preset( + value=max_value, labels=[AlexaGlobalCatalog.VALUE_MAXIMUM] + ) + return self._resource.serialize_capability_resources() + + return None + + def semantics(self): + """Build and return semantics object.""" + + # Cover Position + if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": + self._semantics = AlexaSemantics() + self._semantics.add_action_to_directive( + [AlexaSemantics.ACTION_LOWER], "SetRangeValue", {"rangeValue": 0} + ) + self._semantics.add_action_to_directive( + [AlexaSemantics.ACTION_RAISE], "SetRangeValue", {"rangeValue": 100} + ) + self._semantics.add_states_to_value([AlexaSemantics.STATES_CLOSED], value=0) + self._semantics.add_states_to_range( + [AlexaSemantics.STATES_OPEN], min_value=1, max_value=100 + ) + return self._semantics.serialize_semantics() + + # Cover Tilt Position + if self.instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}": + self._semantics = AlexaSemantics() + self._semantics.add_action_to_directive( + [AlexaSemantics.ACTION_CLOSE], "SetRangeValue", {"rangeValue": 0} + ) + self._semantics.add_action_to_directive( + [AlexaSemantics.ACTION_OPEN], "SetRangeValue", {"rangeValue": 100} + ) + self._semantics.add_states_to_value([AlexaSemantics.STATES_CLOSED], value=0) + self._semantics.add_states_to_range( + [AlexaSemantics.STATES_OPEN], min_value=1, max_value=100 + ) + return self._semantics.serialize_semantics() + + return None class AlexaToggleController(AlexaCapability): @@ -1072,6 +1201,8 @@ class AlexaToggleController(AlexaCapability): def __init__(self, entity, instance, non_controllable=False): """Initialize the entity.""" super().__init__(entity, instance) + self._resource = None + self._semantics = None self.properties_non_controllable = lambda: non_controllable def name(self): @@ -1095,6 +1226,7 @@ class AlexaToggleController(AlexaCapability): if name != "toggleState": raise UnsupportedProperty(name) + # Fan Oscillating if self.instance == f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}": is_on = bool(self.entity.attributes.get(fan.ATTR_OSCILLATING)) return "ON" if is_on else "OFF" @@ -1103,16 +1235,15 @@ class AlexaToggleController(AlexaCapability): def capability_resources(self): """Return capabilityResources object.""" - capability_resources = [] + # Fan Oscillating Resource if self.instance == f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}": - capability_resources = [ - {"type": Catalog.LABEL_ASSET, "value": Catalog.SETTING_OSCILLATE}, - {"type": Catalog.LABEL_TEXT, "value": "Rotate"}, - {"type": Catalog.LABEL_TEXT, "value": "Rotation"}, - ] + self._resource = AlexaCapabilityResource( + [AlexaGlobalCatalog.SETTING_OSCILLATE, "Rotate", "Rotation"] + ) + return self._resource.serialize_capability_resources() - return capability_resources + return None class AlexaChannelController(AlexaCapability): @@ -1186,3 +1317,112 @@ class AlexaSeekController(AlexaCapability): def name(self): """Return the Alexa API name of this interface.""" return "Alexa.SeekController" + + +class AlexaEventDetectionSensor(AlexaCapability): + """Implements Alexa.EventDetectionSensor. + + https://developer.amazon.com/docs/device-apis/alexa-eventdetectionsensor.html + """ + + def __init__(self, hass, entity): + """Initialize the entity.""" + super().__init__(entity) + self.hass = hass + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.EventDetectionSensor" + + def properties_supported(self): + """Return what properties this entity supports.""" + return [{"name": "humanPresenceDetectionState"}] + + def properties_proactively_reported(self): + """Return True if properties asynchronously reported.""" + return True + + def get_property(self, name): + """Read and return a property.""" + if name != "humanPresenceDetectionState": + raise UnsupportedProperty(name) + + human_presence = "NOT_DETECTED" + state = self.entity.state + + # Return None for unavailable and unknown states. + # Allows the Alexa.EndpointHealth Interface to handle the unavailable state in a stateReport. + if state in (STATE_UNAVAILABLE, STATE_UNKNOWN, None): + return None + + if self.entity.domain == image_processing.DOMAIN: + if int(state): + human_presence = "DETECTED" + elif state == STATE_ON: + human_presence = "DETECTED" + + return {"value": human_presence} + + def configuration(self): + """Return supported detection types.""" + return { + "detectionMethods": ["AUDIO", "VIDEO"], + "detectionModes": { + "humanPresence": { + "featureAvailability": "ENABLED", + "supportsNotDetected": True, + } + }, + } + + +class AlexaEqualizerController(AlexaCapability): + """Implements Alexa.EqualizerController. + + https://developer.amazon.com/en-US/docs/alexa/device-apis/alexa-equalizercontroller.html + """ + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.EqualizerController" + + def properties_supported(self): + """Return what properties this entity supports. + + Either bands, mode or both can be specified. Only mode is supported at this time. + """ + return [{"name": "mode"}] + + def get_property(self, name): + """Read and return a property.""" + if name != "mode": + raise UnsupportedProperty(name) + + sound_mode = self.entity.attributes.get(media_player.ATTR_SOUND_MODE) + if sound_mode and sound_mode.upper() in ( + "MOVIE", + "MUSIC", + "NIGHT", + "SPORT", + "TV", + ): + return sound_mode.upper() + + return None + + def configurations(self): + """Return the sound modes supported in the configurations object. + + Valid Values for modes are: MOVIE, MUSIC, NIGHT, SPORT, TV. + """ + configurations = None + sound_mode_list = self.entity.attributes.get(media_player.ATTR_SOUND_MODE_LIST) + if sound_mode_list: + supported_sound_modes = [] + for sound_mode in sound_mode_list: + if sound_mode.upper() in ("MOVIE", "MUSIC", "NIGHT", "SPORT", "TV"): + supported_sound_modes.append({"name": sound_mode.upper()}) + + configurations = {"modes": {"supported": supported_sound_modes}} + + return configurations diff --git a/homeassistant/components/alexa/const.py b/homeassistant/components/alexa/const.py index 2a5f9a512b3..6968ab3a691 100644 --- a/homeassistant/components/alexa/const.py +++ b/homeassistant/components/alexa/const.py @@ -1,9 +1,9 @@ """Constants for the Alexa integration.""" from collections import OrderedDict -from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT -from homeassistant.components.climate import const as climate from homeassistant.components import fan +from homeassistant.components.climate import const as climate +from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT DOMAIN = "alexa" @@ -117,158 +117,90 @@ class Cause: VOICE_INTERACTION = "VOICE_INTERACTION" -class Catalog: - """The Global Alexa catalog. +class Inputs: + """Valid names for the InputController. - https://developer.amazon.com/docs/device-apis/resources-and-assets.html#global-alexa-catalog - - You can use the global Alexa catalog for pre-defined names of devices, settings, values, and units. - This catalog is localized into all the languages that Alexa supports. - - You can reference the following catalog of pre-defined friendly names. - Each item in the following list is an asset identifier followed by its supported friendly names. - The first friendly name for each identifier is the one displayed in the Alexa mobile app. + https://developer.amazon.com/docs/device-apis/alexa-property-schemas.html#input """ - LABEL_ASSET = "asset" - LABEL_TEXT = "text" + VALID_SOURCE_NAME_MAP = { + "aux": "AUX 1", + "aux1": "AUX 1", + "aux2": "AUX 2", + "aux3": "AUX 3", + "aux4": "AUX 4", + "aux5": "AUX 5", + "aux6": "AUX 6", + "aux7": "AUX 7", + "bluray": "BLURAY", + "cable": "CABLE", + "cd": "CD", + "coax": "COAX 1", + "coax1": "COAX 1", + "coax2": "COAX 2", + "composite": "COMPOSITE 1", + "composite1": "COMPOSITE 1", + "dvd": "DVD", + "game": "GAME", + "gameconsole": "GAME", + "hdradio": "HD RADIO", + "hdmi": "HDMI 1", + "hdmi1": "HDMI 1", + "hdmi2": "HDMI 2", + "hdmi3": "HDMI 3", + "hdmi4": "HDMI 4", + "hdmi5": "HDMI 5", + "hdmi6": "HDMI 6", + "hdmi7": "HDMI 7", + "hdmi8": "HDMI 8", + "hdmi9": "HDMI 9", + "hdmi10": "HDMI 10", + "hdmiarc": "HDMI ARC", + "input": "INPUT 1", + "input1": "INPUT 1", + "input2": "INPUT 2", + "input3": "INPUT 3", + "input4": "INPUT 4", + "input5": "INPUT 5", + "input6": "INPUT 6", + "input7": "INPUT 7", + "input8": "INPUT 8", + "input9": "INPUT 9", + "input10": "INPUT 10", + "ipod": "IPOD", + "line": "LINE 1", + "line1": "LINE 1", + "line2": "LINE 2", + "line3": "LINE 3", + "line4": "LINE 4", + "line5": "LINE 5", + "line6": "LINE 6", + "line7": "LINE 7", + "mediaplayer": "MEDIA PLAYER", + "optical": "OPTICAL 1", + "optical1": "OPTICAL 1", + "optical2": "OPTICAL 2", + "phono": "PHONO", + "playstation": "PLAYSTATION", + "playstation3": "PLAYSTATION 3", + "playstation4": "PLAYSTATION 4", + "satellite": "SATELLITE", + "satellitetv": "SATELLITE", + "smartcast": "SMARTCAST", + "tuner": "TUNER", + "tv": "TV", + "usbdac": "USB DAC", + "video": "VIDEO 1", + "video1": "VIDEO 1", + "video2": "VIDEO 2", + "video3": "VIDEO 3", + "xbox": "XBOX", + } - # Shower - DEVICENAME_SHOWER = "Alexa.DeviceName.Shower" - - # Washer, Washing Machine - DEVICENAME_WASHER = "Alexa.DeviceName.Washer" - - # Router, Internet Router, Network Router, Wifi Router, Net Router - DEVICENAME_ROUTER = "Alexa.DeviceName.Router" - - # Fan, Blower - DEVICENAME_FAN = "Alexa.DeviceName.Fan" - - # Air Purifier, Air Cleaner,Clean Air Machine - DEVICENAME_AIRPURIFIER = "Alexa.DeviceName.AirPurifier" - - # Space Heater, Portable Heater - DEVICENAME_SPACEHEATER = "Alexa.DeviceName.SpaceHeater" - - # Rain Head, Overhead shower, Rain Shower, Rain Spout, Rain Faucet - SHOWER_RAINHEAD = "Alexa.Shower.RainHead" - - # Handheld Shower, Shower Wand, Hand Shower - SHOWER_HANDHELD = "Alexa.Shower.HandHeld" - - # Water Temperature, Water Temp, Water Heat - SETTING_WATERTEMPERATURE = "Alexa.Setting.WaterTemperature" - - # Temperature, Temp - SETTING_TEMPERATURE = "Alexa.Setting.Temperature" - - # Wash Cycle, Wash Preset, Wash setting - SETTING_WASHCYCLE = "Alexa.Setting.WashCycle" - - # 2.4G Guest Wi-Fi, 2.4G Guest Network, Guest Network 2.4G, 2G Guest Wifi - SETTING_2GGUESTWIFI = "Alexa.Setting.2GGuestWiFi" - - # 5G Guest Wi-Fi, 5G Guest Network, Guest Network 5G, 5G Guest Wifi - SETTING_5GGUESTWIFI = "Alexa.Setting.5GGuestWiFi" - - # Guest Wi-fi, Guest Network, Guest Net - SETTING_GUESTWIFI = "Alexa.Setting.GuestWiFi" - - # Auto, Automatic, Automatic Mode, Auto Mode - SETTING_AUTO = "Alexa.Setting.Auto" - - # #Night, Night Mode - SETTING_NIGHT = "Alexa.Setting.Night" - - # Quiet, Quiet Mode, Noiseless, Silent - SETTING_QUIET = "Alexa.Setting.Quiet" - - # Oscillate, Swivel, Oscillation, Spin, Back and forth - SETTING_OSCILLATE = "Alexa.Setting.Oscillate" - - # Fan Speed, Airflow speed, Wind Speed, Air speed, Air velocity - SETTING_FANSPEED = "Alexa.Setting.FanSpeed" - - # Preset, Setting - SETTING_PRESET = "Alexa.Setting.Preset" - - # Mode - SETTING_MODE = "Alexa.Setting.Mode" - - # Direction - SETTING_DIRECTION = "Alexa.Setting.Direction" - - # Delicates, Delicate - VALUE_DELICATE = "Alexa.Value.Delicate" - - # Quick Wash, Fast Wash, Wash Quickly, Speed Wash - VALUE_QUICKWASH = "Alexa.Value.QuickWash" - - # Maximum, Max - VALUE_MAXIMUM = "Alexa.Value.Maximum" - - # Minimum, Min - VALUE_MINIMUM = "Alexa.Value.Minimum" - - # High - VALUE_HIGH = "Alexa.Value.High" - - # Low - VALUE_LOW = "Alexa.Value.Low" - - # Medium, Mid - VALUE_MEDIUM = "Alexa.Value.Medium" - - -class Unit: - """Alexa Units of Measure. - - https://developer.amazon.com/docs/device-apis/alexa-property-schemas.html#units-of-measure - """ - - ANGLE_DEGREES = "Alexa.Unit.Angle.Degrees" - - ANGLE_RADIANS = "Alexa.Unit.Angle.Radians" - - DISTANCE_FEET = "Alexa.Unit.Distance.Feet" - - DISTANCE_INCHES = "Alexa.Unit.Distance.Inches" - - DISTANCE_KILOMETERS = "Alexa.Unit.Distance.Kilometers" - - DISTANCE_METERS = "Alexa.Unit.Distance.Meters" - - DISTANCE_MILES = "Alexa.Unit.Distance.Miles" - - DISTANCE_YARDS = "Alexa.Unit.Distance.Yards" - - MASS_GRAMS = "Alexa.Unit.Mass.Grams" - - MASS_KILOGRAMS = "Alexa.Unit.Mass.Kilograms" - - PERCENT = "Alexa.Unit.Percent" - - TEMPERATURE_CELSIUS = "Alexa.Unit.Temperature.Celsius" - - TEMPERATURE_DEGREES = "Alexa.Unit.Temperature.Degrees" - - TEMPERATURE_FAHRENHEIT = "Alexa.Unit.Temperature.Fahrenheit" - - TEMPERATURE_KELVIN = "Alexa.Unit.Temperature.Kelvin" - - VOLUME_CUBICFEET = "Alexa.Unit.Volume.CubicFeet" - - VOLUME_CUBICMETERS = "Alexa.Unit.Volume.CubicMeters" - - VOLUME_GALLONS = "Alexa.Unit.Volume.Gallons" - - VOLUME_LITERS = "Alexa.Unit.Volume.Liters" - - VOLUME_PINTS = "Alexa.Unit.Volume.Pints" - - VOLUME_QUARTS = "Alexa.Unit.Volume.Quarts" - - WEIGHT_OUNCES = "Alexa.Unit.Weight.Ounces" - - WEIGHT_POUNDS = "Alexa.Unit.Weight.Pounds" + VALID_SOUND_MODE_MAP = { + "movie": "MOVIE", + "music": "MUSIC", + "night": "NIGHT", + "sport": "SPORT", + "tv": "TV", + } diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index 6d2f9aef56a..4321d289cec 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -1,7 +1,26 @@ """Alexa entity adapters.""" from typing import List -from homeassistant.core import callback +from homeassistant.components import ( + alarm_control_panel, + alert, + automation, + binary_sensor, + cover, + fan, + group, + image_processing, + input_boolean, + input_number, + light, + lock, + media_player, + scene, + script, + sensor, + switch, +) +from homeassistant.components.climate import const as climate from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_SUPPORTED_FEATURES, @@ -11,28 +30,11 @@ from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, ) +from homeassistant.core import callback from homeassistant.util.decorator import Registry -from homeassistant.components.climate import const as climate -from homeassistant.components import ( - alarm_control_panel, - alert, - automation, - binary_sensor, - cover, - fan, - group, - input_boolean, - light, - lock, - media_player, - scene, - script, - sensor, - switch, -) -from .const import CONF_DESCRIPTION, CONF_DISPLAY_CATEGORIES from .capabilities import ( + Alexa, AlexaBrightnessController, AlexaChannelController, AlexaColorController, @@ -40,6 +42,8 @@ from .capabilities import ( AlexaContactSensor, AlexaDoorbellEventSource, AlexaEndpointHealth, + AlexaEqualizerController, + AlexaEventDetectionSensor, AlexaInputController, AlexaLockController, AlexaModeController, @@ -59,6 +63,7 @@ from .capabilities import ( AlexaThermostatController, AlexaToggleController, ) +from .const import CONF_DESCRIPTION, CONF_DISPLAY_CATEGORIES ENTITY_ADAPTERS = Registry() @@ -80,6 +85,9 @@ class DisplayCategory: # Indicates media devices with video or photo capabilities. CAMERA = "CAMERA" + # Indicates a non-mobile computer, such as a desktop computer. + COMPUTER = "COMPUTER" + # Indicates an endpoint that detects and reports contact. CONTACT_SENSOR = "CONTACT_SENSOR" @@ -89,27 +97,60 @@ class DisplayCategory: # Indicates a doorbell. DOORBELL = "DOORBELL" + # Indicates a window covering on the outside of a structure. + EXTERIOR_BLIND = "EXTERIOR_BLIND" + # Indicates a fan. FAN = "FAN" + # Indicates a game console, such as Microsoft Xbox or Nintendo Switch + GAME_CONSOLE = "GAME_CONSOLE" + + # Indicates a garage door. Garage doors must implement the ModeController interface to open and close the door. + GARAGE_DOOR = "GARAGE_DOOR" + + # Indicates a window covering on the inside of a structure. + INTERIOR_BLIND = "INTERIOR_BLIND" + + # Indicates a laptop or other mobile computer. + LAPTOP = "LAPTOP" + # Indicates light sources or fixtures. LIGHT = "LIGHT" # Indicates a microwave oven. MICROWAVE = "MICROWAVE" + # Indicates a mobile phone. + MOBILE_PHONE = "MOBILE_PHONE" + # Indicates an endpoint that detects and reports motion. MOTION_SENSOR = "MOTION_SENSOR" + # Indicates a network-connected music system. + MUSIC_SYSTEM = "MUSIC_SYSTEM" + # An endpoint that cannot be described in on of the other categories. OTHER = "OTHER" + # Indicates a network router. + NETWORK_HARDWARE = "NETWORK_HARDWARE" + + # Indicates an oven cooking appliance. + OVEN = "OVEN" + + # Indicates a non-mobile phone, such as landline or an IP phone. + PHONE = "PHONE" + # Describes a combination of devices set to a specific state, when the # order of the state change is not important. For example a bedtime scene # might include turning off lights and lowering the thermostat, but the # order is unimportant. Applies to Scenes SCENE_TRIGGER = "SCENE_TRIGGER" + # Indicates a projector screen. + SCREEN = "SCREEN" + # Indicates a security panel. SECURITY_PANEL = "SECURITY_PANEL" @@ -123,10 +164,16 @@ class DisplayCategory: # Indicates the endpoint is a speaker or speaker system. SPEAKER = "SPEAKER" + # Indicates a streaming device such as Apple TV, Chromecast, or Roku. + STREAMING_DEVICE = "STREAMING_DEVICE" + # Indicates in-wall switches wired to the electrical system. Can control a # variety of devices. SWITCH = "SWITCH" + # Indicates a tablet computer. + TABLET = "TABLET" + # Indicates endpoints that report the temperature only. TEMPERATURE_SENSOR = "TEMPERATURE_SENSOR" @@ -137,6 +184,9 @@ class DisplayCategory: # Indicates the endpoint is a television. TV = "TV" + # Indicates a network-connected wearable device, such as an Apple Watch, Fitbit, or Samsung Gear. + WEARABLE = "WEARABLE" + class AlexaEntity: """An adaptation of an entity, expressed in Alexa's terms. @@ -261,6 +311,7 @@ class GenericCapabilities(AlexaEntity): return [ AlexaPowerController(self.entity), AlexaEndpointHealth(self.hass, self.entity), + Alexa(self.hass), ] @@ -270,6 +321,10 @@ class SwitchCapabilities(AlexaEntity): def default_display_categories(self): """Return the display categories for this entity.""" + device_class = self.entity.attributes.get(ATTR_DEVICE_CLASS) + if device_class == switch.DEVICE_CLASS_OUTLET: + return [DisplayCategory.SMARTPLUG] + return [DisplayCategory.SWITCH] def interfaces(self): @@ -277,6 +332,7 @@ class SwitchCapabilities(AlexaEntity): return [ AlexaPowerController(self.entity), AlexaEndpointHealth(self.hass, self.entity), + Alexa(self.hass), ] @@ -299,6 +355,7 @@ class ClimateCapabilities(AlexaEntity): yield AlexaThermostatController(self.hass, self.entity) yield AlexaTemperatureSensor(self.hass, self.entity) yield AlexaEndpointHealth(self.hass, self.entity) + yield Alexa(self.hass) @ENTITY_ADAPTERS.register(cover.DOMAIN) @@ -307,15 +364,43 @@ class CoverCapabilities(AlexaEntity): def default_display_categories(self): """Return the display categories for this entity.""" - return [DisplayCategory.DOOR] + device_class = self.entity.attributes.get(ATTR_DEVICE_CLASS) + if device_class == cover.DEVICE_CLASS_GARAGE: + return [DisplayCategory.GARAGE_DOOR] + if device_class == cover.DEVICE_CLASS_DOOR: + return [DisplayCategory.DOOR] + if device_class in ( + cover.DEVICE_CLASS_BLIND, + cover.DEVICE_CLASS_SHADE, + cover.DEVICE_CLASS_CURTAIN, + ): + return [DisplayCategory.INTERIOR_BLIND] + if device_class in ( + cover.DEVICE_CLASS_WINDOW, + cover.DEVICE_CLASS_AWNING, + cover.DEVICE_CLASS_SHUTTER, + ): + return [DisplayCategory.EXTERIOR_BLIND] + + return [DisplayCategory.OTHER] def interfaces(self): """Yield the supported interfaces.""" - yield AlexaPowerController(self.entity) supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if supported & cover.SUPPORT_SET_POSITION: - yield AlexaPercentageController(self.entity) + yield AlexaRangeController( + self.entity, instance=f"{cover.DOMAIN}.{cover.ATTR_POSITION}" + ) + elif supported & (cover.SUPPORT_CLOSE | cover.SUPPORT_OPEN): + yield AlexaModeController( + self.entity, instance=f"{cover.DOMAIN}.{cover.ATTR_POSITION}" + ) + if supported & cover.SUPPORT_SET_TILT_POSITION: + yield AlexaRangeController( + self.entity, instance=f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}" + ) yield AlexaEndpointHealth(self.hass, self.entity) + yield Alexa(self.hass) @ENTITY_ADAPTERS.register(light.DOMAIN) @@ -337,7 +422,9 @@ class LightCapabilities(AlexaEntity): yield AlexaColorController(self.entity) if supported & light.SUPPORT_COLOR_TEMP: yield AlexaColorTemperatureController(self.entity) + yield AlexaEndpointHealth(self.hass, self.entity) + yield Alexa(self.hass) @ENTITY_ADAPTERS.register(fan.DOMAIN) @@ -351,6 +438,7 @@ class FanCapabilities(AlexaEntity): def interfaces(self): """Yield the supported interfaces.""" yield AlexaPowerController(self.entity) + supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if supported & fan.SUPPORT_SET_SPEED: yield AlexaPercentageController(self.entity) @@ -358,7 +446,6 @@ class FanCapabilities(AlexaEntity): yield AlexaRangeController( self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_SPEED}" ) - if supported & fan.SUPPORT_OSCILLATE: yield AlexaToggleController( self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}" @@ -369,6 +456,7 @@ class FanCapabilities(AlexaEntity): ) yield AlexaEndpointHealth(self.hass, self.entity) + yield Alexa(self.hass) @ENTITY_ADAPTERS.register(lock.DOMAIN) @@ -384,6 +472,7 @@ class LockCapabilities(AlexaEntity): return [ AlexaLockController(self.entity), AlexaEndpointHealth(self.hass, self.entity), + Alexa(self.hass), ] @@ -401,7 +490,6 @@ class MediaPlayerCapabilities(AlexaEntity): def interfaces(self): """Yield the supported interfaces.""" - yield AlexaEndpointHealth(self.hass, self.entity) yield AlexaPowerController(self.entity) supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) @@ -435,6 +523,12 @@ class MediaPlayerCapabilities(AlexaEntity): if supported & media_player.const.SUPPORT_PLAY_MEDIA: yield AlexaChannelController(self.entity) + if supported & media_player.const.SUPPORT_SELECT_SOUND_MODE: + yield AlexaEqualizerController(self.entity) + + yield AlexaEndpointHealth(self.hass, self.entity) + yield Alexa(self.hass) + @ENTITY_ADAPTERS.register(scene.DOMAIN) class SceneCapabilities(AlexaEntity): @@ -453,7 +547,10 @@ class SceneCapabilities(AlexaEntity): def interfaces(self): """Yield the supported interfaces.""" - return [AlexaSceneController(self.entity, supports_deactivation=False)] + return [ + AlexaSceneController(self.entity, supports_deactivation=False), + Alexa(self.hass), + ] @ENTITY_ADAPTERS.register(script.DOMAIN) @@ -467,7 +564,10 @@ class ScriptCapabilities(AlexaEntity): def interfaces(self): """Yield the supported interfaces.""" can_cancel = bool(self.entity.attributes.get("can_cancel")) - return [AlexaSceneController(self.entity, supports_deactivation=can_cancel)] + return [ + AlexaSceneController(self.entity, supports_deactivation=can_cancel), + Alexa(self.hass), + ] @ENTITY_ADAPTERS.register(sensor.DOMAIN) @@ -486,6 +586,7 @@ class SensorCapabilities(AlexaEntity): if attrs.get(ATTR_UNIT_OF_MEASUREMENT) in (TEMP_FAHRENHEIT, TEMP_CELSIUS): yield AlexaTemperatureSensor(self.hass, self.entity) yield AlexaEndpointHealth(self.hass, self.entity) + yield Alexa(self.hass) @ENTITY_ADAPTERS.register(binary_sensor.DOMAIN) @@ -494,6 +595,7 @@ class BinarySensorCapabilities(AlexaEntity): TYPE_CONTACT = "contact" TYPE_MOTION = "motion" + TYPE_PRESENCE = "presence" def default_display_categories(self): """Return the display categories for this entity.""" @@ -502,6 +604,8 @@ class BinarySensorCapabilities(AlexaEntity): return [DisplayCategory.CONTACT_SENSOR] if sensor_type is self.TYPE_MOTION: return [DisplayCategory.MOTION_SENSOR] + if sensor_type is self.TYPE_PRESENCE: + return [DisplayCategory.CAMERA] def interfaces(self): """Yield the supported interfaces.""" @@ -510,22 +614,41 @@ class BinarySensorCapabilities(AlexaEntity): yield AlexaContactSensor(self.hass, self.entity) elif sensor_type is self.TYPE_MOTION: yield AlexaMotionSensor(self.hass, self.entity) + elif sensor_type is self.TYPE_PRESENCE: + yield AlexaEventDetectionSensor(self.hass, self.entity) + # yield additional interfaces based on specified display category in config. entity_conf = self.config.entity_config.get(self.entity.entity_id, {}) if CONF_DISPLAY_CATEGORIES in entity_conf: if entity_conf[CONF_DISPLAY_CATEGORIES] == DisplayCategory.DOORBELL: yield AlexaDoorbellEventSource(self.entity) + elif entity_conf[CONF_DISPLAY_CATEGORIES] == DisplayCategory.CONTACT_SENSOR: + yield AlexaContactSensor(self.hass, self.entity) + elif entity_conf[CONF_DISPLAY_CATEGORIES] == DisplayCategory.MOTION_SENSOR: + yield AlexaMotionSensor(self.hass, self.entity) + elif entity_conf[CONF_DISPLAY_CATEGORIES] == DisplayCategory.CAMERA: + yield AlexaEventDetectionSensor(self.hass, self.entity) yield AlexaEndpointHealth(self.hass, self.entity) + yield Alexa(self.hass) def get_type(self): """Return the type of binary sensor.""" attrs = self.entity.attributes - if attrs.get(ATTR_DEVICE_CLASS) in ("door", "garage_door", "opening", "window"): + if attrs.get(ATTR_DEVICE_CLASS) in ( + binary_sensor.DEVICE_CLASS_DOOR, + binary_sensor.DEVICE_CLASS_GARAGE_DOOR, + binary_sensor.DEVICE_CLASS_OPENING, + binary_sensor.DEVICE_CLASS_WINDOW, + ): return self.TYPE_CONTACT - if attrs.get(ATTR_DEVICE_CLASS) == "motion": + + if attrs.get(ATTR_DEVICE_CLASS) == binary_sensor.DEVICE_CLASS_MOTION: return self.TYPE_MOTION + if attrs.get(ATTR_DEVICE_CLASS) == binary_sensor.DEVICE_CLASS_PRESENCE: + return self.TYPE_PRESENCE + @ENTITY_ADAPTERS.register(alarm_control_panel.DOMAIN) class AlarmControlPanelCapabilities(AlexaEntity): @@ -540,3 +663,37 @@ class AlarmControlPanelCapabilities(AlexaEntity): if not self.entity.attributes.get("code_arm_required"): yield AlexaSecurityPanelController(self.hass, self.entity) yield AlexaEndpointHealth(self.hass, self.entity) + yield Alexa(self.hass) + + +@ENTITY_ADAPTERS.register(image_processing.DOMAIN) +class ImageProcessingCapabilities(AlexaEntity): + """Class to represent image_processing capabilities.""" + + def default_display_categories(self): + """Return the display categories for this entity.""" + return [DisplayCategory.CAMERA] + + def interfaces(self): + """Yield the supported interfaces.""" + yield AlexaEventDetectionSensor(self.hass, self.entity) + yield AlexaEndpointHealth(self.hass, self.entity) + yield Alexa(self.hass) + + +@ENTITY_ADAPTERS.register(input_number.DOMAIN) +class InputNumberCapabilities(AlexaEntity): + """Class to represent input_number capabilities.""" + + def default_display_categories(self): + """Return the display categories for this entity.""" + return [DisplayCategory.OTHER] + + def interfaces(self): + """Yield the supported interfaces.""" + + yield AlexaRangeController( + self.entity, instance=f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}" + ) + yield AlexaEndpointHealth(self.hass, self.entity) + yield Alexa(self.hass) diff --git a/homeassistant/components/alexa/flash_briefings.py b/homeassistant/components/alexa/flash_briefings.py index 0b5c1243764..45d31d6088a 100644 --- a/homeassistant/components/alexa/flash_briefings.py +++ b/homeassistant/components/alexa/flash_briefings.py @@ -3,10 +3,10 @@ import copy import logging import uuid -import homeassistant.util.dt as dt_util from homeassistant.components import http from homeassistant.core import callback from homeassistant.helpers import template +import homeassistant.util.dt as dt_util from .const import ( ATTR_MAIN_TEXT, diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index c23e01f501f..ce6c37a2b39 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -3,13 +3,19 @@ import logging import math from homeassistant import core as ha -from homeassistant.components import cover, fan, group, light, media_player +from homeassistant.components import ( + cover, + fan, + group, + input_number, + light, + media_player, +) from homeassistant.components.climate import const as climate from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, - STATE_ALARM_DISARMED, SERVICE_ALARM_ARM_AWAY, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_NIGHT, @@ -21,6 +27,7 @@ from homeassistant.const import ( SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP, SERVICE_SET_COVER_POSITION, + SERVICE_SET_COVER_TILT_POSITION, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_UNLOCK, @@ -28,23 +35,25 @@ from homeassistant.const import ( SERVICE_VOLUME_MUTE, SERVICE_VOLUME_SET, SERVICE_VOLUME_UP, + STATE_ALARM_DISARMED, TEMP_CELSIUS, TEMP_FAHRENHEIT, ) import homeassistant.util.color as color_util -import homeassistant.util.dt as dt_util from homeassistant.util.decorator import Registry +import homeassistant.util.dt as dt_util from homeassistant.util.temperature import convert as convert_temperature from .const import ( API_TEMP_UNITS, - API_THERMOSTAT_MODES_CUSTOM, API_THERMOSTAT_MODES, + API_THERMOSTAT_MODES_CUSTOM, API_THERMOSTAT_PRESETS, - Cause, PERCENTAGE_FAN_MAP, RANGE_FAN_MAP, SPEED_FAN_MAP, + Cause, + Inputs, ) from .entities import async_get_entities from .errors import ( @@ -110,9 +119,7 @@ async def async_api_turn_on(hass, config, directive, context): domain = ha.DOMAIN service = SERVICE_TURN_ON - if domain == cover.DOMAIN: - service = cover.SERVICE_OPEN_COVER - elif domain == media_player.DOMAIN: + if domain == media_player.DOMAIN: supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) power_features = media_player.SUPPORT_TURN_ON | media_player.SUPPORT_TURN_OFF if not supported & power_features: @@ -138,9 +145,7 @@ async def async_api_turn_off(hass, config, directive, context): domain = ha.DOMAIN service = SERVICE_TURN_OFF - if entity.domain == cover.DOMAIN: - service = cover.SERVICE_CLOSE_COVER - elif domain == media_player.DOMAIN: + if domain == media_player.DOMAIN: supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) power_features = media_player.SUPPORT_TURN_ON | media_player.SUPPORT_TURN_OFF if not supported & power_features: @@ -345,10 +350,6 @@ async def async_api_set_percentage(hass, config, directive, context): speed = "high" data[fan.ATTR_SPEED] = speed - elif entity.domain == cover.DOMAIN: - service = SERVICE_SET_COVER_POSITION - data[cover.ATTR_POSITION] = percentage - await hass.services.async_call( entity.domain, service, data, blocking=False, context=context ) @@ -382,13 +383,6 @@ async def async_api_adjust_percentage(hass, config, directive, context): data[fan.ATTR_SPEED] = speed - elif entity.domain == cover.DOMAIN: - service = SERVICE_SET_COVER_POSITION - - current = entity.attributes.get(cover.ATTR_POSITION) - - data[cover.ATTR_POSITION] = max(0, percentage_delta + current) - await hass.services.async_call( entity.domain, service, data, blocking=False, context=context ) @@ -459,13 +453,20 @@ async def async_api_select_input(hass, config, directive, context): media_input = directive.payload["input"] entity = directive.entity - # attempt to map the ALL UPPERCASE payload name to a source - source_list = entity.attributes[media_player.const.ATTR_INPUT_SOURCE_LIST] or [] + # Attempt to map the ALL UPPERCASE payload name to a source. + # Strips trailing 1 to match single input devices. + source_list = entity.attributes.get(media_player.const.ATTR_INPUT_SOURCE_LIST, []) for source in source_list: - # response will always be space separated, so format the source in the - # most likely way to find a match - formatted_source = source.lower().replace("-", " ").replace("_", " ") - if formatted_source in media_input.lower(): + formatted_source = ( + source.lower().replace("-", "").replace("_", "").replace(" ", "") + ) + media_input = media_input.lower().replace(" ", "") + if ( + formatted_source in Inputs.VALID_SOURCE_NAME_MAP.keys() + and formatted_source == media_input + ) or ( + media_input.endswith("1") and formatted_source == media_input.rstrip("1") + ): media_input = source break else: @@ -950,7 +951,7 @@ async def async_api_disarm(hass, config, directive, context): @HANDLERS.register(("Alexa.ModeController", "SetMode")) async def async_api_set_mode(hass, config, directive, context): - """Process a next request.""" + """Process a SetMode directive.""" entity = directive.entity instance = directive.instance domain = entity.domain @@ -958,45 +959,56 @@ async def async_api_set_mode(hass, config, directive, context): data = {ATTR_ENTITY_ID: entity.entity_id} mode = directive.payload["mode"] - if domain != fan.DOMAIN: - msg = "Entity does not support directive" - raise AlexaInvalidDirectiveError(msg) - + # Fan Direction if instance == f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}": - mode, direction = mode.split(".") - if direction in [fan.DIRECTION_REVERSE, fan.DIRECTION_FORWARD]: + _, direction = mode.split(".") + if direction in (fan.DIRECTION_REVERSE, fan.DIRECTION_FORWARD): service = fan.SERVICE_SET_DIRECTION data[fan.ATTR_DIRECTION] = direction + # Cover Position + elif instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": + _, position = mode.split(".") + + if position == cover.STATE_CLOSED: + service = cover.SERVICE_CLOSE_COVER + elif position == cover.STATE_OPEN: + service = cover.SERVICE_OPEN_COVER + elif position == "custom": + service = cover.SERVICE_STOP_COVER + + else: + msg = "Entity does not support directive" + raise AlexaInvalidDirectiveError(msg) + await hass.services.async_call( domain, service, data, blocking=False, context=context ) - return directive.response() + response = directive.response() + response.add_context_property( + { + "namespace": "Alexa.ModeController", + "instance": instance, + "name": "mode", + "value": mode, + } + ) + + return response @HANDLERS.register(("Alexa.ModeController", "AdjustMode")) async def async_api_adjust_mode(hass, config, directive, context): """Process a AdjustMode request. - Requires modeResources to be ordered. - Only modes that are ordered support the adjustMode directive. + Requires capabilityResources supportedModes to be ordered. + Only supportedModes with ordered=True support the adjustMode directive. """ - entity = directive.entity - instance = directive.instance - domain = entity.domain - if domain != fan.DOMAIN: - msg = "Entity does not support directive" - raise AlexaInvalidDirectiveError(msg) - - if instance is None: - msg = "Entity does not support directive" - raise AlexaInvalidDirectiveError(msg) - - # No modeResources are currently ordered to support this request. - - return directive.response() + # Currently no supportedModes are configured with ordered=True to support this request. + msg = "Entity does not support directive" + raise AlexaInvalidDirectiveError(msg) @HANDLERS.register(("Alexa.ToggleController", "TurnOn")) @@ -1008,19 +1020,29 @@ async def async_api_toggle_on(hass, config, directive, context): service = None data = {ATTR_ENTITY_ID: entity.entity_id} - if domain != fan.DOMAIN: - msg = "Entity does not support directive" - raise AlexaInvalidDirectiveError(msg) - + # Fan Oscillating if instance == f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}": service = fan.SERVICE_OSCILLATE data[fan.ATTR_OSCILLATING] = True + else: + msg = "Entity does not support directive" + raise AlexaInvalidDirectiveError(msg) await hass.services.async_call( domain, service, data, blocking=False, context=context ) - return directive.response() + response = directive.response() + response.add_context_property( + { + "namespace": "Alexa.ToggleController", + "instance": instance, + "name": "toggleState", + "value": "ON", + } + ) + + return response @HANDLERS.register(("Alexa.ToggleController", "TurnOff")) @@ -1032,19 +1054,29 @@ async def async_api_toggle_off(hass, config, directive, context): service = None data = {ATTR_ENTITY_ID: entity.entity_id} - if domain != fan.DOMAIN: - msg = "Entity does not support directive" - raise AlexaInvalidDirectiveError(msg) - + # Fan Oscillating if instance == f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}": service = fan.SERVICE_OSCILLATE data[fan.ATTR_OSCILLATING] = False + else: + msg = "Entity does not support directive" + raise AlexaInvalidDirectiveError(msg) await hass.services.async_call( domain, service, data, blocking=False, context=context ) - return directive.response() + response = directive.response() + response.add_context_property( + { + "namespace": "Alexa.ToggleController", + "instance": instance, + "name": "toggleState", + "value": "OFF", + } + ) + + return response @HANDLERS.register(("Alexa.RangeController", "SetRangeValue")) @@ -1055,15 +1087,12 @@ async def async_api_set_range(hass, config, directive, context): domain = entity.domain service = None data = {ATTR_ENTITY_ID: entity.entity_id} - range_value = int(directive.payload["rangeValue"]) - - if domain != fan.DOMAIN: - msg = "Entity does not support directive" - raise AlexaInvalidDirectiveError(msg) + range_value = directive.payload["rangeValue"] + # Fan Speed if instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}": service = fan.SERVICE_SET_SPEED - speed = SPEED_FAN_MAP.get(range_value, None) + speed = SPEED_FAN_MAP.get(int(range_value)) if not speed: msg = "Entity does not support value" @@ -1074,11 +1103,55 @@ async def async_api_set_range(hass, config, directive, context): data[fan.ATTR_SPEED] = speed + # Cover Position + elif instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": + range_value = int(range_value) + if range_value == 0: + service = cover.SERVICE_CLOSE_COVER + elif range_value == 100: + service = cover.SERVICE_OPEN_COVER + else: + service = cover.SERVICE_SET_COVER_POSITION + data[cover.ATTR_POSITION] = range_value + + # Cover Tilt Position + elif instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}": + range_value = int(range_value) + if range_value == 0: + service = cover.SERVICE_CLOSE_COVER_TILT + elif range_value == 100: + service = cover.SERVICE_OPEN_COVER_TILT + else: + service = cover.SERVICE_SET_COVER_TILT_POSITION + data[cover.ATTR_POSITION] = range_value + + # Input Number Value + elif instance == f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}": + range_value = float(range_value) + service = input_number.SERVICE_SET_VALUE + min_value = float(entity.attributes[input_number.ATTR_MIN]) + max_value = float(entity.attributes[input_number.ATTR_MAX]) + data[input_number.ATTR_VALUE] = min(max_value, max(min_value, range_value)) + + else: + msg = "Entity does not support directive" + raise AlexaInvalidDirectiveError(msg) + await hass.services.async_call( domain, service, data, blocking=False, context=context ) - return directive.response() + response = directive.response() + response.add_context_property( + { + "namespace": "Alexa.RangeController", + "instance": instance, + "name": "rangeValue", + "value": range_value, + } + ) + + return response @HANDLERS.register(("Alexa.RangeController", "AdjustRangeValue")) @@ -1089,25 +1162,71 @@ async def async_api_adjust_range(hass, config, directive, context): domain = entity.domain service = None data = {ATTR_ENTITY_ID: entity.entity_id} - range_delta = int(directive.payload["rangeValueDelta"]) + range_delta = directive.payload["rangeValueDelta"] + response_value = 0 + # Fan Speed if instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}": + range_delta = int(range_delta) service = fan.SERVICE_SET_SPEED - - # adjust range current_range = RANGE_FAN_MAP.get(entity.attributes.get(fan.ATTR_SPEED), 0) - speed = SPEED_FAN_MAP.get(max(0, range_delta + current_range), fan.SPEED_OFF) + speed = SPEED_FAN_MAP.get( + min(3, max(0, range_delta + current_range)), fan.SPEED_OFF + ) if speed == fan.SPEED_OFF: service = fan.SERVICE_TURN_OFF - data[fan.ATTR_SPEED] = speed + data[fan.ATTR_SPEED] = response_value = speed + + # Cover Position + elif instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": + range_delta = int(range_delta) + service = SERVICE_SET_COVER_POSITION + current = entity.attributes.get(cover.ATTR_POSITION) + data[cover.ATTR_POSITION] = response_value = min( + 100, max(0, range_delta + current) + ) + + # Cover Tilt Position + elif instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}": + range_delta = int(range_delta) + service = SERVICE_SET_COVER_TILT_POSITION + current = entity.attributes.get(cover.ATTR_TILT_POSITION) + data[cover.ATTR_TILT_POSITION] = response_value = min( + 100, max(0, range_delta + current) + ) + + # Input Number Value + elif instance == f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}": + range_delta = float(range_delta) + service = input_number.SERVICE_SET_VALUE + min_value = float(entity.attributes[input_number.ATTR_MIN]) + max_value = float(entity.attributes[input_number.ATTR_MAX]) + current = float(entity.state) + data[input_number.ATTR_VALUE] = response_value = min( + max_value, max(min_value, range_delta + current) + ) + + else: + msg = "Entity does not support directive" + raise AlexaInvalidDirectiveError(msg) await hass.services.async_call( domain, service, data, blocking=False, context=context ) - return directive.response() + response = directive.response() + response.add_context_property( + { + "namespace": "Alexa.RangeController", + "instance": instance, + "name": "rangeValue", + "value": response_value, + } + ) + + return response @HANDLERS.register(("Alexa.ChannelController", "ChangeChannel")) @@ -1115,21 +1234,25 @@ async def async_api_changechannel(hass, config, directive, context): """Process a change channel request.""" channel = "0" entity = directive.entity - payload = directive.payload["channel"] + channel_payload = directive.payload["channel"] + metadata_payload = directive.payload["channelMetadata"] payload_name = "number" - if "number" in payload: - channel = payload["number"] + if "number" in channel_payload: + channel = channel_payload["number"] payload_name = "number" - elif "callSign" in payload: - channel = payload["callSign"] + elif "callSign" in channel_payload: + channel = channel_payload["callSign"] payload_name = "callSign" - elif "affiliateCallSign" in payload: - channel = payload["affiliateCallSign"] + elif "affiliateCallSign" in channel_payload: + channel = channel_payload["affiliateCallSign"] payload_name = "affiliateCallSign" - elif "uri" in payload: - channel = payload["uri"] + elif "uri" in channel_payload: + channel = channel_payload["uri"] payload_name = "uri" + elif "name" in metadata_payload: + channel = metadata_payload["name"] + payload_name = "callSign" data = { ATTR_ENTITY_ID: entity.entity_id, @@ -1229,3 +1352,43 @@ async def async_api_seek(hass, config, directive, context): return directive.response( name="StateReport", namespace="Alexa.SeekController", payload=payload ) + + +@HANDLERS.register(("Alexa.EqualizerController", "SetMode")) +async def async_api_set_eq_mode(hass, config, directive, context): + """Process a SetMode request for EqualizerController.""" + mode = directive.payload["mode"] + entity = directive.entity + data = {ATTR_ENTITY_ID: entity.entity_id} + + sound_mode_list = entity.attributes.get(media_player.const.ATTR_SOUND_MODE_LIST) + if sound_mode_list and mode.lower() in sound_mode_list: + data[media_player.const.ATTR_SOUND_MODE] = mode.lower() + else: + msg = "failed to map sound mode {} to a mode on {}".format( + mode, entity.entity_id + ) + raise AlexaInvalidValueError(msg) + + await hass.services.async_call( + entity.domain, + media_player.SERVICE_SELECT_SOUND_MODE, + data, + blocking=False, + context=context, + ) + + return directive.response() + + +@HANDLERS.register(("Alexa.EqualizerController", "AdjustBands")) +@HANDLERS.register(("Alexa.EqualizerController", "ResetBands")) +@HANDLERS.register(("Alexa.EqualizerController", "SetBands")) +async def async_api_bands_directive(hass, config, directive, context): + """Handle an AdjustBands, ResetBands, SetBands request. + + Only mode directives are currently supported for the EqualizerController. + """ + # Currently bands directives are not supported. + msg = "Entity does not support directive" + raise AlexaInvalidDirectiveError(msg) diff --git a/homeassistant/components/alexa/resources.py b/homeassistant/components/alexa/resources.py new file mode 100644 index 00000000000..09927321c36 --- /dev/null +++ b/homeassistant/components/alexa/resources.py @@ -0,0 +1,387 @@ +"""Alexa Resources and Assets.""" + + +class AlexaGlobalCatalog: + """The Global Alexa catalog. + + https://developer.amazon.com/docs/device-apis/resources-and-assets.html#global-alexa-catalog + + You can use the global Alexa catalog for pre-defined names of devices, settings, values, and units. + This catalog is localized into all the languages that Alexa supports. + + You can reference the following catalog of pre-defined friendly names. + Each item in the following list is an asset identifier followed by its supported friendly names. + The first friendly name for each identifier is the one displayed in the Alexa mobile app. + """ + + # Air Purifier, Air Cleaner,Clean Air Machine + DEVICE_NAME_AIR_PURIFIER = "Alexa.DeviceName.AirPurifier" + + # Fan, Blower + DEVICE_NAME_FAN = "Alexa.DeviceName.Fan" + + # Router, Internet Router, Network Router, Wifi Router, Net Router + DEVICE_NAME_ROUTER = "Alexa.DeviceName.Router" + + # Shade, Blind, Curtain, Roller, Shutter, Drape, Awning, Window shade, Interior blind + DEVICE_NAME_SHADE = "Alexa.DeviceName.Shade" + + # Shower + DEVICE_NAME_SHOWER = "Alexa.DeviceName.Shower" + + # Space Heater, Portable Heater + DEVICE_NAME_SPACE_HEATER = "Alexa.DeviceName.SpaceHeater" + + # Washer, Washing Machine + DEVICE_NAME_WASHER = "Alexa.DeviceName.Washer" + + # 2.4G Guest Wi-Fi, 2.4G Guest Network, Guest Network 2.4G, 2G Guest Wifi + SETTING_2G_GUEST_WIFI = "Alexa.Setting.2GGuestWiFi" + + # 5G Guest Wi-Fi, 5G Guest Network, Guest Network 5G, 5G Guest Wifi + SETTING_5G_GUEST_WIFI = "Alexa.Setting.5GGuestWiFi" + + # Auto, Automatic, Automatic Mode, Auto Mode + SETTING_AUTO = "Alexa.Setting.Auto" + + # Direction + SETTING_DIRECTION = "Alexa.Setting.Direction" + + # Dry Cycle, Dry Preset, Dry Setting, Dryer Cycle, Dryer Preset, Dryer Setting + SETTING_DRY_CYCLE = "Alexa.Setting.DryCycle" + + # Fan Speed, Airflow speed, Wind Speed, Air speed, Air velocity + SETTING_FAN_SPEED = "Alexa.Setting.FanSpeed" + + # Guest Wi-fi, Guest Network, Guest Net + SETTING_GUEST_WIFI = "Alexa.Setting.GuestWiFi" + + # Heat + SETTING_HEAT = "Alexa.Setting.Heat" + + # Mode + SETTING_MODE = "Alexa.Setting.Mode" + + # Night, Night Mode + SETTING_NIGHT = "Alexa.Setting.Night" + + # Opening, Height, Lift, Width + SETTING_OPENING = "Alexa.Setting.Opening" + + # Oscillate, Swivel, Oscillation, Spin, Back and forth + SETTING_OSCILLATE = "Alexa.Setting.Oscillate" + + # Preset, Setting + SETTING_PRESET = "Alexa.Setting.Preset" + + # Quiet, Quiet Mode, Noiseless, Silent + SETTING_QUIET = "Alexa.Setting.Quiet" + + # Temperature, Temp + SETTING_TEMPERATURE = "Alexa.Setting.Temperature" + + # Wash Cycle, Wash Preset, Wash setting + SETTING_WASH_CYCLE = "Alexa.Setting.WashCycle" + + # Water Temperature, Water Temp, Water Heat + SETTING_WATER_TEMPERATURE = "Alexa.Setting.WaterTemperature" + + # Handheld Shower, Shower Wand, Hand Shower + SHOWER_HAND_HELD = "Alexa.Shower.HandHeld" + + # Rain Head, Overhead shower, Rain Shower, Rain Spout, Rain Faucet + SHOWER_RAIN_HEAD = "Alexa.Shower.RainHead" + + # Degrees, Degree + UNIT_ANGLE_DEGREES = "Alexa.Unit.Angle.Degrees" + + # Radians, Radian + UNIT_ANGLE_RADIANS = "Alexa.Unit.Angle.Radians" + + # Feet, Foot + UNIT_DISTANCE_FEET = "Alexa.Unit.Distance.Feet" + + # Inches, Inch + UNIT_DISTANCE_INCHES = "Alexa.Unit.Distance.Inches" + + # Kilometers + UNIT_DISTANCE_KILOMETERS = "Alexa.Unit.Distance.Kilometers" + + # Meters, Meter, m + UNIT_DISTANCE_METERS = "Alexa.Unit.Distance.Meters" + + # Miles, Mile + UNIT_DISTANCE_MILES = "Alexa.Unit.Distance.Miles" + + # Yards, Yard + UNIT_DISTANCE_YARDS = "Alexa.Unit.Distance.Yards" + + # Grams, Gram, g + UNIT_MASS_GRAMS = "Alexa.Unit.Mass.Grams" + + # Kilograms, Kilogram, kg + UNIT_MASS_KILOGRAMS = "Alexa.Unit.Mass.Kilograms" + + # Percent + UNIT_PERCENT = "Alexa.Unit.Percent" + + # Celsius, Degrees Celsius, Degrees, C, Centigrade, Degrees Centigrade + UNIT_TEMPERATURE_CELSIUS = "Alexa.Unit.Temperature.Celsius" + + # Degrees, Degree + UNIT_TEMPERATURE_DEGREES = "Alexa.Unit.Temperature.Degrees" + + # Fahrenheit, Degrees Fahrenheit, Degrees F, Degrees, F + UNIT_TEMPERATURE_FAHRENHEIT = "Alexa.Unit.Temperature.Fahrenheit" + + # Kelvin, Degrees Kelvin, Degrees K, Degrees, K + UNIT_TEMPERATURE_KELVIN = "Alexa.Unit.Temperature.Kelvin" + + # Cubic Feet, Cubic Foot + UNIT_VOLUME_CUBIC_FEET = "Alexa.Unit.Volume.CubicFeet" + + # Cubic Meters, Cubic Meter, Meters Cubed + UNIT_VOLUME_CUBIC_METERS = "Alexa.Unit.Volume.CubicMeters" + + # Gallons, Gallon + UNIT_VOLUME_GALLONS = "Alexa.Unit.Volume.Gallons" + + # Liters, Liter, L + UNIT_VOLUME_LITERS = "Alexa.Unit.Volume.Liters" + + # Pints, Pint + UNIT_VOLUME_PINTS = "Alexa.Unit.Volume.Pints" + + # Quarts, Quart + UNIT_VOLUME_QUARTS = "Alexa.Unit.Volume.Quarts" + + # Ounces, Ounce, oz + UNIT_WEIGHT_OUNCES = "Alexa.Unit.Weight.Ounces" + + # Pounds, Pound, lbs + UNIT_WEIGHT_POUNDS = "Alexa.Unit.Weight.Pounds" + + # Close + VALUE_CLOSE = "Alexa.Value.Close" + + # Delicates, Delicate + VALUE_DELICATE = "Alexa.Value.Delicate" + + # High + VALUE_HIGH = "Alexa.Value.High" + + # Low + VALUE_LOW = "Alexa.Value.Low" + + # Maximum, Max + VALUE_MAXIMUM = "Alexa.Value.Maximum" + + # Medium, Mid + VALUE_MEDIUM = "Alexa.Value.Medium" + + # Minimum, Min + VALUE_MINIMUM = "Alexa.Value.Minimum" + + # Open + VALUE_OPEN = "Alexa.Value.Open" + + # Quick Wash, Fast Wash, Wash Quickly, Speed Wash + VALUE_QUICK_WASH = "Alexa.Value.QuickWash" + + +class AlexaCapabilityResource: + """Base class for Alexa capabilityResources, ModeResources, and presetResources objects. + + https://developer.amazon.com/docs/device-apis/resources-and-assets.html#capability-resources + """ + + def __init__(self, labels): + """Initialize an Alexa resource.""" + self._resource_labels = [] + for label in labels: + self._resource_labels.append(label) + + def serialize_capability_resources(self): + """Return capabilityResources object serialized for an API response.""" + return self.serialize_labels(self._resource_labels) + + @staticmethod + def serialize_configuration(): + """Return ModeResources, PresetResources friendlyNames serialized for an API response.""" + return [] + + @staticmethod + def serialize_labels(resources): + """Return resource label objects for friendlyNames serialized for an API response.""" + labels = [] + for label in resources: + if label in AlexaGlobalCatalog.__dict__.values(): + label = {"@type": "asset", "value": {"assetId": label}} + else: + label = {"@type": "text", "value": {"text": label, "locale": "en-US"}} + + labels.append(label) + + return {"friendlyNames": labels} + + +class AlexaModeResource(AlexaCapabilityResource): + """Implements Alexa ModeResources. + + https://developer.amazon.com/docs/device-apis/resources-and-assets.html#capability-resources + """ + + def __init__(self, labels, ordered=False): + """Initialize an Alexa modeResource.""" + super().__init__(labels) + self._supported_modes = [] + self._mode_ordered = ordered + + def add_mode(self, value, labels): + """Add mode to the supportedModes object.""" + self._supported_modes.append({"value": value, "labels": labels}) + + def serialize_configuration(self): + """Return configuration for ModeResources friendlyNames serialized for an API response.""" + mode_resources = [] + for mode in self._supported_modes: + result = { + "value": mode["value"], + "modeResources": self.serialize_labels(mode["labels"]), + } + mode_resources.append(result) + + return {"ordered": self._mode_ordered, "supportedModes": mode_resources} + + +class AlexaPresetResource(AlexaCapabilityResource): + """Implements Alexa PresetResources. + + Use presetResources with RangeController to provide a set of friendlyNames for each RangeController preset. + + https://developer.amazon.com/docs/device-apis/resources-and-assets.html#presetresources + """ + + def __init__(self, labels, min_value, max_value, precision, unit=None): + """Initialize an Alexa presetResource.""" + super().__init__(labels) + self._presets = [] + self._minimum_value = min_value + self._maximum_value = max_value + self._precision = precision + self._unit_of_measure = None + if unit in AlexaGlobalCatalog.__dict__.values(): + self._unit_of_measure = unit + + def add_preset(self, value, labels): + """Add preset to configuration presets array.""" + self._presets.append({"value": value, "labels": labels}) + + def serialize_configuration(self): + """Return configuration for PresetResources friendlyNames serialized for an API response.""" + configuration = { + "supportedRange": { + "minimumValue": self._minimum_value, + "maximumValue": self._maximum_value, + "precision": self._precision, + } + } + + if self._unit_of_measure: + configuration["unitOfMeasure"] = self._unit_of_measure + + if self._presets: + preset_resources = [] + for preset in self._presets: + preset_resources.append( + { + "rangeValue": preset["value"], + "presetResources": self.serialize_labels(preset["labels"]), + } + ) + configuration["presets"] = preset_resources + + return configuration + + +class AlexaSemantics: + """Class for Alexa Semantics Object. + + You can optionally enable additional utterances by using semantics. When you use semantics, + you manually map the phrases "open", "close", "raise", and "lower" to directives. + + Semantics is supported for the following interfaces only: ModeController, RangeController, and ToggleController. + + https://developer.amazon.com/docs/device-apis/alexa-discovery.html#semantics-object + """ + + MAPPINGS_ACTION = "actionMappings" + MAPPINGS_STATE = "stateMappings" + + ACTIONS_TO_DIRECTIVE = "ActionsToDirective" + STATES_TO_VALUE = "StatesToValue" + STATES_TO_RANGE = "StatesToRange" + + ACTION_CLOSE = "Alexa.Actions.Close" + ACTION_LOWER = "Alexa.Actions.Lower" + ACTION_OPEN = "Alexa.Actions.Open" + ACTION_RAISE = "Alexa.Actions.Raise" + + STATES_OPEN = "Alexa.States.Open" + STATES_CLOSED = "Alexa.States.Closed" + + DIRECTIVE_RANGE_SET_VALUE = "SetRangeValue" + DIRECTIVE_RANGE_ADJUST_VALUE = "AdjustRangeValue" + DIRECTIVE_TOGGLE_TURN_ON = "TurnOn" + DIRECTIVE_TOGGLE_TURN_OFF = "TurnOff" + DIRECTIVE_MODE_SET_MODE = "SetMode" + DIRECTIVE_MODE_ADJUST_MODE = "AdjustMode" + + def __init__(self): + """Initialize an Alexa modeResource.""" + self._action_mappings = [] + self._state_mappings = [] + + def _add_action_mapping(self, semantics): + """Add action mapping between actions and interface directives.""" + self._action_mappings.append(semantics) + + def _add_state_mapping(self, semantics): + """Add state mapping between states and interface directives.""" + self._state_mappings.append(semantics) + + def add_states_to_value(self, states, value): + """Add StatesToValue stateMappings.""" + self._add_state_mapping( + {"@type": self.STATES_TO_VALUE, "states": states, "value": value} + ) + + def add_states_to_range(self, states, min_value, max_value): + """Add StatesToRange stateMappings.""" + self._add_state_mapping( + { + "@type": self.STATES_TO_RANGE, + "states": states, + "range": {"minimumValue": min_value, "maximumValue": max_value}, + } + ) + + def add_action_to_directive(self, actions, directive, payload): + """Add ActionsToDirective actionMappings.""" + self._add_action_mapping( + { + "@type": self.ACTIONS_TO_DIRECTIVE, + "actions": actions, + "directive": {"name": directive, "payload": payload}, + } + ) + + def serialize_semantics(self): + """Return semantics object serialized for an API response.""" + semantics = {} + if self._action_mappings: + semantics[self.MAPPINGS_ACTION] = self._action_mappings + if self._state_mappings: + semantics[self.MAPPINGS_STATE] = self._state_mappings + + return semantics diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index 2c34542e25c..9b0955f8fca 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -4,7 +4,7 @@ import logging import homeassistant.core as ha from .const import API_DIRECTIVE, API_HEADER -from .errors import AlexaError, AlexaBridgeUnreachableError +from .errors import AlexaBridgeUnreachableError, AlexaError from .handlers import HANDLERS from .messages import AlexaDirective diff --git a/homeassistant/components/alexa/smart_home_http.py b/homeassistant/components/alexa/smart_home_http.py index ada00e8a326..08d33ffa09c 100644 --- a/homeassistant/components/alexa/smart_home_http.py +++ b/homeassistant/components/alexa/smart_home_http.py @@ -13,8 +13,8 @@ from .const import ( CONF_ENTITY_CONFIG, CONF_FILTER, ) -from .state_report import async_enable_proactive_mode from .smart_home import async_handle_message +from .state_report import async_enable_proactive_mode _LOGGER = logging.getLogger(__name__) SMART_HOME_HTTP_ENDPOINT = "/api/alexa/smart_home" diff --git a/homeassistant/components/alexa/state_report.py b/homeassistant/components/alexa/state_report.py index b5e1b741f0c..44e1b7f4f55 100644 --- a/homeassistant/components/alexa/state_report.py +++ b/homeassistant/components/alexa/state_report.py @@ -6,8 +6,8 @@ import logging import aiohttp import async_timeout -import homeassistant.util.dt as dt_util from homeassistant.const import MATCH_ALL, STATE_ON +import homeassistant.util.dt as dt_util from .const import API_CHANGE, Cause from .entities import ENTITY_ADAPTERS diff --git a/homeassistant/components/almond/.translations/bg.json b/homeassistant/components/almond/.translations/bg.json index da5571ad029..3327e34e765 100644 --- a/homeassistant/components/almond/.translations/bg.json +++ b/homeassistant/components/almond/.translations/bg.json @@ -2,7 +2,13 @@ "config": { "abort": { "already_setup": "\u041c\u043e\u0436\u0435\u0442\u0435 \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u0438\u043d Almond \u0430\u043a\u0430\u0443\u043d\u0442.", - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 Almond \u0441\u044a\u0440\u0432\u044a\u0440\u0430." + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 Almond \u0441\u044a\u0440\u0432\u044a\u0440\u0430.", + "missing_configuration": "\u041c\u043e\u043b\u044f, \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u0442\u0435 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430 \u043a\u0430\u043a \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 Almond." + }, + "step": { + "pick_implementation": { + "title": "\u0418\u0437\u0431\u043e\u0440 \u043d\u0430 \u043c\u0435\u0442\u043e\u0434 \u0437\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f" + } }, "title": "Almond" } diff --git a/homeassistant/components/almond/.translations/da.json b/homeassistant/components/almond/.translations/da.json new file mode 100644 index 00000000000..93158cee94f --- /dev/null +++ b/homeassistant/components/almond/.translations/da.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_setup": "Du kan kun konfigurere en Almond-konto.", + "cannot_connect": "Kan ikke oprette forbindelse til Almond-serveren.", + "missing_configuration": "Tjek venligst dokumentationen om, hvordan man indstiller Almond." + }, + "step": { + "pick_implementation": { + "title": "V\u00e6lg godkendelsesmetode" + } + }, + "title": "Almond" + } +} \ No newline at end of file diff --git a/homeassistant/components/almond/.translations/de.json b/homeassistant/components/almond/.translations/de.json index 4e2816cb001..1495cabf9c9 100644 --- a/homeassistant/components/almond/.translations/de.json +++ b/homeassistant/components/almond/.translations/de.json @@ -2,7 +2,13 @@ "config": { "abort": { "already_setup": "Sie k\u00f6nnen nur ein Almond-Konto konfigurieren.", - "cannot_connect": "Verbindung zum Almond-Server nicht m\u00f6glich." + "cannot_connect": "Verbindung zum Almond-Server nicht m\u00f6glich.", + "missing_configuration": "Bitte \u00fcberpr\u00fcfen Sie die Dokumentation zur Einrichtung von Almond." + }, + "step": { + "pick_implementation": { + "title": "W\u00e4hle die Authentifizierungsmethode" + } }, "title": "Almond" } diff --git a/homeassistant/components/almond/.translations/fr.json b/homeassistant/components/almond/.translations/fr.json index 0208366cea1..9ae881d332c 100644 --- a/homeassistant/components/almond/.translations/fr.json +++ b/homeassistant/components/almond/.translations/fr.json @@ -5,6 +5,11 @@ "cannot_connect": "Impossible de se connecter au serveur Almond", "missing_configuration": "Veuillez consulter la documentation pour savoir comment configurer Almond." }, + "step": { + "pick_implementation": { + "title": "S\u00e9lectionner une m\u00e9thode d'authentification" + } + }, "title": "Almond" } } \ No newline at end of file diff --git a/homeassistant/components/almond/.translations/it.json b/homeassistant/components/almond/.translations/it.json index 740535f4f46..9d529e5e5c8 100644 --- a/homeassistant/components/almond/.translations/it.json +++ b/homeassistant/components/almond/.translations/it.json @@ -5,6 +5,11 @@ "cannot_connect": "Impossibile connettersi al server Almond.", "missing_configuration": "Si prega di controllare la documentazione su come impostare Almond." }, + "step": { + "pick_implementation": { + "title": "Seleziona metodo di autenticazione" + } + }, "title": "Almond" } } \ No newline at end of file diff --git a/homeassistant/components/almond/.translations/ko.json b/homeassistant/components/almond/.translations/ko.json index 9440242ebbc..9f1e71163d6 100644 --- a/homeassistant/components/almond/.translations/ko.json +++ b/homeassistant/components/almond/.translations/ko.json @@ -2,7 +2,13 @@ "config": { "abort": { "already_setup": "\ud558\ub098\uc758 Almond \uacc4\uc815\ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", - "cannot_connect": "Almond \uc11c\ubc84\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4." + "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." + }, + "step": { + "pick_implementation": { + "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd" + } }, "title": "Almond" } diff --git a/homeassistant/components/almond/.translations/nl.json b/homeassistant/components/almond/.translations/nl.json index dfe9c238db7..d77fe69f7fa 100644 --- a/homeassistant/components/almond/.translations/nl.json +++ b/homeassistant/components/almond/.translations/nl.json @@ -5,6 +5,11 @@ "cannot_connect": "Kan geen verbinding maken met de Almond-server.", "missing_configuration": "Raadpleeg de documentatie over het instellen van Almond." }, + "step": { + "pick_implementation": { + "title": "Kies de authenticatie methode" + } + }, "title": "Almond" } } \ No newline at end of file diff --git a/homeassistant/components/almond/.translations/nn.json b/homeassistant/components/almond/.translations/nn.json new file mode 100644 index 00000000000..a25f5dc1574 --- /dev/null +++ b/homeassistant/components/almond/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Almond" + } +} \ No newline at end of file diff --git a/homeassistant/components/almond/.translations/pt-BR.json b/homeassistant/components/almond/.translations/pt-BR.json new file mode 100644 index 00000000000..94dfbefb86a --- /dev/null +++ b/homeassistant/components/almond/.translations/pt-BR.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "pick_implementation": { + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/almond/__init__.py b/homeassistant/components/almond/__init__.py index 6d4ab31bf17..8877107b984 100644 --- a/homeassistant/components/almond/__init__.py +++ b/homeassistant/components/almond/__init__.py @@ -5,26 +5,26 @@ import logging import time from typing import Optional +from aiohttp import ClientError, ClientSession import async_timeout -from aiohttp import ClientSession, ClientError -from pyalmond import AlmondLocalAuth, AbstractAlmondWebAuth, WebAlmondAPI +from pyalmond import AbstractAlmondWebAuth, AlmondLocalAuth, WebAlmondAPI import voluptuous as vol -from homeassistant.core import HomeAssistant, CoreState -from homeassistant.const import CONF_TYPE, CONF_HOST, EVENT_HOMEASSISTANT_START -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant import config_entries from homeassistant.auth.const import GROUP_ID_ADMIN +from homeassistant.components import conversation +from homeassistant.const import CONF_HOST, CONF_TYPE, EVENT_HOMEASSISTANT_START +from homeassistant.core import Context, CoreState, HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import ( - config_validation as cv, + aiohttp_client, config_entry_oauth2_flow, + config_validation as cv, event, intent, - aiohttp_client, - storage, network, + storage, ) -from homeassistant import config_entries -from homeassistant.components import conversation from . import config_flow from .const import DOMAIN, TYPE_LOCAL, TYPE_OAUTH2 @@ -263,8 +263,6 @@ class AlmondAgent(conversation.AbstractConversationAgent): host = self.entry.data["host"] if self.entry.data.get("is_hassio"): host = "/core_almond" - elif self.entry.data["type"] != TYPE_LOCAL: - host = f"{host}/me" return { "text": "Would you like to opt-in to share your anonymized commands with Stanford to improve Almond's responses?", "url": f"{host}/conversation", @@ -279,7 +277,7 @@ class AlmondAgent(conversation.AbstractConversationAgent): return True async def async_process( - self, text: str, conversation_id: Optional[str] = None + self, text: str, context: Context, conversation_id: Optional[str] = None ) -> intent.IntentResponse: """Process a sentence.""" response = await self.api.async_converse_text(text, conversation_id) diff --git a/homeassistant/components/almond/config_flow.py b/homeassistant/components/almond/config_flow.py index d79bf6bd605..42f9318a06f 100644 --- a/homeassistant/components/almond/config_flow.py +++ b/homeassistant/components/almond/config_flow.py @@ -2,14 +2,14 @@ import asyncio import logging -import async_timeout from aiohttp import ClientError -from yarl import URL -import voluptuous as vol +import async_timeout from pyalmond import AlmondLocalAuth, WebAlmondAPI +import voluptuous as vol +from yarl import URL -from homeassistant import data_entry_flow, config_entries, core -from homeassistant.helpers import config_entry_oauth2_flow, aiohttp_client +from homeassistant import config_entries, core, data_entry_flow +from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow from .const import DOMAIN, TYPE_LOCAL, TYPE_OAUTH2 diff --git a/homeassistant/components/alpha_vantage/sensor.py b/homeassistant/components/alpha_vantage/sensor.py index da29e4e25e1..7d871c286e5 100644 --- a/homeassistant/components/alpha_vantage/sensor.py +++ b/homeassistant/components/alpha_vantage/sensor.py @@ -2,9 +2,9 @@ from datetime import timedelta import logging -import voluptuous as vol -from alpha_vantage.timeseries import TimeSeries from alpha_vantage.foreignexchange import ForeignExchange +from alpha_vantage.timeseries import TimeSeries +import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_CURRENCY, CONF_NAME diff --git a/homeassistant/components/amazon_polly/tts.py b/homeassistant/components/amazon_polly/tts.py index 3d05236935f..bcb4a24e95b 100644 --- a/homeassistant/components/amazon_polly/tts.py +++ b/homeassistant/components/amazon_polly/tts.py @@ -1,7 +1,7 @@ """Support for the Amazon Polly text to speech service.""" import logging -import boto3 +import boto3 import voluptuous as vol from homeassistant.components.tts import PLATFORM_SCHEMA, Provider diff --git a/homeassistant/components/ambiclimate/.translations/ru.json b/homeassistant/components/ambiclimate/.translations/ru.json index ba667ea7b9a..2a99430e436 100644 --- a/homeassistant/components/ambiclimate/.translations/ru.json +++ b/homeassistant/components/ambiclimate/.translations/ru.json @@ -2,7 +2,7 @@ "config": { "abort": { "access_token": "\u041f\u0440\u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0438 \u0442\u043e\u043a\u0435\u043d\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430.", - "already_setup": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430.", + "already_setup": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430.", "no_config": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 Ambiclimate \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \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](https://www.home-assistant.io/components/ambiclimate/)." }, "create_entry": { @@ -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\u0435\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 \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/__init__.py b/homeassistant/components/ambiclimate/__init__.py index 962c8c8a82d..e15f6dea2ec 100644 --- a/homeassistant/components/ambiclimate/__init__.py +++ b/homeassistant/components/ambiclimate/__init__.py @@ -4,10 +4,10 @@ import logging import voluptuous as vol from homeassistant.helpers import config_validation as cv + from . import config_flow from .const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, DOMAIN - _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema( diff --git a/homeassistant/components/ambiclimate/climate.py b/homeassistant/components/ambiclimate/climate.py index bb3e5ab2b25..a8ed166903e 100644 --- a/homeassistant/components/ambiclimate/climate.py +++ b/homeassistant/components/ambiclimate/climate.py @@ -7,13 +7,14 @@ import voluptuous as vol from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( - SUPPORT_TARGET_TEMPERATURE, - HVAC_MODE_OFF, HVAC_MODE_HEAT, + HVAC_MODE_OFF, + SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ATTR_NAME, ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession + from .const import ( ATTR_VALUE, CONF_CLIENT_ID, diff --git a/homeassistant/components/ambiclimate/config_flow.py b/homeassistant/components/ambiclimate/config_flow.py index 99563dcb97d..4996a458a1f 100644 --- a/homeassistant/components/ambiclimate/config_flow.py +++ b/homeassistant/components/ambiclimate/config_flow.py @@ -7,14 +7,15 @@ from homeassistant import config_entries from homeassistant.components.http import HomeAssistantView from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession + from .const import ( AUTH_CALLBACK_NAME, AUTH_CALLBACK_PATH, CONF_CLIENT_ID, CONF_CLIENT_SECRET, DOMAIN, - STORAGE_VERSION, STORAGE_KEY, + STORAGE_VERSION, ) DATA_AMBICLIMATE_IMPL = "ambiclimate_flow_implementation" diff --git a/homeassistant/components/ambiclimate/manifest.json b/homeassistant/components/ambiclimate/manifest.json index 3d175165abd..151b761dff8 100644 --- a/homeassistant/components/ambiclimate/manifest.json +++ b/homeassistant/components/ambiclimate/manifest.json @@ -3,11 +3,7 @@ "name": "Ambiclimate", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ambiclimate", - "requirements": [ - "ambiclimate==0.2.1" - ], - "dependencies": [], - "codeowners": [ - "@danielhiversen" - ] + "requirements": ["ambiclimate==0.2.1"], + "dependencies": ["http"], + "codeowners": ["@danielhiversen"] } diff --git a/homeassistant/components/ambient_station/.translations/da.json b/homeassistant/components/ambient_station/.translations/da.json index ac3d86a995b..6cec31eca29 100644 --- a/homeassistant/components/ambient_station/.translations/da.json +++ b/homeassistant/components/ambient_station/.translations/da.json @@ -8,7 +8,7 @@ "step": { "user": { "data": { - "api_key": "API n\u00f8gle", + "api_key": "API-n\u00f8gle", "app_key": "Applikationsn\u00f8gle" }, "title": "Udfyld dine oplysninger" diff --git a/homeassistant/components/ambient_station/.translations/ko.json b/homeassistant/components/ambient_station/.translations/ko.json index 541b8699dc8..eb9209a6c37 100644 --- a/homeassistant/components/ambient_station/.translations/ko.json +++ b/homeassistant/components/ambient_station/.translations/ko.json @@ -1,15 +1,15 @@ { "config": { "error": { - "identifier_exists": "Application \ud0a4 \ud639\uc740 API \ud0a4\uac00 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "invalid_key": "Application \ud0a4 \ud639\uc740 API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "identifier_exists": "\uc560\ud50c\ub9ac\ucf00\uc774\uc158 \ud0a4 \ud639\uc740 API \ud0a4\uac00 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "invalid_key": "\uc560\ud50c\ub9ac\ucf00\uc774\uc158 \ud0a4 \ud639\uc740 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": { "user": { "data": { "api_key": "API \ud0a4", - "app_key": "Application \ud0a4" + "app_key": "\uc560\ud50c\ub9ac\ucf00\uc774\uc158 \ud0a4" }, "title": "\uc0ac\uc6a9\uc790 \uc815\ubcf4 \uc785\ub825" } diff --git a/homeassistant/components/ambient_station/.translations/ru.json b/homeassistant/components/ambient_station/.translations/ru.json index 3a7c405ea4c..438b1cf87a7 100644 --- a/homeassistant/components/ambient_station/.translations/ru.json +++ b/homeassistant/components/ambient_station/.translations/ru.json @@ -3,7 +3,7 @@ "error": { "identifier_exists": "\u041a\u043b\u044e\u0447 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0438/\u0438\u043b\u0438 \u043a\u043b\u044e\u0447 API \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d.", "invalid_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API \u0438/\u0438\u043b\u0438 \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f.", - "no_devices": "\u0412 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b." + "no_devices": "\u0412 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b." }, "step": { "user": { diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index 7a805d6b867..58389dd1831 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -8,8 +8,8 @@ import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( - ATTR_NAME, ATTR_LOCATION, + ATTR_NAME, CONF_API_KEY, EVENT_HOMEASSISTANT_STOP, ) diff --git a/homeassistant/components/ambient_station/config_flow.py b/homeassistant/components/ambient_station/config_flow.py index 256e55ba402..c20b43598ca 100644 --- a/homeassistant/components/ambient_station/config_flow.py +++ b/homeassistant/components/ambient_station/config_flow.py @@ -1,4 +1,6 @@ """Config flow to configure the Ambient PWS component.""" +from aioambient import Client +from aioambient.errors import AmbientError import voluptuous as vol from homeassistant import config_entries @@ -40,8 +42,6 @@ class AmbientStationFlowHandler(config_entries.ConfigFlow): async def async_step_user(self, user_input=None): """Handle the start of the config flow.""" - from aioambient import Client - from aioambient.errors import AmbientError if not user_input: return await self._show_form() diff --git a/homeassistant/components/ambient_station/manifest.json b/homeassistant/components/ambient_station/manifest.json index 8f363ba219f..1e6c06f260a 100644 --- a/homeassistant/components/ambient_station/manifest.json +++ b/homeassistant/components/ambient_station/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ambient_station", "requirements": [ - "aioambient==0.3.2" + "aioambient==1.0.2" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/amcrest/__init__.py b/homeassistant/components/amcrest/__init__.py index d49104a0b26..b934a7e0549 100644 --- a/homeassistant/components/amcrest/__init__.py +++ b/homeassistant/components/amcrest/__init__.py @@ -1,6 +1,6 @@ """Support for Amcrest IP cameras.""" -import logging from datetime import timedelta +import logging import threading import aiohttp @@ -11,7 +11,6 @@ from homeassistant.auth.permissions.const import POLICY_CONTROL from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR from homeassistant.components.camera import DOMAIN as CAMERA from homeassistant.components.sensor import DOMAIN as SENSOR -from homeassistant.components.switch import DOMAIN as SWITCH from homeassistant.const import ( ATTR_ENTITY_ID, CONF_AUTHENTICATION, @@ -22,7 +21,6 @@ from homeassistant.const import ( CONF_PORT, CONF_SCAN_INTERVAL, CONF_SENSORS, - CONF_SWITCHES, CONF_USERNAME, ENTITY_MATCH_ALL, HTTP_BASIC_AUTHENTICATION, @@ -34,12 +32,11 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send, dispatcher_s from homeassistant.helpers.event import track_time_interval from homeassistant.helpers.service import async_extract_entity_ids -from .binary_sensor import BINARY_SENSOR_MOTION_DETECTED, BINARY_SENSORS +from .binary_sensor import BINARY_SENSORS from .camera import CAMERA_SERVICES, STREAM_SOURCE_LIST -from .const import CAMERAS, DOMAIN, DATA_AMCREST, DEVICES, SERVICE_UPDATE +from .const import CAMERAS, DATA_AMCREST, DEVICES, DOMAIN, SERVICE_UPDATE from .helpers import service_signal -from .sensor import SENSOR_MOTION_DETECTOR, SENSORS -from .switch import SWITCHES +from .sensor import SENSORS _LOGGER = logging.getLogger(__name__) @@ -65,68 +62,36 @@ SCAN_INTERVAL = timedelta(seconds=10) AUTHENTICATION_LIST = {"basic": "basic"} -def _deprecated_sensor_values(sensors): - if SENSOR_MOTION_DETECTOR in sensors: - _LOGGER.warning( - "The '%s' option value '%s' is deprecated, " - "please remove it from your configuration and use " - "the '%s' option with value '%s' instead", - CONF_SENSORS, - SENSOR_MOTION_DETECTOR, - CONF_BINARY_SENSORS, - BINARY_SENSOR_MOTION_DETECTED, - ) - return sensors - - -def _deprecated_switches(config): - if CONF_SWITCHES in config: - _LOGGER.warning( - "The '%s' option (with value %s) is deprecated, " - "please remove it from your configuration and use " - "services and attributes instead", - CONF_SWITCHES, - config[CONF_SWITCHES], - ) - return config - - def _has_unique_names(devices): names = [device[CONF_NAME] for device in devices] vol.Schema(vol.Unique())(names) return devices -AMCREST_SCHEMA = vol.All( - vol.Schema( - { - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional( - CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION - ): vol.All(vol.In(AUTHENTICATION_LIST)), - vol.Optional(CONF_RESOLUTION, default=DEFAULT_RESOLUTION): vol.All( - vol.In(RESOLUTION_LIST) - ), - vol.Optional(CONF_STREAM_SOURCE, default=STREAM_SOURCE_LIST[0]): vol.All( - vol.In(STREAM_SOURCE_LIST) - ), - vol.Optional(CONF_FFMPEG_ARGUMENTS, default=DEFAULT_ARGUMENTS): cv.string, - vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): cv.time_period, - vol.Optional(CONF_BINARY_SENSORS): vol.All( - cv.ensure_list, [vol.In(BINARY_SENSORS)] - ), - vol.Optional(CONF_SENSORS): vol.All( - cv.ensure_list, [vol.In(SENSORS)], _deprecated_sensor_values - ), - vol.Optional(CONF_SWITCHES): vol.All(cv.ensure_list, [vol.In(SWITCHES)]), - vol.Optional(CONF_CONTROL_LIGHT, default=True): cv.boolean, - } - ), - _deprecated_switches, +AMCREST_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION): vol.All( + vol.In(AUTHENTICATION_LIST) + ), + vol.Optional(CONF_RESOLUTION, default=DEFAULT_RESOLUTION): vol.All( + vol.In(RESOLUTION_LIST) + ), + vol.Optional(CONF_STREAM_SOURCE, default=STREAM_SOURCE_LIST[0]): vol.All( + vol.In(STREAM_SOURCE_LIST) + ), + vol.Optional(CONF_FFMPEG_ARGUMENTS, default=DEFAULT_ARGUMENTS): cv.string, + vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): cv.time_period, + vol.Optional(CONF_BINARY_SENSORS): vol.All( + cv.ensure_list, [vol.In(BINARY_SENSORS)] + ), + vol.Optional(CONF_SENSORS): vol.All(cv.ensure_list, [vol.In(SENSORS)]), + vol.Optional(CONF_CONTROL_LIGHT, default=True): cv.boolean, + } ) CONFIG_SCHEMA = vol.Schema( @@ -216,7 +181,6 @@ def setup(hass, config): resolution = RESOLUTION_LIST[device[CONF_RESOLUTION]] binary_sensors = device.get(CONF_BINARY_SENSORS) sensors = device.get(CONF_SENSORS) - switches = device.get(CONF_SWITCHES) stream_source = device[CONF_STREAM_SOURCE] control_light = device.get(CONF_CONTROL_LIGHT) @@ -252,11 +216,6 @@ def setup(hass, config): hass, SENSOR, DOMAIN, {CONF_NAME: name, CONF_SENSORS: sensors}, config ) - if switches: - discovery.load_platform( - hass, SWITCH, DOMAIN, {CONF_NAME: name, CONF_SWITCHES: switches}, config - ) - if not hass.data[DATA_AMCREST][DEVICES]: return False diff --git a/homeassistant/components/amcrest/binary_sensor.py b/homeassistant/components/amcrest/binary_sensor.py index f8b50d1114e..ac16f0664aa 100644 --- a/homeassistant/components/amcrest/binary_sensor.py +++ b/homeassistant/components/amcrest/binary_sensor.py @@ -5,11 +5,11 @@ import logging from amcrest import AmcrestError from homeassistant.components.binary_sensor import ( - BinarySensorDevice, DEVICE_CLASS_CONNECTIVITY, DEVICE_CLASS_MOTION, + BinarySensorDevice, ) -from homeassistant.const import CONF_NAME, CONF_BINARY_SENSORS +from homeassistant.const import CONF_BINARY_SENSORS, CONF_NAME from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import ( diff --git a/homeassistant/components/amcrest/sensor.py b/homeassistant/components/amcrest/sensor.py index b53f05273fa..be03b3bedff 100644 --- a/homeassistant/components/amcrest/sensor.py +++ b/homeassistant/components/amcrest/sensor.py @@ -15,12 +15,10 @@ _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(seconds=SENSOR_SCAN_INTERVAL_SECS) -SENSOR_MOTION_DETECTOR = "motion_detector" SENSOR_PTZ_PRESET = "ptz_preset" SENSOR_SDCARD = "sdcard" # Sensor types are defined like: Name, units, icon SENSORS = { - SENSOR_MOTION_DETECTOR: ["Motion Detected", None, "mdi:run"], SENSOR_PTZ_PRESET: ["PTZ Preset", None, "mdi:camera-iris"], SENSOR_SDCARD: ["SD Used", "%", "mdi:sd"], } @@ -94,11 +92,7 @@ class AmcrestSensor(Entity): _LOGGER.debug("Updating %s sensor", self._name) try: - if self._sensor_type == SENSOR_MOTION_DETECTOR: - self._state = self._api.is_motion_detected - self._attrs["Record Mode"] = self._api.record_mode - - elif self._sensor_type == SENSOR_PTZ_PRESET: + if self._sensor_type == SENSOR_PTZ_PRESET: self._state = self._api.ptz_presets_count elif self._sensor_type == SENSOR_SDCARD: diff --git a/homeassistant/components/amcrest/switch.py b/homeassistant/components/amcrest/switch.py deleted file mode 100644 index 0c3390c16f9..00000000000 --- a/homeassistant/components/amcrest/switch.py +++ /dev/null @@ -1,126 +0,0 @@ -"""Support for toggling Amcrest IP camera settings.""" -import logging - -from amcrest import AmcrestError - -from homeassistant.const import CONF_NAME, CONF_SWITCHES -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import ToggleEntity - -from .const import DATA_AMCREST, DEVICES, SERVICE_UPDATE -from .helpers import log_update_error, service_signal - -_LOGGER = logging.getLogger(__name__) - -MOTION_DETECTION = "motion_detection" -MOTION_RECORDING = "motion_recording" -# Switch types are defined like: Name, icon -SWITCHES = { - MOTION_DETECTION: ["Motion Detection", "mdi:run-fast"], - MOTION_RECORDING: ["Motion Recording", "mdi:record-rec"], -} - - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the IP Amcrest camera switch platform.""" - if discovery_info is None: - return - - name = discovery_info[CONF_NAME] - device = hass.data[DATA_AMCREST][DEVICES][name] - async_add_entities( - [ - AmcrestSwitch(name, device, setting) - for setting in discovery_info[CONF_SWITCHES] - ], - True, - ) - - -class AmcrestSwitch(ToggleEntity): - """Representation of an Amcrest IP camera switch.""" - - def __init__(self, name, device, setting): - """Initialize the Amcrest switch.""" - self._name = "{} {}".format(name, SWITCHES[setting][0]) - self._signal_name = name - self._api = device.api - self._setting = setting - self._state = False - self._icon = SWITCHES[setting][1] - self._unsub_dispatcher = None - - @property - def name(self): - """Return the name of the switch if any.""" - return self._name - - @property - def is_on(self): - """Return true if switch is on.""" - return self._state - - def turn_on(self, **kwargs): - """Turn setting on.""" - if not self.available: - return - try: - if self._setting == MOTION_DETECTION: - self._api.motion_detection = "true" - elif self._setting == MOTION_RECORDING: - self._api.motion_recording = "true" - except AmcrestError as error: - log_update_error(_LOGGER, "turn on", self.name, "switch", error) - - def turn_off(self, **kwargs): - """Turn setting off.""" - if not self.available: - return - try: - if self._setting == MOTION_DETECTION: - self._api.motion_detection = "false" - elif self._setting == MOTION_RECORDING: - self._api.motion_recording = "false" - except AmcrestError as error: - log_update_error(_LOGGER, "turn off", self.name, "switch", error) - - @property - def available(self): - """Return True if entity is available.""" - return self._api.available - - def update(self): - """Update setting state.""" - if not self.available: - return - _LOGGER.debug("Updating %s switch", self._name) - - try: - if self._setting == MOTION_DETECTION: - detection = self._api.is_motion_detector_on() - elif self._setting == MOTION_RECORDING: - detection = self._api.is_record_on_motion_detection() - self._state = detection - except AmcrestError as error: - log_update_error(_LOGGER, "update", self.name, "switch", error) - - @property - def icon(self): - """Return the icon for the switch.""" - return self._icon - - async def async_on_demand_update(self): - """Update state.""" - self.async_schedule_update_ha_state(True) - - async def async_added_to_hass(self): - """Subscribe to update signal.""" - self._unsub_dispatcher = async_dispatcher_connect( - self.hass, - service_signal(SERVICE_UPDATE, self._signal_name), - self.async_on_demand_update, - ) - - async def async_will_remove_from_hass(self): - """Disconnect from update signal.""" - self._unsub_dispatcher() diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index 8b68f089617..39e5bfb2cdf 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -3,8 +3,8 @@ "name": "Androidtv", "documentation": "https://www.home-assistant.io/integrations/androidtv", "requirements": [ - "adb-shell==0.0.8", - "androidtv==0.0.34", + "adb-shell==0.1.0", + "androidtv==0.0.36", "pure-python-adb==0.2.2.dev0" ], "dependencies": [], diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index b1cb86f7633..15acd594bee 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -2,7 +2,6 @@ import functools import logging import os -import voluptuous as vol from adb_shell.auth.keygen import keygen from adb_shell.exceptions import ( @@ -11,10 +10,11 @@ from adb_shell.exceptions import ( InvalidResponseError, TcpTimeoutException, ) -from androidtv import setup, ha_state_detection_rules_validator +from androidtv import ha_state_detection_rules_validator, setup from androidtv.constants import APPS, KEYS +import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, @@ -55,6 +55,7 @@ SUPPORT_ANDROIDTV = ( | SUPPORT_TURN_OFF | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK + | SUPPORT_SELECT_SOURCE | SUPPORT_STOP | SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_STEP @@ -145,7 +146,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): adb_log = f"using Python ADB implementation with adbkey='{adbkey}'" aftv = setup( - host, + config[CONF_HOST], + config[CONF_PORT], adbkey, device_class=config[CONF_DEVICE_CLASS], state_detection_rules=config[CONF_STATE_DETECTION_RULES], @@ -158,7 +160,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) aftv = setup( - host, + config[CONF_HOST], + config[CONF_PORT], config[CONF_ADBKEY], device_class=config[CONF_DEVICE_CLASS], state_detection_rules=config[CONF_STATE_DETECTION_RULES], @@ -170,7 +173,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): adb_log = f"using ADB server at {config[CONF_ADB_SERVER_IP]}:{config[CONF_ADB_SERVER_PORT]}" aftv = setup( - host, + config[CONF_HOST], + config[CONF_PORT], adb_server_ip=config[CONF_ADB_SERVER_IP], adb_server_port=config[CONF_ADB_SERVER_PORT], device_class=config[CONF_DEVICE_CLASS], @@ -199,6 +203,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): aftv, config[CONF_NAME], config[CONF_APPS], + config[CONF_GET_SOURCES], config.get(CONF_TURN_ON_COMMAND), config.get(CONF_TURN_OFF_COMMAND), ) @@ -287,7 +292,9 @@ def adb_decorator(override_available=False): class ADBDevice(MediaPlayerDevice): """Representation of an Android TV or Fire TV device.""" - def __init__(self, aftv, name, apps, turn_on_command, turn_off_command): + def __init__( + self, aftv, name, apps, get_sources, turn_on_command, turn_off_command + ): """Initialize the Android TV / Fire TV device.""" self.aftv = aftv self._name = name @@ -296,6 +303,7 @@ class ADBDevice(MediaPlayerDevice): self._app_name_to_id = { value: key for key, value in self._app_id_to_name.items() } + self._get_sources = get_sources self._keys = KEYS self._device_properties = self.aftv.device_properties @@ -325,6 +333,7 @@ class ADBDevice(MediaPlayerDevice): self._adb_response = None self._available = True self._current_app = None + self._sources = None self._state = None @property @@ -357,6 +366,16 @@ class ADBDevice(MediaPlayerDevice): """Device should be polled.""" return True + @property + def source(self): + """Return the current app.""" + return self._app_id_to_name.get(self._current_app, self._current_app) + + @property + def source_list(self): + """Return a list of running apps.""" + return self._sources + @property def state(self): """Return the state of the player.""" @@ -408,6 +427,20 @@ class ADBDevice(MediaPlayerDevice): """Send next track command (results in fast-forward).""" self.aftv.media_next_track() + @adb_decorator() + def select_source(self, source): + """Select input source. + + If the source starts with a '!', then it will close the app instead of + opening it. + """ + if isinstance(source, str): + if not source.startswith("!"): + self.aftv.launch_app(self._app_name_to_id.get(source, source)) + else: + source_ = source[1:].lstrip() + self.aftv.stop_app(self._app_name_to_id.get(source_, source_)) + @adb_decorator() def adb_command(self, cmd): """Send an ADB command to an Android TV / Fire TV device.""" @@ -436,11 +469,14 @@ class ADBDevice(MediaPlayerDevice): class AndroidTVDevice(ADBDevice): """Representation of an Android TV device.""" - def __init__(self, aftv, name, apps, turn_on_command, turn_off_command): + def __init__( + self, aftv, name, apps, get_sources, turn_on_command, turn_off_command + ): """Initialize the Android TV device.""" - super().__init__(aftv, name, apps, turn_on_command, turn_off_command) + super().__init__( + aftv, name, apps, get_sources, turn_on_command, turn_off_command + ) - self._device = None self._is_volume_muted = None self._volume_level = None @@ -465,25 +501,28 @@ class AndroidTVDevice(ADBDevice): ( state, self._current_app, - self._device, + running_apps, + _, self._is_volume_muted, self._volume_level, - ) = self.aftv.update() + ) = self.aftv.update(self._get_sources) self._state = ANDROIDTV_STATES.get(state) if self._state is None: self._available = False + if running_apps: + self._sources = [ + self._app_id_to_name.get(app_id, app_id) for app_id in running_apps + ] + else: + self._sources = None + @property def is_volume_muted(self): """Boolean if volume is currently muted.""" return self._is_volume_muted - @property - def source(self): - """Return the current playback device.""" - return self._device - @property def supported_features(self): """Flag media player features that are supported.""" @@ -518,15 +557,6 @@ class AndroidTVDevice(ADBDevice): class FireTVDevice(ADBDevice): """Representation of a Fire TV device.""" - def __init__( - self, aftv, name, apps, get_sources, turn_on_command, turn_off_command - ): - """Initialize the Fire TV device.""" - super().__init__(aftv, name, apps, turn_on_command, turn_off_command) - - self._get_sources = get_sources - self._sources = None - @adb_decorator(override_available=True) def update(self): """Update the device state and, if necessary, re-connect.""" @@ -558,16 +588,6 @@ class FireTVDevice(ADBDevice): else: self._sources = None - @property - def source(self): - """Return the current app.""" - return self._app_id_to_name.get(self._current_app, self._current_app) - - @property - def source_list(self): - """Return a list of running apps.""" - return self._sources - @property def supported_features(self): """Flag media player features that are supported.""" @@ -577,17 +597,3 @@ class FireTVDevice(ADBDevice): def media_stop(self): """Send stop (back) command.""" self.aftv.back() - - @adb_decorator() - def select_source(self, source): - """Select input source. - - If the source starts with a '!', then it will close the app instead of - opening it. - """ - if isinstance(source, str): - if not source.startswith("!"): - self.aftv.launch_app(self._app_name_to_id.get(source, source)) - else: - source_ = source[1:].lstrip() - self.aftv.stop_app(self._app_name_to_id.get(source_, source_)) diff --git a/homeassistant/components/anthemav/media_player.py b/homeassistant/components/anthemav/media_player.py index d472af6104e..f7b385d80a2 100644 --- a/homeassistant/components/anthemav/media_player.py +++ b/homeassistant/components/anthemav/media_player.py @@ -2,10 +2,9 @@ import logging import anthemav - import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, diff --git a/homeassistant/components/apache_kafka/__init__.py b/homeassistant/components/apache_kafka/__init__.py index e0c8b824913..7bd23630bd0 100644 --- a/homeassistant/components/apache_kafka/__init__.py +++ b/homeassistant/components/apache_kafka/__init__.py @@ -64,7 +64,7 @@ class DateTimeJSONEncoder(json.JSONEncoder): Additionally add encoding for datetime objects as isoformat. """ - def default(self, o): # pylint: disable=E0202 + def default(self, o): # pylint: disable=method-hidden """Implement encoding logic.""" if isinstance(o, datetime): return o.isoformat() diff --git a/homeassistant/components/apcupsd/binary_sensor.py b/homeassistant/components/apcupsd/binary_sensor.py index 62f0c90a447..29825fd695e 100644 --- a/homeassistant/components/apcupsd/binary_sensor.py +++ b/homeassistant/components/apcupsd/binary_sensor.py @@ -1,10 +1,10 @@ """Support for tracking the online status of a UPS.""" import voluptuous as vol -from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA +from homeassistant.components import apcupsd +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice from homeassistant.const import CONF_NAME import homeassistant.helpers.config_validation as cv -from homeassistant.components import apcupsd DEFAULT_NAME = "UPS Online Status" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( diff --git a/homeassistant/components/api/__init__.py b/homeassistant/components/api/__init__.py index d4faa55ed8c..fc2f01d418d 100644 --- a/homeassistant/components/api/__init__.py +++ b/homeassistant/components/api/__init__.py @@ -8,6 +8,7 @@ from aiohttp.web_exceptions import HTTPBadRequest import async_timeout import voluptuous as vol +from homeassistant.auth.permissions.const import POLICY_READ from homeassistant.bootstrap import DATA_LOGGING from homeassistant.components.http import HomeAssistantView from homeassistant.const import ( @@ -31,12 +32,11 @@ from homeassistant.const import ( __version__, ) import homeassistant.core as ha -from homeassistant.auth.permissions.const import POLICY_READ -from homeassistant.exceptions import TemplateError, Unauthorized, ServiceNotFound +from homeassistant.exceptions import ServiceNotFound, TemplateError, Unauthorized from homeassistant.helpers import template +from homeassistant.helpers.json import JSONEncoder from homeassistant.helpers.service import async_get_all_descriptions from homeassistant.helpers.state import AsyncTrackStates -from homeassistant.helpers.json import JSONEncoder _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/apns/const.py b/homeassistant/components/apns/const.py new file mode 100644 index 00000000000..a8dc1204aa1 --- /dev/null +++ b/homeassistant/components/apns/const.py @@ -0,0 +1,2 @@ +"""Constants for the apns component.""" +DOMAIN = "apns" diff --git a/homeassistant/components/apns/manifest.json b/homeassistant/components/apns/manifest.json index 4845c45a963..3c38238f7eb 100644 --- a/homeassistant/components/apns/manifest.json +++ b/homeassistant/components/apns/manifest.json @@ -2,9 +2,8 @@ "domain": "apns", "name": "Apns", "documentation": "https://www.home-assistant.io/integrations/apns", - "requirements": [ - "apns2==0.3.0" - ], + "requirements": ["apns2==0.3.0"], "dependencies": [], + "after_dependencies": ["device_tracker"], "codeowners": [] } diff --git a/homeassistant/components/apns/notify.py b/homeassistant/components/apns/notify.py index c24c9cc1605..990598508af 100644 --- a/homeassistant/components/apns/notify.py +++ b/homeassistant/components/apns/notify.py @@ -6,10 +6,10 @@ from apns2.errors import Unregistered from apns2.payload import Payload import voluptuous as vol +from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN from homeassistant.components.notify import ( ATTR_DATA, ATTR_TARGET, - DOMAIN, PLATFORM_SCHEMA, BaseNotificationService, ) @@ -19,12 +19,12 @@ from homeassistant.helpers import template as template_helper import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import track_state_change +from .const import DOMAIN + APNS_DEVICES = "apns.yaml" CONF_CERTFILE = "cert_file" CONF_TOPIC = "topic" CONF_SANDBOX = "sandbox" -DEVICE_TRACKER_DOMAIN = "device_tracker" -SERVICE_REGISTER = "apns_register" ATTR_PUSH_ID = "push_id" diff --git a/homeassistant/components/apple_tv/__init__.py b/homeassistant/components/apple_tv/__init__.py index 38d520f73da..e11b246fd5e 100644 --- a/homeassistant/components/apple_tv/__init__.py +++ b/homeassistant/components/apple_tv/__init__.py @@ -7,11 +7,11 @@ from pyatv import AppleTVDevice, connect_to_apple_tv, scan_for_apple_tvs from pyatv.exceptions import DeviceAuthenticationError import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.discovery import SERVICE_APPLE_TV from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_NAME from homeassistant.helpers import discovery from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/apprise/manifest.json b/homeassistant/components/apprise/manifest.json index 3e971a96e7e..09d840e796a 100644 --- a/homeassistant/components/apprise/manifest.json +++ b/homeassistant/components/apprise/manifest.json @@ -3,7 +3,7 @@ "name": "Apprise", "documentation": "https://www.home-assistant.io/components/apprise", "requirements": [ - "apprise==0.8.1" + "apprise==0.8.2" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/apprise/notify.py b/homeassistant/components/apprise/notify.py index 662cc9c1ab6..0c8c5b26eec 100644 --- a/homeassistant/components/apprise/notify.py +++ b/homeassistant/components/apprise/notify.py @@ -1,11 +1,8 @@ """Apprise platform for notify component.""" import logging -import voluptuous as vol - import apprise - -import homeassistant.helpers.config_validation as cv +import voluptuous as vol from homeassistant.components.notify import ( ATTR_TARGET, @@ -14,6 +11,7 @@ from homeassistant.components.notify import ( PLATFORM_SCHEMA, BaseNotificationService, ) +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/aprs/device_tracker.py b/homeassistant/components/aprs/device_tracker.py index 0d23cedb4ee..6258b470ebb 100644 --- a/homeassistant/components/aprs/device_tracker.py +++ b/homeassistant/components/aprs/device_tracker.py @@ -3,11 +3,9 @@ import logging import threading -import geopy.distance import aprslib -from aprslib import ConnectionError as AprsConnectionError -from aprslib import LoginError - +from aprslib import ConnectionError as AprsConnectionError, LoginError +import geopy.distance import voluptuous as vol from homeassistant.components.device_tracker import PLATFORM_SCHEMA diff --git a/homeassistant/components/aquostv/media_player.py b/homeassistant/components/aquostv/media_player.py index d8770592c9f..f71f41dc293 100644 --- a/homeassistant/components/aquostv/media_player.py +++ b/homeassistant/components/aquostv/media_player.py @@ -2,10 +2,9 @@ import logging import sharp_aquos_rc - import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, diff --git a/homeassistant/components/arcam_fmj/__init__.py b/homeassistant/components/arcam_fmj/__init__.py index bdb3bf67bbe..d818414753f 100644 --- a/homeassistant/components/arcam_fmj/__init__.py +++ b/homeassistant/components/arcam_fmj/__init__.py @@ -1,31 +1,32 @@ """Arcam component.""" -import logging import asyncio +import logging -import voluptuous as vol -import async_timeout -from arcam.fmj.client import Client from arcam.fmj import ConnectionFailed +from arcam.fmj.client import Client +import async_timeout +import voluptuous as vol from homeassistant import config_entries -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import HomeAssistantType, ConfigType from homeassistant.const import ( - EVENT_HOMEASSISTANT_STOP, CONF_HOST, CONF_NAME, CONF_PORT, CONF_SCAN_INTERVAL, CONF_ZONE, + EVENT_HOMEASSISTANT_STOP, SERVICE_TURN_ON, ) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType, HomeAssistantType + from .const import ( - DOMAIN, - DOMAIN_DATA_ENTRIES, - DOMAIN_DATA_CONFIG, DEFAULT_NAME, DEFAULT_PORT, DEFAULT_SCAN_INTERVAL, + DOMAIN, + DOMAIN_DATA_CONFIG, + DOMAIN_DATA_ENTRIES, SIGNAL_CLIENT_DATA, SIGNAL_CLIENT_STARTED, SIGNAL_CLIENT_STOPPED, diff --git a/homeassistant/components/arcam_fmj/media_player.py b/homeassistant/components/arcam_fmj/media_player.py index 231e9821dc6..8a54c745695 100644 --- a/homeassistant/components/arcam_fmj/media_player.py +++ b/homeassistant/components/arcam_fmj/media_player.py @@ -6,14 +6,13 @@ from arcam.fmj import DecodeMode2CH, DecodeModeMCH, IncomingAudioFormat, SourceC from arcam.fmj.state import State from homeassistant import config_entries -from homeassistant.core import callback from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, SUPPORT_SELECT_SOUND_MODE, SUPPORT_SELECT_SOURCE, - SUPPORT_TURN_ON, SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, @@ -25,15 +24,16 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) -from homeassistant.helpers.typing import HomeAssistantType, ConfigType +from homeassistant.core import callback from homeassistant.helpers.service import async_call_from_config +from homeassistant.helpers.typing import ConfigType, HomeAssistantType from .const import ( + DOMAIN, + DOMAIN_DATA_ENTRIES, SIGNAL_CLIENT_DATA, SIGNAL_CLIENT_STARTED, SIGNAL_CLIENT_STOPPED, - DOMAIN_DATA_ENTRIES, - DOMAIN, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/arduino/__init__.py b/homeassistant/components/arduino/__init__.py index f973ec136e3..61b03a3160d 100644 --- a/homeassistant/components/arduino/__init__.py +++ b/homeassistant/components/arduino/__init__.py @@ -1,13 +1,15 @@ """Support for Arduino boards running with the Firmata firmware.""" import logging +from PyMata.pymata import PyMata import serial import voluptuous as vol -from PyMata.pymata import PyMata - -from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP -from homeassistant.const import CONF_PORT +from homeassistant.const import ( + CONF_PORT, + EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP, +) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/arduino/sensor.py b/homeassistant/components/arduino/sensor.py index a92432537ca..c5863475512 100644 --- a/homeassistant/components/arduino/sensor.py +++ b/homeassistant/components/arduino/sensor.py @@ -3,11 +3,11 @@ import logging import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.components import arduino +from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_NAME -from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/arduino/switch.py b/homeassistant/components/arduino/switch.py index 63d83c8575e..5b5b161a24a 100644 --- a/homeassistant/components/arduino/switch.py +++ b/homeassistant/components/arduino/switch.py @@ -4,7 +4,7 @@ import logging import voluptuous as vol from homeassistant.components import arduino -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import CONF_NAME import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/arest/binary_sensor.py b/homeassistant/components/arest/binary_sensor.py index 669a28b7078..3bd0a85c6f0 100644 --- a/homeassistant/components/arest/binary_sensor.py +++ b/homeassistant/components/arest/binary_sensor.py @@ -1,18 +1,18 @@ """Support for an exposed aREST RESTful API of a device.""" -import logging from datetime import timedelta +import logging import requests import voluptuous as vol from homeassistant.components.binary_sensor import ( - BinarySensorDevice, - PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA, + PLATFORM_SCHEMA, + BinarySensorDevice, ) -from homeassistant.const import CONF_RESOURCE, CONF_PIN, CONF_NAME, CONF_DEVICE_CLASS -from homeassistant.util import Throttle +from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME, CONF_PIN, CONF_RESOURCE import homeassistant.helpers.config_validation as cv +from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -38,7 +38,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): response = requests.get(resource, timeout=10).json() except requests.exceptions.MissingSchema: _LOGGER.error( - "Missing resource or schema in configuration. " "Add http:// to your URL" + "Missing resource or schema in configuration. Add http:// to your URL" ) return False except requests.exceptions.ConnectionError: diff --git a/homeassistant/components/arest/sensor.py b/homeassistant/components/arest/sensor.py index 2416eeb0ebb..2533ce3619e 100644 --- a/homeassistant/components/arest/sensor.py +++ b/homeassistant/components/arest/sensor.py @@ -1,22 +1,22 @@ """Support for an exposed aREST RESTful API of a device.""" -import logging from datetime import timedelta +import logging import requests import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_UNIT_OF_MEASUREMENT, - CONF_VALUE_TEMPLATE, - CONF_RESOURCE, CONF_MONITORED_VARIABLES, CONF_NAME, + CONF_RESOURCE, + CONF_UNIT_OF_MEASUREMENT, + CONF_VALUE_TEMPLATE, ) from homeassistant.exceptions import TemplateError +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -59,7 +59,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): response = requests.get(resource, timeout=10).json() except requests.exceptions.MissingSchema: _LOGGER.error( - "Missing resource or schema in configuration. " "Add http:// to your URL" + "Missing resource or schema in configuration. Add http:// to your URL" ) return False except requests.exceptions.ConnectionError: diff --git a/homeassistant/components/arest/switch.py b/homeassistant/components/arest/switch.py index e1a7edacb7e..ccc2c5d8bf5 100644 --- a/homeassistant/components/arest/switch.py +++ b/homeassistant/components/arest/switch.py @@ -5,7 +5,7 @@ import logging import requests import voluptuous as vol -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import CONF_NAME, CONF_RESOURCE import homeassistant.helpers.config_validation as cv @@ -46,7 +46,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): response = requests.get(resource, timeout=10) except requests.exceptions.MissingSchema: _LOGGER.error( - "Missing resource or schema in configuration. " "Add http:// to your URL" + "Missing resource or schema in configuration. Add http:// to your URL" ) return False except requests.exceptions.ConnectionError: diff --git a/homeassistant/components/arlo/alarm_control_panel.py b/homeassistant/components/arlo/alarm_control_panel.py index a56b2a63372..838f319abc1 100644 --- a/homeassistant/components/arlo/alarm_control_panel.py +++ b/homeassistant/components/arlo/alarm_control_panel.py @@ -7,6 +7,11 @@ from homeassistant.components.alarm_control_panel import ( PLATFORM_SCHEMA, AlarmControlPanel, ) +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, +) from homeassistant.const import ( ATTR_ATTRIBUTION, STATE_ALARM_ARMED_AWAY, @@ -91,6 +96,11 @@ class ArloBaseStation(AlarmControlPanel): """Return the state of the device.""" return self._state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT + def update(self): """Update the state of the device.""" _LOGGER.debug("Updating Arlo Alarm Control Panel %s", self.name) diff --git a/homeassistant/components/arwn/sensor.py b/homeassistant/components/arwn/sensor.py index 23cd811a3e0..685e5d90f53 100644 --- a/homeassistant/components/arwn/sensor.py +++ b/homeassistant/components/arwn/sensor.py @@ -3,8 +3,8 @@ import json import logging from homeassistant.components import mqtt +from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import callback -from homeassistant.const import TEMP_FAHRENHEIT, TEMP_CELSIUS from homeassistant.helpers.entity import Entity from homeassistant.util import slugify diff --git a/homeassistant/components/asterisk_cdr/mailbox.py b/homeassistant/components/asterisk_cdr/mailbox.py index 4146ca9ddf9..0bae6ebf3ad 100644 --- a/homeassistant/components/asterisk_cdr/mailbox.py +++ b/homeassistant/components/asterisk_cdr/mailbox.py @@ -1,12 +1,14 @@ """Support for the Asterisk CDR interface.""" -import logging -import hashlib import datetime +import hashlib +import logging -from homeassistant.core import callback -from homeassistant.components.asterisk_mbox import SIGNAL_CDR_UPDATE -from homeassistant.components.asterisk_mbox import DOMAIN as ASTERISK_DOMAIN +from homeassistant.components.asterisk_mbox import ( + DOMAIN as ASTERISK_DOMAIN, + SIGNAL_CDR_UPDATE, +) from homeassistant.components.mailbox import Mailbox +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/asuswrt/__init__.py b/homeassistant/components/asuswrt/__init__.py index e0c6830adfe..64d2d7c7a4b 100644 --- a/homeassistant/components/asuswrt/__init__.py +++ b/homeassistant/components/asuswrt/__init__.py @@ -1,15 +1,16 @@ """Support for ASUSWRT devices.""" import logging +from aioasuswrt.asuswrt import AsusWrt import voluptuous as vol from homeassistant.const import ( CONF_HOST, - CONF_PASSWORD, - CONF_USERNAME, - CONF_PORT, CONF_MODE, + CONF_PASSWORD, + CONF_PORT, CONF_PROTOCOL, + CONF_USERNAME, ) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.discovery import async_load_platform @@ -53,7 +54,6 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass, config): """Set up the asuswrt component.""" - from aioasuswrt.asuswrt import AsusWrt conf = config[DOMAIN] diff --git a/homeassistant/components/aten_pe/__init__.py b/homeassistant/components/aten_pe/__init__.py new file mode 100644 index 00000000000..2a0fb277a48 --- /dev/null +++ b/homeassistant/components/aten_pe/__init__.py @@ -0,0 +1 @@ +"""The ATEN PE component.""" diff --git a/homeassistant/components/aten_pe/manifest.json b/homeassistant/components/aten_pe/manifest.json new file mode 100644 index 00000000000..4f6416dd76c --- /dev/null +++ b/homeassistant/components/aten_pe/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "aten_pe", + "name": "ATEN eco PDUs", + "documentation": "https://www.home-assistant.io/integrations/aten_pe", + "requirements": [ + "atenpdu==0.3.0" + ], + "dependencies": [], + "codeowners": [ + "@mtdcr" + ] +} diff --git a/homeassistant/components/aten_pe/switch.py b/homeassistant/components/aten_pe/switch.py new file mode 100644 index 00000000000..2ec6ec4b83d --- /dev/null +++ b/homeassistant/components/aten_pe/switch.py @@ -0,0 +1,122 @@ +"""The ATEN PE switch component.""" + +import logging + +from atenpdu import AtenPE, AtenPEError +import voluptuous as vol + +from homeassistant.components.switch import ( + DEVICE_CLASS_OUTLET, + PLATFORM_SCHEMA, + SwitchDevice, +) +from homeassistant.const import CONF_HOST, CONF_PORT, CONF_USERNAME +from homeassistant.exceptions import PlatformNotReady +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +CONF_AUTH_KEY = "auth_key" +CONF_COMMUNITY = "community" +CONF_PRIV_KEY = "priv_key" +DEFAULT_COMMUNITY = "private" +DEFAULT_PORT = "161" +DEFAULT_USERNAME = "administrator" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_COMMUNITY, default=DEFAULT_COMMUNITY): cv.string, + vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, + vol.Optional(CONF_AUTH_KEY): cv.string, + vol.Optional(CONF_PRIV_KEY): cv.string, + } +) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the ATEN PE switch.""" + node = config[CONF_HOST] + serv = config[CONF_PORT] + + dev = AtenPE( + node=node, + serv=serv, + community=config[CONF_COMMUNITY], + username=config[CONF_USERNAME], + authkey=config.get(CONF_AUTH_KEY), + privkey=config.get(CONF_PRIV_KEY), + ) + + try: + await hass.async_add_executor_job(dev.initialize) + mac = await dev.deviceMAC() + outlets = dev.outlets() + except AtenPEError as exc: + _LOGGER.error("Failed to initialize %s:%s: %s", node, serv, str(exc)) + raise PlatformNotReady + + switches = [] + async for outlet in outlets: + switches.append(AtenSwitch(dev, mac, outlet.id, outlet.name)) + + async_add_entities(switches) + + +class AtenSwitch(SwitchDevice): + """Represents an ATEN PE switch.""" + + def __init__(self, device, mac, outlet, name): + """Initialize an ATEN PE switch.""" + self._device = device + self._mac = mac + self._outlet = outlet + self._name = name or f"Outlet {outlet}" + self._enabled = False + self._outlet_power = 0.0 + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return f"{self._mac}-{self._outlet}" + + @property + def name(self) -> str: + """Return the name of the entity.""" + return self._name + + @property + def device_class(self) -> str: + """Return the class of this device, from component DEVICE_CLASSES.""" + return DEVICE_CLASS_OUTLET + + @property + def is_on(self) -> bool: + """Return True if entity is on.""" + return self._enabled + + @property + def current_power_w(self) -> float: + """Return the current power usage in W.""" + return self._outlet_power + + async def async_turn_on(self, **kwargs): + """Turn the switch on.""" + await self._device.setOutletStatus(self._outlet, "on") + self._enabled = True + + async def async_turn_off(self, **kwargs): + """Turn the switch off.""" + await self._device.setOutletStatus(self._outlet, "off") + self._enabled = False + + async def async_update(self): + """Process update from entity.""" + status = await self._device.displayOutletStatus(self._outlet) + if status == "on": + self._enabled = True + self._outlet_power = await self._device.outletPower(self._outlet) + elif status == "off": + self._enabled = False + self._outlet_power = 0.0 diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py index c98b634bb21..f9dd6b2dd61 100644 --- a/homeassistant/components/atome/sensor.py +++ b/homeassistant/components/atome/sensor.py @@ -1,23 +1,22 @@ """Linky Atome.""" -import logging from datetime import timedelta +import logging +from pyatome.client import AtomeClient, PyAtomeError import voluptuous as vol -from pyatome.client import AtomeClient -from pyatome.client import PyAtomeError +from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( + CONF_NAME, CONF_PASSWORD, CONF_USERNAME, - CONF_NAME, DEVICE_CLASS_POWER, - POWER_WATT, ENERGY_KILO_WATT_HOUR, + POWER_WATT, ) -from homeassistant.components.sensor import PLATFORM_SCHEMA +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index 468e6e429a7..8cbe41dac9e 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -254,7 +254,7 @@ class AugustData: ) except RequestException as ex: _LOGGER.error( - "Request error trying to retrieve doorbell" " status for %s. %s", + "Request error trying to retrieve doorbell status for %s. %s", doorbell.device_name, ex, ) @@ -301,7 +301,7 @@ class AugustData: ) except RequestException as ex: _LOGGER.error( - "Request error trying to retrieve door" " status for %s. %s", + "Request error trying to retrieve door status for %s. %s", lock.device_name, ex, ) @@ -327,7 +327,7 @@ class AugustData: ) except RequestException as ex: _LOGGER.error( - "Request error trying to retrieve door" " status for %s. %s", + "Request error trying to retrieve door status for %s. %s", lock.device_name, ex, ) @@ -342,7 +342,7 @@ class AugustData: ) except RequestException as ex: _LOGGER.error( - "Request error trying to retrieve door" " details for %s. %s", + "Request error trying to retrieve door details for %s. %s", lock.device_name, ex, ) diff --git a/homeassistant/components/aurora/binary_sensor.py b/homeassistant/components/aurora/binary_sensor.py index a69433c4186..454c3ad2405 100644 --- a/homeassistant/components/aurora/binary_sensor.py +++ b/homeassistant/components/aurora/binary_sensor.py @@ -7,13 +7,13 @@ import requests import voluptuous as vol from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice -from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION +from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) -ATTRIBUTION = "Data provided by the National Oceanic and Atmospheric " "Administration" +ATTRIBUTION = "Data provided by the National Oceanic and Atmospheric Administration" CONF_THRESHOLD = "forecast_threshold" DEFAULT_DEVICE_CLASS = "visible" diff --git a/homeassistant/components/aurora_abb_powerone/sensor.py b/homeassistant/components/aurora_abb_powerone/sensor.py index 05ed5fa99bf..a2645e5d7cb 100644 --- a/homeassistant/components/aurora_abb_powerone/sensor.py +++ b/homeassistant/components/aurora_abb_powerone/sensor.py @@ -2,8 +2,8 @@ import logging +from aurorapy.client import AuroraError, AuroraSerialClient import voluptuous as vol -from aurorapy.client import AuroraSerialClient, AuroraError from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( diff --git a/homeassistant/components/auth/.translations/da.json b/homeassistant/components/auth/.translations/da.json index f461f376d16..7877a813218 100644 --- a/homeassistant/components/auth/.translations/da.json +++ b/homeassistant/components/auth/.translations/da.json @@ -2,7 +2,7 @@ "mfa_setup": { "notify": { "abort": { - "no_available_service": "Ingen underretningstjenester til r\u00e5dighed." + "no_available_service": "Ingen meddelelsestjenester tilg\u00e6ngelige." }, "error": { "invalid_code": "Ugyldig kode, pr\u00f8v venligst igen." @@ -10,14 +10,14 @@ "step": { "init": { "description": "V\u00e6lg venligst en af meddelelsestjenesterne:", - "title": "Ops\u00e6t engangsadgangskode, der er leveret af besked komponenten" + "title": "Ops\u00e6t engangsadgangskoder leveret af notify-komponenten" }, "setup": { "description": "En engangsadgangskode er blevet sendt via **notify.{notify_service}**. Indtast den venligst nedenunder:", "title": "Bekr\u00e6ft ops\u00e6tningen" } }, - "title": "Advis\u00e9r engangskodeord" + "title": "Notify-engangsadgangskode" }, "totp": { "error": { @@ -25,8 +25,8 @@ }, "step": { "init": { - "description": "Hvis du vil aktivere tofaktorautentificering ved hj\u00e6lp af tidsbaserede engangskoder skal du scanne QR-koden med din autentificeringsapp. Hvis du ikke har en anbefaler vi enten [Google Authenticator] (https://support.google.com/accounts/answer/1066447) eller [Authy] (https://authy.com/). \n\n {qr_code} \n \nN\u00e5r du har scannet koden skal du indtaste den sekscifrede kode fra din app for at bekr\u00e6fte ops\u00e6tningen. Hvis du har problemer med at scanne QR-koden skal du lave en manuel ops\u00e6tning med kode **`{code}`**.", - "title": "Konfigurer to-faktors godkendelse ved hj\u00e6lp af TOTP" + "description": "Hvis du vil aktivere tofaktorgodkendelse ved hj\u00e6lp af tidsbaserede engangskoder skal du scanne QR-koden med din autentificeringsapp. Hvis du ikke har en anbefaler vi enten [Google Authenticator] (https://support.google.com/accounts/answer/1066447) eller [Authy] (https://authy.com/). \n\n {qr_code} \n \nN\u00e5r du har scannet koden skal du indtaste den sekscifrede kode fra din app for at bekr\u00e6fte ops\u00e6tningen. Hvis du har problemer med at scanne QR-koden skal du lave en manuel ops\u00e6tning med kode **`{code}`**.", + "title": "Konfigurer tofaktorgodkendelse ved hj\u00e6lp af TOTP" } }, "title": "TOTP" diff --git a/homeassistant/components/auth/.translations/nl.json b/homeassistant/components/auth/.translations/nl.json index 9ec8006507b..d61613097dd 100644 --- a/homeassistant/components/auth/.translations/nl.json +++ b/homeassistant/components/auth/.translations/nl.json @@ -9,7 +9,7 @@ }, "step": { "init": { - "description": "Selecteer een van de meldingsdiensten:", + "description": "Selecteer een van de meldingsservices:", "title": "Stel een \u00e9\u00e9nmalig wachtwoord in dat wordt afgegeven door een meldingscomponent" }, "setup": { diff --git a/homeassistant/components/auth/__init__.py b/homeassistant/components/auth/__init__.py index d0da9d39fe8..888ef98a582 100644 --- a/homeassistant/components/auth/__init__.py +++ b/homeassistant/components/auth/__init__.py @@ -114,31 +114,29 @@ Result will be a long-lived access token: } """ +from datetime import timedelta import logging import uuid -from datetime import timedelta from aiohttp import web import voluptuous as vol from homeassistant.auth.models import ( - User, - Credentials, TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN, + Credentials, + User, ) -from homeassistant.loader import bind_hass from homeassistant.components import websocket_api from homeassistant.components.http import KEY_REAL_IP from homeassistant.components.http.auth import async_sign_path from homeassistant.components.http.ban import log_invalid_auth from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.components.http.view import HomeAssistantView -from homeassistant.core import callback, HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.loader import bind_hass from homeassistant.util import dt as dt_util -from . import indieauth -from . import login_flow -from . import mfa_setup_flow +from . import indieauth, login_flow, mfa_setup_flow DOMAIN = "auth" WS_TYPE_CURRENT_USER = "auth/current_user" diff --git a/homeassistant/components/auth/indieauth.py b/homeassistant/components/auth/indieauth.py index 6a0a516bee2..3266ae65d7a 100644 --- a/homeassistant/components/auth/indieauth.py +++ b/homeassistant/components/auth/indieauth.py @@ -1,9 +1,9 @@ """Helpers to resolve client ID/secret.""" -import logging import asyncio -from ipaddress import ip_address from html.parser import HTMLParser -from urllib.parse import urlparse, urljoin +from ipaddress import ip_address +import logging +from urllib.parse import urljoin, urlparse import aiohttp @@ -30,6 +30,14 @@ async def verify_redirect_uri(hass, client_id, redirect_uri): if is_valid: return True + # Whitelist the iOS and Android callbacks so that people can link apps + # without being connected to the internet. + if redirect_uri == "homeassistant://auth-callback" and client_id in ( + "https://home-assistant.io/android", + "https://home-assistant.io/iOS", + ): + return True + # IndieAuth 4.2.2 allows for redirect_uri to be on different domain # but needs to be specified in link tag when fetching `client_id`. redirect_uris = await fetch_redirect_uris(hass, client_id) @@ -91,7 +99,7 @@ async def fetch_redirect_uris(hass, url): pass except aiohttp.client_exceptions.ClientConnectionError: _LOGGER.error( - ("Low level connection error while looking up " "redirect_uri %s"), url + "Low level connection error while looking up redirect_uri %s", url ) pass except aiohttp.client_exceptions.ClientError: diff --git a/homeassistant/components/auth/login_flow.py b/homeassistant/components/auth/login_flow.py index d6844396ce7..6f8d2751018 100644 --- a/homeassistant/components/auth/login_flow.py +++ b/homeassistant/components/auth/login_flow.py @@ -73,12 +73,13 @@ import voluptuous_serialize from homeassistant import data_entry_flow from homeassistant.components.http import KEY_REAL_IP from homeassistant.components.http.ban import ( - process_wrong_login, - process_success_login, log_invalid_auth, + process_success_login, + process_wrong_login, ) from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.components.http.view import HomeAssistantView + from . import indieauth diff --git a/homeassistant/components/auth/manifest.json b/homeassistant/components/auth/manifest.json index 1b0ab33f381..2f3e724b583 100644 --- a/homeassistant/components/auth/manifest.json +++ b/homeassistant/components/auth/manifest.json @@ -3,10 +3,7 @@ "name": "Auth", "documentation": "https://www.home-assistant.io/integrations/auth", "requirements": [], - "dependencies": [ - "http" - ], - "codeowners": [ - "@home-assistant/core" - ] + "dependencies": ["http"], + "after_dependencies": ["onboarding"], + "codeowners": ["@home-assistant/core"] } diff --git a/homeassistant/components/auth/mfa_setup_flow.py b/homeassistant/components/auth/mfa_setup_flow.py index 271e9ae1634..92926e2e7c5 100644 --- a/homeassistant/components/auth/mfa_setup_flow.py +++ b/homeassistant/components/auth/mfa_setup_flow.py @@ -6,7 +6,7 @@ import voluptuous_serialize from homeassistant import data_entry_flow from homeassistant.components import websocket_api -from homeassistant.core import callback, HomeAssistant +from homeassistant.core import HomeAssistant, callback WS_TYPE_SETUP_MFA = "auth/setup_mfa" SCHEMA_WS_SETUP_MFA = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( diff --git a/homeassistant/components/automatic/device_tracker.py b/homeassistant/components/automatic/device_tracker.py index fbb823dd329..3c9e33cdc84 100644 --- a/homeassistant/components/automatic/device_tracker.py +++ b/homeassistant/components/automatic/device_tracker.py @@ -5,9 +5,8 @@ import json import logging import os -from aiohttp import web import aioautomatic - +from aiohttp import web import voluptuous as vol from homeassistant.components.device_tracker import ( @@ -233,7 +232,7 @@ class AutomaticData: if event.created_at < self.vehicle_seen[event.vehicle.id]: # Skip events received out of order _LOGGER.debug( - "Skipping out of order event. Event Created %s. " "Last seen event: %s", + "Skipping out of order event. Event Created %s. Last seen event: %s", event.created_at, self.vehicle_seen[event.vehicle.id], ) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 3409ce832dd..671d7bd3d5b 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -24,15 +24,15 @@ from homeassistant.core import Context, CoreState, HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import condition, extract_domain_configs, script import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA +from homeassistant.helpers.config_validation import make_entity_service_schema from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.service import async_register_admin_service from homeassistant.helpers.typing import TemplateVarsType from homeassistant.loader import bind_hass from homeassistant.util.dt import parse_datetime, utcnow - # mypy: allow-untyped-calls, allow-untyped-defs # mypy: no-check-untyped-defs, no-warn-return-any @@ -106,7 +106,7 @@ PLATFORM_SCHEMA = vol.Schema( } ) -TRIGGER_SERVICE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( +TRIGGER_SERVICE_SCHEMA = make_entity_service_schema( {vol.Optional(ATTR_VARIABLES, default={}): dict} ) @@ -179,17 +179,27 @@ async def async_setup(hass, config): DOMAIN, SERVICE_TRIGGER, trigger_service_handler, schema=TRIGGER_SERVICE_SCHEMA ) - hass.services.async_register( - DOMAIN, SERVICE_RELOAD, reload_service_handler, schema=RELOAD_SERVICE_SCHEMA + async_register_admin_service( + hass, + DOMAIN, + SERVICE_RELOAD, + reload_service_handler, + schema=RELOAD_SERVICE_SCHEMA, ) hass.services.async_register( - DOMAIN, SERVICE_TOGGLE, toggle_service_handler, schema=ENTITY_SERVICE_SCHEMA + DOMAIN, + SERVICE_TOGGLE, + toggle_service_handler, + schema=make_entity_service_schema({}), ) for service in (SERVICE_TURN_ON, SERVICE_TURN_OFF): hass.services.async_register( - DOMAIN, service, turn_onoff_service_handler, schema=ENTITY_SERVICE_SCHEMA + DOMAIN, + service, + turn_onoff_service_handler, + schema=make_entity_service_schema({}), ) return True @@ -265,7 +275,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): else: enable_automation = DEFAULT_INITIAL_STATE _LOGGER.debug( - "Automation %s not in state storage, state %s from " "default is used.", + "Automation %s not in state storage, state %s from default is used.", self.entity_id, enable_automation, ) diff --git a/homeassistant/components/automation/config.py b/homeassistant/components/automation/config.py index 5733cd2e83e..d11472a2128 100644 --- a/homeassistant/components/automation/config.py +++ b/homeassistant/components/automation/config.py @@ -7,8 +7,8 @@ import voluptuous as vol from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, ) -from homeassistant.const import CONF_PLATFORM from homeassistant.config import async_log_exception, config_without_domain +from homeassistant.const import CONF_PLATFORM from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import condition, config_per_platform, script from homeassistant.loader import IntegrationNotFound diff --git a/homeassistant/components/automation/device.py b/homeassistant/components/automation/device.py index ced8f65cbf5..b2892d1abaa 100644 --- a/homeassistant/components/automation/device.py +++ b/homeassistant/components/automation/device.py @@ -7,7 +7,6 @@ from homeassistant.components.device_automation import ( ) from homeassistant.const import CONF_DOMAIN - # mypy: allow-untyped-defs, no-check-untyped-defs TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/automation/event.py b/homeassistant/components/automation/event.py index 26dacac974d..9fc78746a7c 100644 --- a/homeassistant/components/automation/event.py +++ b/homeassistant/components/automation/event.py @@ -3,11 +3,10 @@ import logging import voluptuous as vol -from homeassistant.core import callback from homeassistant.const import CONF_PLATFORM +from homeassistant.core import callback from homeassistant.helpers import config_validation as cv - # mypy: allow-untyped-defs CONF_EVENT_TYPE = "event_type" diff --git a/homeassistant/components/automation/geo_location.py b/homeassistant/components/automation/geo_location.py index 0ef0884d329..5dc4f3c80f6 100644 --- a/homeassistant/components/automation/geo_location.py +++ b/homeassistant/components/automation/geo_location.py @@ -2,7 +2,6 @@ import voluptuous as vol from homeassistant.components.geo_location import DOMAIN -from homeassistant.core import callback from homeassistant.const import ( CONF_EVENT, CONF_PLATFORM, @@ -10,10 +9,10 @@ from homeassistant.const import ( CONF_ZONE, EVENT_STATE_CHANGED, ) +from homeassistant.core import callback from homeassistant.helpers import condition, config_validation as cv from homeassistant.helpers.config_validation import entity_domain - # mypy: allow-untyped-defs, no-check-untyped-defs EVENT_ENTER = "enter" diff --git a/homeassistant/components/automation/homeassistant.py b/homeassistant/components/automation/homeassistant.py index e4eb029d5aa..743b169c86c 100644 --- a/homeassistant/components/automation/homeassistant.py +++ b/homeassistant/components/automation/homeassistant.py @@ -3,9 +3,8 @@ import logging import voluptuous as vol -from homeassistant.core import callback, CoreState -from homeassistant.const import CONF_PLATFORM, CONF_EVENT, EVENT_HOMEASSISTANT_STOP - +from homeassistant.const import CONF_EVENT, CONF_PLATFORM, EVENT_HOMEASSISTANT_STOP +from homeassistant.core import CoreState, callback # mypy: allow-untyped-defs diff --git a/homeassistant/components/automation/litejet.py b/homeassistant/components/automation/litejet.py index 9512db8261d..466fc941a9a 100644 --- a/homeassistant/components/automation/litejet.py +++ b/homeassistant/components/automation/litejet.py @@ -3,12 +3,11 @@ import logging import voluptuous as vol -from homeassistant.core import callback from homeassistant.const import CONF_PLATFORM +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt_util 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 diff --git a/homeassistant/components/automation/mqtt.py b/homeassistant/components/automation/mqtt.py index 135a421f72e..fb0073c78d5 100644 --- a/homeassistant/components/automation/mqtt.py +++ b/homeassistant/components/automation/mqtt.py @@ -3,12 +3,11 @@ import json import voluptuous as vol -from homeassistant.core import callback from homeassistant.components import mqtt -from homeassistant.const import CONF_PLATFORM, CONF_PAYLOAD +from homeassistant.const import CONF_PAYLOAD, CONF_PLATFORM +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv - # mypy: allow-untyped-defs CONF_ENCODING = "encoding" diff --git a/homeassistant/components/automation/numeric_state.py b/homeassistant/components/automation/numeric_state.py index 0c8ab3d9c8b..e944b66751b 100644 --- a/homeassistant/components/automation/numeric_state.py +++ b/homeassistant/components/automation/numeric_state.py @@ -4,18 +4,17 @@ import logging import voluptuous as vol from homeassistant import exceptions -from homeassistant.core import CALLBACK_TYPE, callback from homeassistant.const import ( - CONF_VALUE_TEMPLATE, - CONF_PLATFORM, - CONF_ENTITY_ID, - CONF_BELOW, CONF_ABOVE, + CONF_BELOW, + CONF_ENTITY_ID, CONF_FOR, + CONF_PLATFORM, + CONF_VALUE_TEMPLATE, ) -from homeassistant.helpers.event import async_track_state_change, async_track_same_state +from homeassistant.core import CALLBACK_TYPE, callback from homeassistant.helpers import condition, config_validation as cv, template - +from homeassistant.helpers.event import async_track_same_state, async_track_state_change # mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs # mypy: no-check-untyped-defs diff --git a/homeassistant/components/automation/reproduce_state.py b/homeassistant/components/automation/reproduce_state.py index 553d6871087..4cfe519d585 100644 --- a/homeassistant/components/automation/reproduce_state.py +++ b/homeassistant/components/automation/reproduce_state.py @@ -5,10 +5,10 @@ from typing import Iterable, Optional from homeassistant.const import ( ATTR_ENTITY_ID, - STATE_ON, - STATE_OFF, SERVICE_TURN_OFF, SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, ) from homeassistant.core import Context, State from homeassistant.helpers.typing import HomeAssistantType diff --git a/homeassistant/components/automation/state.py b/homeassistant/components/automation/state.py index 47c44587b08..fc3fff47514 100644 --- a/homeassistant/components/automation/state.py +++ b/homeassistant/components/automation/state.py @@ -6,11 +6,10 @@ from typing import Dict import voluptuous as vol from homeassistant import exceptions -from homeassistant.core import HomeAssistant, CALLBACK_TYPE, callback -from homeassistant.const import MATCH_ALL, CONF_PLATFORM, CONF_FOR +from homeassistant.const import CONF_FOR, CONF_PLATFORM, MATCH_ALL +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers import config_validation as cv, template -from homeassistant.helpers.event import async_track_state_change, async_track_same_state - +from homeassistant.helpers.event import async_track_same_state, async_track_state_change # mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs # mypy: no-check-untyped-defs diff --git a/homeassistant/components/automation/sun.py b/homeassistant/components/automation/sun.py index 66892784a54..c416742f397 100644 --- a/homeassistant/components/automation/sun.py +++ b/homeassistant/components/automation/sun.py @@ -4,16 +4,15 @@ import logging import voluptuous as vol -from homeassistant.core import callback from homeassistant.const import ( CONF_EVENT, CONF_OFFSET, CONF_PLATFORM, SUN_EVENT_SUNRISE, ) -from homeassistant.helpers.event import async_track_sunrise, async_track_sunset +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv - +from homeassistant.helpers.event import async_track_sunrise, async_track_sunset # mypy: allow-untyped-defs, no-check-untyped-defs diff --git a/homeassistant/components/automation/template.py b/homeassistant/components/automation/template.py index 95b6b857c9d..ee4484410cd 100644 --- a/homeassistant/components/automation/template.py +++ b/homeassistant/components/automation/template.py @@ -3,13 +3,11 @@ import logging import voluptuous as vol -from homeassistant.core import callback -from homeassistant.const import CONF_VALUE_TEMPLATE, CONF_PLATFORM, CONF_FOR from homeassistant import exceptions -from homeassistant.helpers import condition +from homeassistant.const import CONF_FOR, CONF_PLATFORM, CONF_VALUE_TEMPLATE +from homeassistant.core import callback +from homeassistant.helpers import condition, config_validation as cv, template from homeassistant.helpers.event import async_track_same_state, async_track_template -from homeassistant.helpers import config_validation as cv, template - # mypy: allow-untyped-defs, no-check-untyped-defs diff --git a/homeassistant/components/automation/time.py b/homeassistant/components/automation/time.py index 231bc346e14..5f461952960 100644 --- a/homeassistant/components/automation/time.py +++ b/homeassistant/components/automation/time.py @@ -3,12 +3,11 @@ import logging import voluptuous as vol -from homeassistant.core import callback from homeassistant.const import CONF_AT, CONF_PLATFORM +from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.event import async_track_time_change - # mypy: allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/automation/time_pattern.py b/homeassistant/components/automation/time_pattern.py index ee092916112..65d44f5b1ca 100644 --- a/homeassistant/components/automation/time_pattern.py +++ b/homeassistant/components/automation/time_pattern.py @@ -3,12 +3,11 @@ import logging import voluptuous as vol -from homeassistant.core import callback from homeassistant.const import CONF_PLATFORM +from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.event import async_track_time_change - # mypy: allow-untyped-defs, no-check-untyped-defs CONF_HOURS = "hours" diff --git a/homeassistant/components/automation/webhook.py b/homeassistant/components/automation/webhook.py index bbcf9bd9ddc..5d01c6454a8 100644 --- a/homeassistant/components/automation/webhook.py +++ b/homeassistant/components/automation/webhook.py @@ -5,13 +5,12 @@ import logging from aiohttp import hdrs import voluptuous as vol -from homeassistant.core import callback from homeassistant.const import CONF_PLATFORM, CONF_WEBHOOK_ID +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from . import DOMAIN as AUTOMATION_DOMAIN - # mypy: allow-untyped-defs DEPENDENCIES = ("webhook",) diff --git a/homeassistant/components/automation/zone.py b/homeassistant/components/automation/zone.py index 535ef298a2a..3dba1a4df35 100644 --- a/homeassistant/components/automation/zone.py +++ b/homeassistant/components/automation/zone.py @@ -1,17 +1,16 @@ """Offer zone automation rules.""" import voluptuous as vol -from homeassistant.core import callback from homeassistant.const import ( - CONF_EVENT, CONF_ENTITY_ID, + CONF_EVENT, + CONF_PLATFORM, CONF_ZONE, MATCH_ALL, - CONF_PLATFORM, ) -from homeassistant.helpers.event import async_track_state_change +from homeassistant.core import callback from homeassistant.helpers import condition, config_validation as cv, location - +from homeassistant.helpers.event import async_track_state_change # mypy: allow-untyped-defs, no-check-untyped-defs diff --git a/homeassistant/components/avea/light.py b/homeassistant/components/avea/light.py index e6ceedcf96d..92d66a554da 100644 --- a/homeassistant/components/avea/light.py +++ b/homeassistant/components/avea/light.py @@ -1,5 +1,6 @@ """Support for the Elgato Avea lights.""" import logging + import avea from homeassistant.components.light import ( @@ -12,7 +13,6 @@ from homeassistant.components.light import ( from homeassistant.exceptions import PlatformNotReady import homeassistant.util.color as color_util - _LOGGER = logging.getLogger(__name__) SUPPORT_AVEA = SUPPORT_BRIGHTNESS | SUPPORT_COLOR diff --git a/homeassistant/components/aws/__init__.py b/homeassistant/components/aws/__init__.py index b553b7eafd6..600874b0d25 100644 --- a/homeassistant/components/aws/__init__.py +++ b/homeassistant/components/aws/__init__.py @@ -1,10 +1,9 @@ """Support for Amazon Web Services (AWS).""" import asyncio -import logging from collections import OrderedDict +import logging import aiobotocore - import voluptuous as vol from homeassistant import config_entries diff --git a/homeassistant/components/aws/notify.py b/homeassistant/components/aws/notify.py index 2afa9a3a402..13fa189a318 100644 --- a/homeassistant/components/aws/notify.py +++ b/homeassistant/components/aws/notify.py @@ -12,8 +12,9 @@ from homeassistant.components.notify import ( ATTR_TITLE_DEFAULT, BaseNotificationService, ) -from homeassistant.const import CONF_PLATFORM, CONF_NAME +from homeassistant.const import CONF_NAME, CONF_PLATFORM from homeassistant.helpers.json import JSONEncoder + from .const import ( CONF_CONTEXT, CONF_CREDENTIAL_NAME, diff --git a/homeassistant/components/axis/.translations/da.json b/homeassistant/components/axis/.translations/da.json index c169f85f280..21f33d120f7 100644 --- a/homeassistant/components/axis/.translations/da.json +++ b/homeassistant/components/axis/.translations/da.json @@ -8,11 +8,11 @@ }, "error": { "already_configured": "Enheden er allerede konfigureret", - "already_in_progress": "Enheds konfiguration er allerede i gang.", + "already_in_progress": "Enhedskonfiguration er allerede i gang.", "device_unavailable": "Enheden er ikke tilg\u00e6ngelig", "faulty_credentials": "Ugyldige legitimationsoplysninger" }, - "flow_title": "Axis enhed: {name} ({host})", + "flow_title": "Axis-enhed: {name} ({host})", "step": { "user": { "data": { @@ -21,9 +21,9 @@ "port": "Port", "username": "Brugernavn" }, - "title": "Konfigurer Axis enhed" + "title": "Indstil Axis-enhed" } }, - "title": "Axis enhed" + "title": "Axis-enhed" } } \ No newline at end of file diff --git a/homeassistant/components/axis/.translations/ru.json b/homeassistant/components/axis/.translations/ru.json index 0345862b865..24990bb0f1a 100644 --- a/homeassistant/components/axis/.translations/ru.json +++ b/homeassistant/components/axis/.translations/ru.json @@ -10,7 +10,7 @@ "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u043d\u0430\u0447\u0430\u0442\u0430.", "device_unavailable": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e.", - "faulty_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435." + "faulty_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435." }, "flow_title": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Axis {name} ({host})", "step": { diff --git a/homeassistant/components/axis/device.py b/homeassistant/components/axis/device.py index e42a758f3c4..b05c5b2fed0 100644 --- a/homeassistant/components/axis/device.py +++ b/homeassistant/components/axis/device.py @@ -1,8 +1,8 @@ """Axis network device abstraction.""" import asyncio -import async_timeout +import async_timeout import axis from axis.streammanager import SIGNAL_PLAYING @@ -21,7 +21,6 @@ from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import CONF_CAMERA, CONF_EVENTS, CONF_MODEL, DOMAIN, LOGGER - from .errors import AuthenticationRequired, CannotConnect diff --git a/homeassistant/components/azure_event_hub/__init__.py b/homeassistant/components/azure_event_hub/__init__.py index 371b8d1ea8d..7e141cd8060 100644 --- a/homeassistant/components/azure_event_hub/__init__.py +++ b/homeassistant/components/azure_event_hub/__init__.py @@ -3,8 +3,8 @@ import json import logging from typing import Any, Dict -import voluptuous as vol from azure.eventhub import EventData, EventHubClientAsync +import voluptuous as vol from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, diff --git a/homeassistant/components/bayesian/binary_sensor.py b/homeassistant/components/bayesian/binary_sensor.py index ffa13a6288c..1d3720f6723 100644 --- a/homeassistant/components/bayesian/binary_sensor.py +++ b/homeassistant/components/bayesian/binary_sensor.py @@ -133,7 +133,7 @@ class BayesianBinarySensor(BinarySensorDevice): to_observe.update(set([obs.get("entity_id")])) if "value_template" in obs: to_observe.update(set(obs.get(CONF_VALUE_TEMPLATE).extract_entities())) - self.entity_obs = dict.fromkeys(to_observe, []) + self.entity_obs = {key: [] for key in to_observe} for ind, obs in enumerate(self._observations): obs["id"] = ind diff --git a/homeassistant/components/bbb_gpio/binary_sensor.py b/homeassistant/components/bbb_gpio/binary_sensor.py index 105015da720..3ef13c117a2 100644 --- a/homeassistant/components/bbb_gpio/binary_sensor.py +++ b/homeassistant/components/bbb_gpio/binary_sensor.py @@ -4,8 +4,8 @@ import logging import voluptuous as vol from homeassistant.components import bbb_gpio -from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA -from homeassistant.const import DEVICE_DEFAULT_NAME, CONF_NAME +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice +from homeassistant.const import CONF_NAME, DEVICE_DEFAULT_NAME import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/bbb_gpio/switch.py b/homeassistant/components/bbb_gpio/switch.py index 45f95609758..eb75c6f374c 100644 --- a/homeassistant/components/bbb_gpio/switch.py +++ b/homeassistant/components/bbb_gpio/switch.py @@ -3,11 +3,11 @@ import logging import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA from homeassistant.components import bbb_gpio -from homeassistant.const import DEVICE_DEFAULT_NAME, CONF_NAME -from homeassistant.helpers.entity import ToggleEntity +from homeassistant.components.switch import PLATFORM_SCHEMA +from homeassistant.const import CONF_NAME, DEVICE_DEFAULT_NAME import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import ToggleEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/bbox/device_tracker.py b/homeassistant/components/bbox/device_tracker.py index 122016ecf96..8097c11eb89 100644 --- a/homeassistant/components/bbox/device_tracker.py +++ b/homeassistant/components/bbox/device_tracker.py @@ -5,7 +5,6 @@ import logging from typing import List import pybbox - import voluptuous as vol from homeassistant.components.device_tracker import ( diff --git a/homeassistant/components/bbox/sensor.py b/homeassistant/components/bbox/sensor.py index ad6bcc39796..f5e5865f6f0 100644 --- a/homeassistant/components/bbox/sensor.py +++ b/homeassistant/components/bbox/sensor.py @@ -1,17 +1,22 @@ """Support for Bbox Bouygues Modem Router.""" -import logging from datetime import timedelta +import logging -import requests import pybbox - +import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, CONF_MONITORED_VARIABLES, ATTR_ATTRIBUTION +from homeassistant.const import ( + ATTR_ATTRIBUTION, + CONF_MONITORED_VARIABLES, + CONF_NAME, + 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 _LOGGER = logging.getLogger(__name__) @@ -45,6 +50,8 @@ SENSOR_TYPES = { BANDWIDTH_MEGABITS_SECONDS, "mdi:upload", ], + "uptime": ["Uptime", None, "mdi:clock"], + "number_of_reboots": ["Number of reboot", None, "mdi:restart"], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -72,11 +79,61 @@ def setup_platform(hass, config, add_entities, discovery_info=None): sensors = [] for variable in config[CONF_MONITORED_VARIABLES]: - sensors.append(BboxSensor(bbox_data, variable, name)) + if variable == "uptime": + sensors.append(BboxUptimeSensor(bbox_data, variable, name)) + else: + sensors.append(BboxSensor(bbox_data, variable, name)) add_entities(sensors, True) +class BboxUptimeSensor(Entity): + """Bbox uptime sensor.""" + + def __init__(self, bbox_data, sensor_type, name): + """Initialize the sensor.""" + self.client_name = name + self.type = sensor_type + self._name = SENSOR_TYPES[sensor_type][0] + self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] + self._icon = SENSOR_TYPES[sensor_type][2] + self.bbox_data = bbox_data + self._state = None + + @property + def name(self): + """Return the name of the sensor.""" + return f"{self.client_name} {self._name}" + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return self._icon + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return {ATTR_ATTRIBUTION: ATTRIBUTION} + + @property + def device_class(self): + """Return the class of this sensor.""" + return DEVICE_CLASS_TIMESTAMP + + def update(self): + """Get the latest data from Bbox and update the state.""" + self.bbox_data.update() + uptime = utcnow() - timedelta( + seconds=self.bbox_data.router_infos["device"]["uptime"] + ) + self._state = uptime.replace(microsecond=0).isoformat() + + class BboxSensor(Entity): """Implementation of a Bbox sensor.""" @@ -126,6 +183,8 @@ class BboxSensor(Entity): self._state = round(self.bbox_data.data["rx"]["bandwidth"] / 1000, 2) elif self.type == "current_up_bandwidth": self._state = round(self.bbox_data.data["tx"]["bandwidth"] / 1000, 2) + elif self.type == "number_of_reboots": + self._state = self.bbox_data.router_infos["device"]["numberofboots"] class BboxData: @@ -134,6 +193,7 @@ class BboxData: def __init__(self): """Initialize the data object.""" self.data = None + self.router_infos = None @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): @@ -142,7 +202,9 @@ class BboxData: try: box = pybbox.Bbox() self.data = box.get_ip_stats() + self.router_infos = box.get_bbox_info() except requests.exceptions.HTTPError as error: _LOGGER.error(error) self.data = None + self.router_infos = None return False diff --git a/homeassistant/components/beewi_smartclim/sensor.py b/homeassistant/components/beewi_smartclim/sensor.py index 7bfa8883013..be1697e4f88 100644 --- a/homeassistant/components/beewi_smartclim/sensor.py +++ b/homeassistant/components/beewi_smartclim/sensor.py @@ -5,15 +5,15 @@ from beewi_smartclim import BeewiSmartClimPoller import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( - CONF_NAME, CONF_MAC, - TEMP_CELSIUS, + CONF_NAME, + DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_BATTERY, + TEMP_CELSIUS, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/bh1750/sensor.py b/homeassistant/components/bh1750/sensor.py index cc91fa48bae..924bfcd5507 100644 --- a/homeassistant/components/bh1750/sensor.py +++ b/homeassistant/components/bh1750/sensor.py @@ -2,14 +2,13 @@ from functools import partial import logging -import smbus # pylint: disable=import-error from i2csense.bh1750 import BH1750 # pylint: disable=import-error - +import smbus # pylint: disable=import-error import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_NAME, DEVICE_CLASS_ILLUMINANCE +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/binary_sensor/.translations/da.json b/homeassistant/components/binary_sensor/.translations/da.json index f7bd834561c..19229c16cb3 100644 --- a/homeassistant/components/binary_sensor/.translations/da.json +++ b/homeassistant/components/binary_sensor/.translations/da.json @@ -1,6 +1,7 @@ { "device_automation": { "condition_type": { + "is_bat_low": "{entity_name} batteri er lavt", "is_cold": "{entity_name} er kold", "is_connected": "{entity_name} er tilsluttet", "is_gas": "{entity_name} registrerer gas", @@ -17,6 +18,7 @@ "is_no_smoke": "{entity_name} registrerer ikke r\u00f8g", "is_no_sound": "{entity_name} registrerer ikke lyd", "is_no_vibration": "{entity_name} registrerer ikke vibration", + "is_not_bat_low": "{entity_name} batteri er normalt", "is_not_cold": "{entity_name} er ikke kold", "is_not_connected": "{entity_name} er afbrudt", "is_not_hot": "{entity_name} er ikke varm", @@ -25,10 +27,17 @@ "is_not_moving": "{entity_name} bev\u00e6ger sig ikke", "is_not_occupied": "{entity_name} er ikke optaget", "is_not_open": "{entity_name} er lukket", + "is_not_plugged_in": "{entity_name} er ikke tilsluttet str\u00f8m", + "is_not_powered": "{entity_name} er ikke tilsluttet str\u00f8m", "is_not_present": "{entity_name} er ikke til stede", "is_not_unsafe": "{entity_name} er sikker", "is_occupied": "{entity_name} er optaget", + "is_off": "{entity_name} er sl\u00e5et fra", + "is_on": "{entity_name} er sl\u00e5et til", "is_open": "{entity_name} er \u00e5ben", + "is_plugged_in": "{entity_name} er tilsluttet str\u00f8m", + "is_powered": "{entity_name} er tilsluttet str\u00f8m", + "is_present": "{entity_name} er til stede", "is_problem": "{entity_name} registrerer problem", "is_smoke": "{entity_name} registrerer r\u00f8g", "is_sound": "{entity_name} registrerer lyd", @@ -36,9 +45,14 @@ "is_vibration": "{entity_name} registrerer vibration" }, "trigger_type": { + "bat_low": "{entity_name} lavt batteriniveau", "closed": "{entity_name} lukket", "cold": "{entity_name} blev kold", "connected": "{entity_name} tilsluttet", + "gas": "{entity_name} begyndte at registrere gas", + "hot": "{entity_name} blev varm", + "light": "{entity_name} begyndte at registrere lys", + "locked": "{entity_name} l\u00e5st", "moist": "{entity_name} blev fugtig", "moist\u00a7": "{entity_name} blev fugtig", "motion": "{entity_name} begyndte at registrere bev\u00e6gelse", @@ -50,18 +64,31 @@ "no_smoke": "{entity_name} stoppede med at registrere r\u00f8g", "no_sound": "{entity_name} stoppede med at registrere lyd", "no_vibration": "{entity_name} stoppede med at registrere vibration", + "not_bat_low": "{entity_name} batteri normalt", + "not_cold": "{entity_name} blev ikke kold", "not_connected": "{entity_name} afbrudt", "not_hot": "{entity_name} blev ikke varm", "not_locked": "{entity_name} l\u00e5st op", "not_moist": "{entity_name} blev t\u00f8r", + "not_moving": "{entity_name} stoppede med at bev\u00e6ge sig", + "not_occupied": "{entity_name} blev ikke optaget", "not_opened": "{entity_name} lukket", + "not_plugged_in": "{entity_name} ikke tilsluttet str\u00f8m", + "not_powered": "{entity_name} ikke tilsluttet str\u00f8m", "not_present": "{entity_name} ikke til stede", "not_unsafe": "{entity_name} blev sikker", "occupied": "{entity_name} blev optaget", + "opened": "{entity_name} \u00e5bnet", + "plugged_in": "{entity_name} tilsluttet str\u00f8m", + "powered": "{entity_name} tilsluttet str\u00f8m", "present": "{entity_name} til stede", "problem": "{entity_name} begyndte at registrere problem", "smoke": "{entity_name} begyndte at registrere r\u00f8g", - "sound": "{entity_name} begyndte at registrere lyd" + "sound": "{entity_name} begyndte at registrere lyd", + "turned_off": "{entity_name} slukkede", + "turned_on": "{entity_name} t\u00e6ndte", + "unsafe": "{entity_name} blev usikker", + "vibration": "{entity_name} begyndte at registrere vibration" } } } \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/es.json b/homeassistant/components/binary_sensor/.translations/es.json index 756a370ca3c..9720fb974f6 100644 --- a/homeassistant/components/binary_sensor/.translations/es.json +++ b/homeassistant/components/binary_sensor/.translations/es.json @@ -72,14 +72,14 @@ "not_moist": "{entity_name} se sec\u00f3", "not_moving": "{entity_name} dej\u00f3 de moverse", "not_occupied": "{entity_name} no est\u00e1 ocupado", - "not_opened": "{nombre_de_la_entidad} cerrado", + "not_opened": "{entity_name} cerrado", "not_plugged_in": "{entity_name} desconectado", "not_powered": "{entity_name} no est\u00e1 activado", "not_present": "{entity_name} no est\u00e1 presente", "not_unsafe": "{entity_name} se volvi\u00f3 seguro", "occupied": "{entity_name} se convirti\u00f3 en ocupado", "opened": "{entity_name} abierto", - "plugged_in": "{nombre_de_la_entidad} conectado", + "plugged_in": "{entity_name} conectado", "powered": "{entity_name} alimentado", "present": "{entity_name} presente", "problem": "{entity_name} empez\u00f3 a detectar problemas", diff --git a/homeassistant/components/binary_sensor/.translations/ko.json b/homeassistant/components/binary_sensor/.translations/ko.json index 167708c2cf1..4c1cba2bec5 100644 --- a/homeassistant/components/binary_sensor/.translations/ko.json +++ b/homeassistant/components/binary_sensor/.translations/ko.json @@ -1,94 +1,94 @@ { "device_automation": { "condition_type": { - "is_bat_low": "{entity_name} \uc758 \ubc30\ud130\ub9ac \uc794\ub7c9\uc774 \ubd80\uc871\ud569\ub2c8\ub2e4", - "is_cold": "{entity_name} \uc774(\uac00) \ucc28\uac11\uc2b5\ub2c8\ub2e4", - "is_connected": "{entity_name} \uc774(\uac00) \uc5f0\uacb0\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "is_gas": "{entity_name} \uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0\ud588\uc2b5\ub2c8\ub2e4", - "is_hot": "{entity_name} \uc774(\uac00) \ub728\uac81\uc2b5\ub2c8\ub2e4", - "is_light": "{entity_name} \uc774(\uac00) \ube5b\uc744 \uac10\uc9c0\ud588\uc2b5\ub2c8\ub2e4", - "is_locked": "{entity_name} \uc774(\uac00) \uc7a0\uacbc\uc2b5\ub2c8\ub2e4", - "is_moist": "{entity_name} \uc774(\uac00) \uc2b5\ud569\ub2c8\ub2e4", - "is_motion": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc784\uc744 \uac10\uc9c0\ud588\uc2b5\ub2c8\ub2e4", - "is_moving": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc600\uc2b5\ub2c8\ub2e4", - "is_no_gas": "{entity_name} \uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", - "is_no_light": "{entity_name} \uc774(\uac00) \ube5b\uc744 \uac10\uc9c0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", - "is_no_motion": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc784\uc744 \uac10\uc9c0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", - "is_no_problem": "{entity_name} \uc774(\uac00) \ubb38\uc81c\ub97c \uac10\uc9c0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", - "is_no_smoke": "{entity_name} \uc774(\uac00) \uc5f0\uae30\ub97c \uac10\uc9c0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", - "is_no_sound": "{entity_name} \uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", - "is_no_vibration": "{entity_name} \uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", - "is_not_bat_low": "{entity_name} \uc758 \ubc30\ud130\ub9ac\uac00 \uc815\uc0c1\uc785\ub2c8\ub2e4", - "is_not_cold": "{entity_name} \uc774(\uac00) \ucc28\uac11\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.", - "is_not_connected": "{entity_name} \uc758 \uc5f0\uacb0\uc774 \ub04a\uc5b4\uc84c\uc2b5\ub2c8\ub2e4", - "is_not_hot": "{entity_name} \uc774(\uac00) \ub728\uac81\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.", - "is_not_locked": "{entity_name} \uc758 \uc7a0\uae08\uc774 \ud574\uc81c\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "is_not_moist": "{entity_name} \uc774(\uac00) \uac74\uc870\ud569\ub2c8\ub2e4", - "is_not_moving": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc774\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", - "is_not_occupied": "{entity_name} \uc774(\uac00) \uc0ac\uc6a9\uc911\uc774\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", - "is_not_open": "{entity_name} \uc774(\uac00) \ub2eb\ud614\uc2b5\ub2c8\ub2e4", - "is_not_plugged_in": "{entity_name} \uc774(\uac00) \ubf51\ud614\uc2b5\ub2c8\ub2e4", - "is_not_powered": "{entity_name} \uc5d0 \uc804\uc6d0\uc774 \uacf5\uae09\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", - "is_not_present": "{entity_name} \uc774(\uac00) \uc5c6\uc2b5\ub2c8\ub2e4", - "is_not_unsafe": "{entity_name} \uc740(\ub294) \uc548\uc804\ud569\ub2c8\ub2e4", - "is_occupied": "{entity_name} \uc774(\uac00) \uc0ac\uc6a9\uc911\uc785\ub2c8\ub2e4", - "is_off": "{entity_name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4", - "is_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4", - "is_open": "{entity_name} \uc774(\uac00) \uc5f4\ub838\uc2b5\ub2c8\ub2e4", - "is_plugged_in": "{entity_name} \uc774(\uac00) \uaf3d\ud614\uc2b5\ub2c8\ub2e4", - "is_powered": "{entity_name} \uc5d0 \uc804\uc6d0\uc774 \uacf5\uae09\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "is_present": "{entity_name} \uc774(\uac00) \uc788\uc2b5\ub2c8\ub2e4", - "is_problem": "{entity_name} \uc774(\uac00) \ubb38\uc81c\ub97c \uac10\uc9c0\ud588\uc2b5\ub2c8\ub2e4", - "is_smoke": "{entity_name} \uc774(\uac00) \uc5f0\uae30\ub97c \uac10\uc9c0\ud588\uc2b5\ub2c8\ub2e4", - "is_sound": "{entity_name} \uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0\ud588\uc2b5\ub2c8\ub2e4", - "is_unsafe": "{entity_name} \uc740(\ub294) \uc548\uc804\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", - "is_vibration": "{entity_name} \uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud588\uc2b5\ub2c8\ub2e4" + "is_bat_low": "{entity_name} \uc758 \ubc30\ud130\ub9ac \uc794\ub7c9\uc774 \ubd80\uc871\ud558\uba74", + "is_cold": "{entity_name} \uc774(\uac00) \ucc28\uac00\uc6b0\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} \uc774(\uac00) \ub728\uac70\uc6b0\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} \uc774(\uac00) \ucc28\uac11\uc9c0 \uc54a\ub2e4\uba74", + "is_not_connected": "{entity_name} \uc758 \uc5f0\uacb0\uc774 \ub04a\uc5b4\uc838 \uc788\ub2e4\uba74", + "is_not_hot": "{entity_name} \uc774(\uac00) \ub728\uac81\uc9c0 \uc54a\ub2e4\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) \uc0ac\uc6a9 \uc911\uc774\uc9c0 \uc54a\uc73c\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) \uc0ac\uc6a9 \uc911\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" }, "trigger_type": { - "bat_low": "{entity_name} \uc758 \ubc30\ud130\ub9ac \uc794\ub7c9 \ubd80\uc871", - "closed": "{entity_name} \uc774(\uac00) \ub2eb\ud798", - "cold": "{entity_name} \uc774(\uac00) \ucc28\uac00\uc6cc\uc9d0", - "connected": "{entity_name} \uc774(\uac00) \uc5f0\uacb0\ub428", - "gas": "{entity_name} \uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0\ud568", - "hot": "{entity_name} \uc774(\uac00) \ub728\uac70\uc6cc\uc9d0", - "light": "{entity_name} \uc774(\uac00) \ube5b\uc744 \uac10\uc9c0\ud568", - "locked": "{entity_name} \uc774(\uac00) \uc7a0\uae40", - "moist": "{entity_name} \uc774(\uac00) \uc2b5\ud574\uc9d0", - "moist\u00a7": "{entity_name} \uc774(\uac00) \uc2b5\ud574\uc9d0", - "motion": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc784\uc744 \uac10\uc9c0\ud568", - "moving": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc784", - "no_gas": "{entity_name} \uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0 \ubabb\ud568", - "no_light": "{entity_name} \uc774(\uac00) \ube5b\uc744 \uac10\uc9c0 \ubabb\ud568", - "no_motion": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc784\uc744 \uac10\uc9c0 \ubabb\ud568", - "no_problem": "{entity_name} \uc774(\uac00) \ubb38\uc81c\ub97c \uac10\uc9c0 \ubabb\ud568", - "no_smoke": "{entity_name} \uc774(\uac00) \uc5f0\uae30\ub97c \uac10\uc9c0 \ubabb\ud568", - "no_sound": "{entity_name} \uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0 \ubabb\ud568", - "no_vibration": "{entity_name} \uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0 \ubabb\ud568", - "not_bat_low": "{entity_name} \uc758 \ubc30\ud130\ub9ac \uc815\uc0c1", - "not_cold": "{entity_name} \uc774(\uac00) \ucc28\uac11\uc9c0 \uc54a\uc74c", - "not_connected": "{entity_name} \uc758 \uc5f0\uacb0\uc774 \ub04a\uc5b4\uc9d0", - "not_hot": "{entity_name} \uc774(\uac00) \ub728\uac81\uc9c0 \uc54a\uc74c", - "not_locked": "{entity_name} \uc758 \uc7a0\uae08\uc774 \ud574\uc81c\ub428", - "not_moist": "{entity_name} \uc774(\uac00) \uac74\uc870\ud574\uc9d0", - "not_moving": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc774\uc9c0 \uc54a\uc74c", - "not_occupied": "{entity_name} \uc774(\uac00) \uc0ac\uc6a9\uc911\uc774\uc9c0 \uc54a\uc74c", - "not_opened": "{entity_name} \uc774(\uac00) \ub2eb\ud798", - "not_plugged_in": "{entity_name} \uc774(\uac00) \ubf51\ud798", - "not_powered": "{entity_name} \uc5d0 \uc804\uc6d0\uc774 \uacf5\uae09\ub418\uc9c0 \uc54a\uc74c", - "not_present": "{entity_name} \uc774(\uac00) \uc5c6\uc74c", - "not_unsafe": "{entity_name} \uc740(\ub294) \uc548\uc804\ud574\uc9d0", - "occupied": "{entity_name} \uc774(\uac00) \uc0ac\uc6a9\uc911", - "opened": "{entity_name} \uc774(\uac00) \uc5f4\ub9bc", - "plugged_in": "{entity_name} \uc774(\uac00) \uaf3d\ud798", - "powered": "{entity_name} \uc5d0 \uc804\uc6d0\uc774 \uacf5\uae09\ub428", - "present": "{entity_name} \uc774(\uac00) \uc788\uc74c", - "problem": "{entity_name} \uc774(\uac00) \ubb38\uc81c\ub97c \uac10\uc9c0\ud568", - "smoke": "{entity_name} \uc774(\uac00) \uc5f0\uae30\ub97c \uac10\uc9c0\ud568", - "sound": "{entity_name} \uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0\ud568", - "turned_off": "{entity_name} \uc774(\uac00) \uaebc\uc9d0", - "turned_on": "{entity_name} \uc774(\uac00) \ucf1c\uc9d0", - "unsafe": "{entity_name} \uc740(\ub294) \uc548\uc804\ud558\uc9c0 \uc54a\uc74c", - "vibration": "{entity_name} \uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud568" + "bat_low": "{entity_name} \ubc30\ud130\ub9ac \uc794\ub7c9\uc774 \ubd80\uc871\ud574\uc9c8 \ub54c", + "closed": "{entity_name} \uc774(\uac00) \ub2eb\ud790 \ub54c", + "cold": "{entity_name} \uc774(\uac00) \ucc28\uac00\uc6cc\uc9c8 \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} \uc774(\uac00) \ub728\uac70\uc6cc\uc9c8 \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", + "moist\u00a7": "{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} \uc774(\uac00) \ucc28\uac11\uc9c0 \uc54a\uac8c \ub420 \ub54c", + "not_connected": "{entity_name} \uc758 \uc5f0\uacb0\uc774 \ub04a\uc5b4\uc9c8 \ub54c", + "not_hot": "{entity_name} \uc774(\uac00) \ub728\uac81\uc9c0 \uc54a\uac8c \ub420 \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) \uc0ac\uc6a9 \uc911\uc774\uc9c0 \uc54a\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) \uc0ac\uc6a9 \uc911\uc774 \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" } } } \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/__init__.py b/homeassistant/components/binary_sensor/__init__.py index e5f5dc94ff1..73d5e0be458 100644 --- a/homeassistant/components/binary_sensor/__init__.py +++ b/homeassistant/components/binary_sensor/__init__.py @@ -5,14 +5,13 @@ import logging import voluptuous as vol -from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.entity import Entity -from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.helpers.config_validation import ( # noqa: F401 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 diff --git a/homeassistant/components/binary_sensor/device_condition.py b/homeassistant/components/binary_sensor/device_condition.py index 0766d82c727..842790e0178 100644 --- a/homeassistant/components/binary_sensor/device_condition.py +++ b/homeassistant/components/binary_sensor/device_condition.py @@ -1,10 +1,11 @@ """Implemenet device conditions for binary sensor.""" from typing import Dict, List + import voluptuous as vol -from homeassistant.core import HomeAssistant from homeassistant.components.device_automation.const import CONF_IS_OFF, CONF_IS_ON from homeassistant.const import ATTR_DEVICE_CLASS, CONF_ENTITY_ID, CONF_FOR, CONF_TYPE +from homeassistant.core import HomeAssistant from homeassistant.helpers import condition, config_validation as cv from homeassistant.helpers.entity_registry import ( async_entries_for_device, @@ -13,7 +14,6 @@ from homeassistant.helpers.entity_registry import ( from homeassistant.helpers.typing import ConfigType from . import ( - DOMAIN, DEVICE_CLASS_BATTERY, DEVICE_CLASS_COLD, DEVICE_CLASS_CONNECTIVITY, @@ -37,6 +37,7 @@ from . import ( DEVICE_CLASS_SOUND, DEVICE_CLASS_VIBRATION, DEVICE_CLASS_WINDOW, + DOMAIN, ) DEVICE_CLASS_NONE = "none" diff --git a/homeassistant/components/binary_sensor/device_trigger.py b/homeassistant/components/binary_sensor/device_trigger.py index c51b9749288..288cc101d93 100644 --- a/homeassistant/components/binary_sensor/device_trigger.py +++ b/homeassistant/components/binary_sensor/device_trigger.py @@ -8,11 +8,10 @@ from homeassistant.components.device_automation.const import ( CONF_TURNED_ON, ) from homeassistant.const import ATTR_DEVICE_CLASS, CONF_ENTITY_ID, CONF_FOR, CONF_TYPE -from homeassistant.helpers.entity_registry import async_entries_for_device from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.entity_registry import async_entries_for_device from . import ( - DOMAIN, DEVICE_CLASS_BATTERY, DEVICE_CLASS_COLD, DEVICE_CLASS_CONNECTIVITY, @@ -36,9 +35,9 @@ from . import ( DEVICE_CLASS_SOUND, DEVICE_CLASS_VIBRATION, DEVICE_CLASS_WINDOW, + DOMAIN, ) - # mypy: allow-untyped-defs, no-check-untyped-defs DEVICE_CLASS_NONE = "none" diff --git a/homeassistant/components/bitcoin/sensor.py b/homeassistant/components/bitcoin/sensor.py index b62bb434e85..bc8394d51a5 100644 --- a/homeassistant/components/bitcoin/sensor.py +++ b/homeassistant/components/bitcoin/sensor.py @@ -148,7 +148,7 @@ class BitcoinSensor(Entity): elif self.type == "total_btc": self._state = "{0:.2f}".format(stats.total_btc * 0.00000001) elif self.type == "total_blocks": - self._state = "{0:.2f}".format(stats.total_blocks) + self._state = "{0:.0f}".format(stats.total_blocks) elif self.type == "next_retarget": self._state = "{0:.2f}".format(stats.next_retarget) elif self.type == "estimated_transaction_volume_usd": diff --git a/homeassistant/components/bizkaibus/sensor.py b/homeassistant/components/bizkaibus/sensor.py index c54a61c66b1..931fbbb834d 100644 --- a/homeassistant/components/bizkaibus/sensor.py +++ b/homeassistant/components/bizkaibus/sensor.py @@ -2,15 +2,14 @@ import logging -import voluptuous as vol from bizkaibus.bizkaibus import BizkaibusData -import homeassistant.helpers.config_validation as cv +import voluptuous as vol -from homeassistant.const import CONF_NAME 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 - _LOGGER = logging.getLogger(__name__) ATTR_DUE_IN = "Due in" diff --git a/homeassistant/components/blackbird/const.py b/homeassistant/components/blackbird/const.py new file mode 100644 index 00000000000..aa8d7e7d514 --- /dev/null +++ b/homeassistant/components/blackbird/const.py @@ -0,0 +1,3 @@ +"""Constants for the Monoprice Blackbird Matrix Switch component.""" +DOMAIN = "blackbird" +SERVICE_SETALLZONES = "set_all_zones" diff --git a/homeassistant/components/blackbird/media_player.py b/homeassistant/components/blackbird/media_player.py index e1aa7200c07..a0ea369bb9b 100644 --- a/homeassistant/components/blackbird/media_player.py +++ b/homeassistant/components/blackbird/media_player.py @@ -8,7 +8,6 @@ import voluptuous as vol from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( - DOMAIN, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, @@ -24,6 +23,8 @@ from homeassistant.const import ( ) import homeassistant.helpers.config_validation as cv +from .const import DOMAIN, SERVICE_SETALLZONES + _LOGGER = logging.getLogger(__name__) SUPPORT_BLACKBIRD = SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE @@ -39,7 +40,6 @@ CONF_SOURCES = "sources" DATA_BLACKBIRD = "blackbird" -SERVICE_SETALLZONES = "blackbird_set_all_zones" ATTR_SOURCE = "source" BLACKBIRD_SETALLZONES_SCHEMA = MEDIA_PLAYER_SCHEMA.extend( diff --git a/homeassistant/components/blackbird/services.yaml b/homeassistant/components/blackbird/services.yaml index e69de29bb2d..d541e21049d 100644 --- a/homeassistant/components/blackbird/services.yaml +++ b/homeassistant/components/blackbird/services.yaml @@ -0,0 +1,10 @@ +set_all_zones: + description: Set all Blackbird zones to a single source. + fields: + entity_id: + description: Name of any blackbird zone. + example: 'media_player.zone_1' + source: + description: Name of source to switch to. + example: 'Source 1' + diff --git a/homeassistant/components/blink/alarm_control_panel.py b/homeassistant/components/blink/alarm_control_panel.py index b1c9f6a7ec0..9b23c1606d4 100644 --- a/homeassistant/components/blink/alarm_control_panel.py +++ b/homeassistant/components/blink/alarm_control_panel.py @@ -2,6 +2,7 @@ import logging from homeassistant.components.alarm_control_panel import AlarmControlPanel +from homeassistant.components.alarm_control_panel.const import SUPPORT_ALARM_ARM_AWAY from homeassistant.const import ( ATTR_ATTRIBUTION, STATE_ALARM_ARMED_AWAY, @@ -52,6 +53,11 @@ class BlinkSyncModule(AlarmControlPanel): """Return the state of the device.""" return self._state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_AWAY + @property def name(self): """Return the name of the panel.""" diff --git a/homeassistant/components/blinkt/light.py b/homeassistant/components/blinkt/light.py index e626a73d287..0fedc2b794b 100644 --- a/homeassistant/components/blinkt/light.py +++ b/homeassistant/components/blinkt/light.py @@ -4,16 +4,16 @@ import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.light import ( ATTR_BRIGHTNESS, - SUPPORT_BRIGHTNESS, ATTR_HS_COLOR, + PLATFORM_SCHEMA, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR, Light, - PLATFORM_SCHEMA, ) from homeassistant.const import CONF_NAME +import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/bloomsky/binary_sensor.py b/homeassistant/components/bloomsky/binary_sensor.py index 99951fcf5c5..cc6562a0bc1 100644 --- a/homeassistant/components/bloomsky/binary_sensor.py +++ b/homeassistant/components/bloomsky/binary_sensor.py @@ -3,7 +3,7 @@ import logging import voluptuous as vol -from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice from homeassistant.const import CONF_MONITORED_CONDITIONS import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/bloomsky/sensor.py b/homeassistant/components/bloomsky/sensor.py index 18f60036397..84871b7b30e 100644 --- a/homeassistant/components/bloomsky/sensor.py +++ b/homeassistant/components/bloomsky/sensor.py @@ -4,9 +4,9 @@ import logging import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import TEMP_FAHRENHEIT, TEMP_CELSIUS, CONF_MONITORED_CONDITIONS -from homeassistant.helpers.entity import Entity +from homeassistant.const import CONF_MONITORED_CONDITIONS, TEMP_CELSIUS, TEMP_FAHRENHEIT import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity from . import BLOOMSKY diff --git a/homeassistant/components/bluesound/const.py b/homeassistant/components/bluesound/const.py new file mode 100644 index 00000000000..af1a8e5187c --- /dev/null +++ b/homeassistant/components/bluesound/const.py @@ -0,0 +1,6 @@ +"""Constants for the Bluesound HiFi wireless speakers and audio integrations component.""" +DOMAIN = "bluesound" +SERVICE_CLEAR_TIMER = "clear_sleep_timer" +SERVICE_JOIN = "join" +SERVICE_SET_TIMER = "set_sleep_timer" +SERVICE_UNJOIN = "unjoin" diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index 7b2719c1e4e..04ba21555d4 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -15,7 +15,6 @@ import xmltodict from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( ATTR_MEDIA_ENQUEUE, - DOMAIN, MEDIA_TYPE_MUSIC, SUPPORT_CLEAR_PLAYLIST, SUPPORT_NEXT_TRACK, @@ -51,6 +50,14 @@ from homeassistant.helpers.event import async_track_time_interval from homeassistant.util import Throttle import homeassistant.util.dt as dt_util +from .const import ( + DOMAIN, + SERVICE_CLEAR_TIMER, + SERVICE_JOIN, + SERVICE_SET_TIMER, + SERVICE_UNJOIN, +) + _LOGGER = logging.getLogger(__name__) ATTR_BLUESOUND_GROUP = "bluesound_group" @@ -62,10 +69,6 @@ DEFAULT_PORT = 11000 NODE_OFFLINE_CHECK_TIMEOUT = 180 NODE_RETRY_INITIATION = timedelta(minutes=3) -SERVICE_CLEAR_TIMER = "bluesound_clear_sleep_timer" -SERVICE_JOIN = "bluesound_join" -SERVICE_SET_TIMER = "bluesound_set_sleep_timer" -SERVICE_UNJOIN = "bluesound_unjoin" STATE_GROUPED = "grouped" SYNC_STATUS_INTERVAL = timedelta(minutes=5) diff --git a/homeassistant/components/bluesound/services.yaml b/homeassistant/components/bluesound/services.yaml index e69de29bb2d..6c85c77e961 100644 --- a/homeassistant/components/bluesound/services.yaml +++ b/homeassistant/components/bluesound/services.yaml @@ -0,0 +1,30 @@ +join: + description: Group player together. + fields: + master: + description: Entity ID of the player that should become the master of the group. + example: 'media_player.bluesound_livingroom' + entity_id: + description: Name(s) of entities that will coordinate the grouping. Platform dependent. + example: 'media_player.bluesound_livingroom' + +unjoin: + description: Unjoin the player from a group. + fields: + entity_id: + description: Name(s) of entities that will be unjoined from their group. Platform dependent. + example: 'media_player.bluesound_livingroom' + +set_sleep_timer: + description: "Set a Bluesound timer. It will increase timer in steps: 15, 30, 45, 60, 90, 0" + fields: + entity_id: + description: Name(s) of entities that will have a timer set. + example: 'media_player.bluesound_livingroom' + +clear_sleep_timer: + description: Clear a Bluesound timer. + fields: + entity_id: + description: Name(s) of entities that will have the timer cleared. + example: 'media_player.bluesound_livingroom' \ No newline at end of file diff --git a/homeassistant/components/bluetooth_le_tracker/device_tracker.py b/homeassistant/components/bluetooth_le_tracker/device_tracker.py index 18edd750639..9c64232c6e9 100644 --- a/homeassistant/components/bluetooth_le_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_le_tracker/device_tracker.py @@ -4,18 +4,18 @@ import logging import pygatt # pylint: disable=import-error -from homeassistant.helpers.event import track_point_in_utc_time +from homeassistant.components.device_tracker.const import ( + CONF_SCAN_INTERVAL, + CONF_TRACK_NEW, + SCAN_INTERVAL, + SOURCE_TYPE_BLUETOOTH_LE, +) from homeassistant.components.device_tracker.legacy import ( YAML_DEVICES, async_load_config, ) -from homeassistant.components.device_tracker.const import ( - CONF_TRACK_NEW, - CONF_SCAN_INTERVAL, - SCAN_INTERVAL, - SOURCE_TYPE_BLUETOOTH_LE, -) from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.helpers.event import track_point_in_utc_time import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -77,7 +77,7 @@ def setup_scanner(hass, config, see, discovery_info=None): devices = {x["address"]: x["name"] for x in devs} _LOGGER.debug("Bluetooth LE devices discovered = %s", devices) - except RuntimeError as error: + except (RuntimeError, pygatt.exceptions.BLEError) as error: _LOGGER.error("Error during Bluetooth LE scan: %s", error) return {} return devices diff --git a/homeassistant/components/bluetooth_tracker/const.py b/homeassistant/components/bluetooth_tracker/const.py new file mode 100644 index 00000000000..b481efa296f --- /dev/null +++ b/homeassistant/components/bluetooth_tracker/const.py @@ -0,0 +1,3 @@ +"""Constants for the Bluetooth Tracker component.""" +DOMAIN = "bluetooth_tracker" +SERVICE_UPDATE = "update" diff --git a/homeassistant/components/bluetooth_tracker/device_tracker.py b/homeassistant/components/bluetooth_tracker/device_tracker.py index 6a26775b0a8..d833f60c84f 100644 --- a/homeassistant/components/bluetooth_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_tracker/device_tracker.py @@ -1,7 +1,7 @@ """Tracking for bluetooth devices.""" import asyncio import logging -from typing import List, Set, Tuple, Optional +from typing import List, Optional, Set, Tuple # pylint: disable=import-error import bluetooth @@ -13,7 +13,6 @@ from homeassistant.components.device_tracker.const import ( CONF_SCAN_INTERVAL, CONF_TRACK_NEW, DEFAULT_TRACK_NEW, - DOMAIN, SCAN_INTERVAL, SOURCE_TYPE_BLUETOOTH, ) @@ -25,6 +24,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import HomeAssistantType +from .const import DOMAIN, SERVICE_UPDATE _LOGGER = logging.getLogger(__name__) @@ -184,8 +184,6 @@ async def async_setup_scanner( hass.async_create_task(update_bluetooth()) async_track_time_interval(hass, update_bluetooth, interval) - hass.services.async_register( - DOMAIN, "bluetooth_tracker_update", handle_manual_update_bluetooth - ) + hass.services.async_register(DOMAIN, SERVICE_UPDATE, handle_manual_update_bluetooth) return True diff --git a/homeassistant/components/bluetooth_tracker/services.yaml b/homeassistant/components/bluetooth_tracker/services.yaml index e69de29bb2d..b48c48a8968 100644 --- a/homeassistant/components/bluetooth_tracker/services.yaml +++ b/homeassistant/components/bluetooth_tracker/services.yaml @@ -0,0 +1,2 @@ +update: + description: Trigger manual tracker update \ No newline at end of file diff --git a/homeassistant/components/bme280/sensor.py b/homeassistant/components/bme280/sensor.py index b9bc18e6abf..e1e33210c9b 100644 --- a/homeassistant/components/bme280/sensor.py +++ b/homeassistant/components/bme280/sensor.py @@ -3,14 +3,13 @@ from datetime import timedelta from functools import partial import logging -import smbus # pylint: disable=import-error from i2csense.bme280 import BME280 # pylint: disable=import-error - +import smbus # pylint: disable=import-error import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_NAME, TEMP_FAHRENHEIT import homeassistant.helpers.config_validation as cv -from homeassistant.const import TEMP_FAHRENHEIT, CONF_NAME, CONF_MONITORED_CONDITIONS from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle from homeassistant.util.temperature import celsius_to_fahrenheit diff --git a/homeassistant/components/bme680/sensor.py b/homeassistant/components/bme680/sensor.py index 5a1e9fd120f..65c87890242 100644 --- a/homeassistant/components/bme680/sensor.py +++ b/homeassistant/components/bme680/sensor.py @@ -3,8 +3,8 @@ import logging import threading from time import sleep, time -from smbus import SMBus # pylint: disable=import-error import bme680 # pylint: disable=import-error +from smbus import SMBus # pylint: disable=import-error import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index 455d821e669..6e7723b16ec 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -119,7 +119,7 @@ class BMWConnectedDriveAccount: def __init__( self, username: str, password: str, region_str: str, name: str, read_only ) -> None: - """Constructor.""" + """Initialize account.""" region = get_region_from_name(region_str) diff --git a/homeassistant/components/bmw_connected_drive/binary_sensor.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py index 8163ae4eae3..591cdadda35 100644 --- a/homeassistant/components/bmw_connected_drive/binary_sensor.py +++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py @@ -13,7 +13,7 @@ _LOGGER = logging.getLogger(__name__) SENSOR_TYPES = { "lids": ["Doors", "opening", "mdi:car-door-lock"], "windows": ["Windows", "opening", "mdi:car-door"], - "door_lock_state": ["Door lock state", "safety", "mdi:car-key"], + "door_lock_state": ["Door lock state", "lock", "mdi:car-key"], "lights_parking": ["Parking lights", "light", "mdi:car-parking-lights"], "condition_based_services": ["Condition based services", "problem", "mdi:wrench"], "check_control_messages": ["Control messages", "problem", "mdi:car-tire-alert"], @@ -59,7 +59,7 @@ class BMWConnectedDriveSensor(BinarySensorDevice): def __init__( self, account, vehicle, attribute: str, sensor_name, device_class, icon ): - """Constructor.""" + """Initialize sensor.""" self._account = account self._vehicle = vehicle self._attribute = attribute diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index f919bba6b95..3c40900bed8 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -69,7 +69,7 @@ class BMWConnectedDriveSensor(Entity): """Representation of a BMW vehicle sensor.""" def __init__(self, account, vehicle, attribute: str, attribute_info): - """Constructor.""" + """Initialize BMW vehicle sensor.""" self._vehicle = vehicle self._account = account self._attribute = attribute diff --git a/homeassistant/components/bom/sensor.py b/homeassistant/components/bom/sensor.py index ed22be003ad..7d951968cb2 100644 --- a/homeassistant/components/bom/sensor.py +++ b/homeassistant/components/bom/sensor.py @@ -12,19 +12,19 @@ import zipfile import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt_util from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_MONITORED_CONDITIONS, - TEMP_CELSIUS, - CONF_NAME, ATTR_ATTRIBUTION, CONF_LATITUDE, CONF_LONGITUDE, + CONF_MONITORED_CONDITIONS, + CONF_NAME, + TEMP_CELSIUS, ) +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 _RESOURCE = "http://www.bom.gov.au/fwo/{}/{}.{}.json" _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/broadlink/__init__.py b/homeassistant/components/broadlink/__init__.py index 589da62feaa..3f9b5cd4597 100644 --- a/homeassistant/components/broadlink/__init__.py +++ b/homeassistant/components/broadlink/__init__.py @@ -1,10 +1,12 @@ """The broadlink component.""" import asyncio from base64 import b64decode, b64encode +from binascii import unhexlify +from datetime import timedelta import logging +import re import socket -from datetime import timedelta import voluptuous as vol from homeassistant.const import CONF_HOST @@ -27,6 +29,31 @@ def data_packet(value): return b64decode(value) +def hostname(value): + """Validate a hostname.""" + host = str(value).lower() + if len(host) > 253: + raise ValueError + if host[-1] == ".": + host = host[:-1] + allowed = re.compile(r"(?!-)[a-z\d-]{1,63}(? 0: _LOGGER.warning( - "Unable to parse data from Buienradar." "(Msg: %s)", + "Unable to parse data from Buienradar. (Msg: %s)", result.get(MESSAGE), ) await self.schedule_update(SCHEDULE_NOK) diff --git a/homeassistant/components/buienradar/weather.py b/homeassistant/components/buienradar/weather.py index c95e57807c4..98cbb2f5e43 100644 --- a/homeassistant/components/buienradar/weather.py +++ b/homeassistant/components/buienradar/weather.py @@ -28,8 +28,8 @@ from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_C from homeassistant.helpers import config_validation as cv # Reuse data and API logic from the sensor implementation -from .util import BrData from .const import DEFAULT_TIMEFRAME +from .util import BrData _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index ca240925cf5..35b25b86d43 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -17,7 +17,6 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.template import DATE_STR_FORMAT from homeassistant.util import dt - # mypy: allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/calendar/services.yaml b/homeassistant/components/calendar/services.yaml index ebf0c7b1591..d8a0575bced 100644 --- a/homeassistant/components/calendar/services.yaml +++ b/homeassistant/components/calendar/services.yaml @@ -1,26 +1,2 @@ # Describes the format for available calendar services -todoist_new_task: - description: Create a new task and add it to a project. - fields: - content: - description: The name of the task. - example: Pick up the mail - project: - description: The name of the project this task should belong to. Defaults to Inbox. - example: Errands - labels: - description: Any labels that you want to apply to this task, separated by a comma. - example: Chores,Deliveries - priority: - description: The priority of this task, from 1 (normal) to 4 (urgent). - example: 2 - due_date_string: - description: The day this task is due, in natural language. - example: "tomorrow" - due_date_lang: - description: The language of due_date_string. - example: "en" - due_date: - description: The day this task is due, in format YYYY-MM-DD. - example: "2018-04-01" diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 58b6db139f5..4fe52a7d164 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -4,55 +4,54 @@ import base64 import collections from contextlib import suppress from datetime import timedelta -import logging import hashlib +import logging from random import SystemRandom -import attr from aiohttp import web import async_timeout +import attr import voluptuous as vol -from homeassistant.core import callback +from homeassistant.components import websocket_api +from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView +from homeassistant.components.media_player.const import ( + ATTR_MEDIA_CONTENT_ID, + ATTR_MEDIA_CONTENT_TYPE, + 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.const import ( ATTR_ENTITY_ID, + CONF_FILENAME, SERVICE_TURN_OFF, SERVICE_TURN_ON, - CONF_FILENAME, ) +from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.loader import bind_hass -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.entity_component import EntityComponent +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) -from homeassistant.components.http import HomeAssistantView, KEY_AUTHENTICATED -from homeassistant.components.media_player.const import ( - ATTR_MEDIA_CONTENT_ID, - ATTR_MEDIA_CONTENT_TYPE, - SERVICE_PLAY_MEDIA, - DOMAIN as DOMAIN_MP, -) -from homeassistant.components.stream import request_stream -from homeassistant.components.stream.const import ( - OUTPUT_FORMATS, - FORMAT_CONTENT_TYPE, - CONF_STREAM_SOURCE, - CONF_LOOKBACK, - CONF_DURATION, - SERVICE_RECORD, - DOMAIN as DOMAIN_STREAM, -) -from homeassistant.components import websocket_api -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.loader import bind_hass from homeassistant.setup import async_when_setup -from .const import DOMAIN, DATA_CAMERA_PREFS +from .const import DATA_CAMERA_PREFS, DOMAIN from .prefs import CameraPreferences - # mypy: allow-untyped-calls, allow-untyped-defs _LOGGER = logging.getLogger(__name__) @@ -169,7 +168,7 @@ async def async_get_still_stream(request, image_cb, content_type, interval): This method must be run in the event loop. """ response = web.StreamResponse() - response.content_type = "multipart/x-mixed-replace; " "boundary=--frameboundary" + response.content_type = "multipart/x-mixed-replace; boundary=--frameboundary" await response.prepare(request) async def write_to_mjpeg_stream(img_bytes): diff --git a/homeassistant/components/camera/manifest.json b/homeassistant/components/camera/manifest.json index a3395965e4f..25d344d05ad 100644 --- a/homeassistant/components/camera/manifest.json +++ b/homeassistant/components/camera/manifest.json @@ -3,11 +3,7 @@ "name": "Camera", "documentation": "https://www.home-assistant.io/integrations/camera", "requirements": [], - "dependencies": [ - "http" - ], - "after_dependencies": [ - "stream" - ], + "dependencies": ["http"], + "after_dependencies": ["media_player"], "codeowners": [] } diff --git a/homeassistant/components/camera/prefs.py b/homeassistant/components/camera/prefs.py index d83e0b55c96..ae182c62dc6 100644 --- a/homeassistant/components/camera/prefs.py +++ b/homeassistant/components/camera/prefs.py @@ -1,7 +1,6 @@ """Preference management for camera component.""" from .const import DOMAIN, PREF_PRELOAD_STREAM - # mypy: allow-untyped-defs, no-check-untyped-defs STORAGE_KEY = DOMAIN diff --git a/homeassistant/components/camera/services.yaml b/homeassistant/components/camera/services.yaml index 4c2d89db86d..c50e2926a3f 100644 --- a/homeassistant/components/camera/services.yaml +++ b/homeassistant/components/camera/services.yaml @@ -68,16 +68,6 @@ record: description: (Optional) Target lookback period (in seconds) to include in addition to duration. Only available if there is currently an active HLS stream. example: 4 -local_file_update_file_path: - description: Update the file_path for a local_file camera. - fields: - entity_id: - description: Name(s) of entities to update. - example: 'camera.local_file' - file_path: - description: Path to the new image file. - example: '/images/newimage.jpg' - onvif_ptz: description: Pan/Tilt/Zoom service for ONVIF camera. fields: diff --git a/homeassistant/components/canary/alarm_control_panel.py b/homeassistant/components/canary/alarm_control_panel.py index 856ecb9f3a2..cceb78743d3 100644 --- a/homeassistant/components/canary/alarm_control_panel.py +++ b/homeassistant/components/canary/alarm_control_panel.py @@ -4,6 +4,11 @@ import logging from canary.api import LOCATION_MODE_AWAY, LOCATION_MODE_HOME, LOCATION_MODE_NIGHT from homeassistant.components.alarm_control_panel import AlarmControlPanel +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, +) from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, @@ -59,6 +64,11 @@ class CanaryAlarm(AlarmControlPanel): return STATE_ALARM_ARMED_NIGHT return None + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT + @property def device_state_attributes(self): """Return the state attributes.""" diff --git a/homeassistant/components/canary/sensor.py b/homeassistant/components/canary/sensor.py index 6bb01c9d114..67654c99f3e 100644 --- a/homeassistant/components/canary/sensor.py +++ b/homeassistant/components/canary/sensor.py @@ -1,4 +1,5 @@ """Support for Canary sensors.""" +from canary.api import SensorType from homeassistant.const import TEMP_CELSIUS from homeassistant.helpers.entity import Entity @@ -103,8 +104,6 @@ class CanarySensor(Entity): """Get the latest state of the sensor.""" self._data.update() - from canary.api import SensorType - canary_sensor_type = None if self._sensor_type[0] == "air_quality": canary_sensor_type = SensorType.AIR_QUALITY diff --git a/homeassistant/components/cast/.translations/ko.json b/homeassistant/components/cast/.translations/ko.json index 1374372aa24..f0eebf4b7b9 100644 --- a/homeassistant/components/cast/.translations/ko.json +++ b/homeassistant/components/cast/.translations/ko.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "Google \uce90\uc2a4\ud2b8\ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "Google \uce90\uc2a4\ud2b8\ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "Google \uce90\uc2a4\ud2b8" } }, diff --git a/homeassistant/components/cast/discovery.py b/homeassistant/components/cast/discovery.py index d3097b3cc29..54f165889af 100644 --- a/homeassistant/components/cast/discovery.py +++ b/homeassistant/components/cast/discovery.py @@ -9,9 +9,9 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import dispatcher_send from .const import ( + INTERNAL_DISCOVERY_RUNNING_KEY, KNOWN_CHROMECAST_INFO_KEY, SIGNAL_CAST_DISCOVERED, - INTERNAL_DISCOVERY_RUNNING_KEY, SIGNAL_CAST_REMOVED, ) from .helpers import ChromecastInfo, ChromeCastZeroconf diff --git a/homeassistant/components/cast/helpers.py b/homeassistant/components/cast/helpers.py index ea5c77ebc1a..e82f6c9e4ed 100644 --- a/homeassistant/components/cast/helpers.py +++ b/homeassistant/components/cast/helpers.py @@ -150,7 +150,6 @@ class CastStatusListener: chromecast.register_status_listener(self) chromecast.socket_client.media_controller.register_status_listener(self) chromecast.register_connection_listener(self) - # pylint: disable=protected-access if cast_device._cast_info.is_audio_group: self._mz_mgr.add_multizone(chromecast) else: diff --git a/homeassistant/components/cast/home_assistant_cast.py b/homeassistant/components/cast/home_assistant_cast.py index d5d35ba7c9f..0b8633e1916 100644 --- a/homeassistant/components/cast/home_assistant_cast.py +++ b/homeassistant/components/cast/home_assistant_cast.py @@ -1,9 +1,8 @@ """Home Assistant Cast integration for Cast.""" from typing import Optional -import voluptuous as vol - from pychromecast.controllers.homeassistant import HomeAssistantController +import voluptuous as vol from homeassistant import auth, config_entries, core from homeassistant.const import ATTR_ENTITY_ID diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index b6776a17f7c..8ad6f8fdb8d 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -5,6 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/cast", "requirements": ["pychromecast==4.0.1"], "dependencies": [], + "after_dependencies": ["cloud"], "zeroconf": ["_googlecast._tcp.local."], "codeowners": [] } diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index c2d847fd09b..03174134502 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -4,12 +4,12 @@ import logging from typing import Optional import pychromecast +from pychromecast.controllers.homeassistant import HomeAssistantController +from pychromecast.controllers.multizone import MultizoneManager from pychromecast.socket_client import ( CONNECTION_STATUS_CONNECTED, CONNECTION_STATUS_DISCONNECTED, ) -from pychromecast.controllers.multizone import MultizoneManager -from pychromecast.controllers.homeassistant import HomeAssistantController import voluptuous as vol from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice @@ -46,22 +46,22 @@ import homeassistant.util.dt as dt_util from homeassistant.util.logging import async_create_catching_coro from .const import ( - DOMAIN as CAST_DOMAIN, ADDED_CAST_DEVICES_KEY, - SIGNAL_CAST_DISCOVERED, - KNOWN_CHROMECAST_INFO_KEY, CAST_MULTIZONE_MANAGER_KEY, DEFAULT_PORT, + DOMAIN as CAST_DOMAIN, + KNOWN_CHROMECAST_INFO_KEY, + SIGNAL_CAST_DISCOVERED, SIGNAL_CAST_REMOVED, SIGNAL_HASS_CAST_SHOW_VIEW, ) +from .discovery import discover_chromecast, setup_internal_discovery from .helpers import ( - ChromecastInfo, CastStatusListener, - DynamicGroupCastStatusListener, + ChromecastInfo, ChromeCastZeroconf, + DynamicGroupCastStatusListener, ) -from .discovery import setup_internal_discovery, discover_chromecast _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/cert_expiry/.translations/da.json b/homeassistant/components/cert_expiry/.translations/da.json index c95a56320c9..26ee436860a 100644 --- a/homeassistant/components/cert_expiry/.translations/da.json +++ b/homeassistant/components/cert_expiry/.translations/da.json @@ -21,6 +21,6 @@ "title": "Definer certifikatet, der skal testes" } }, - "title": "Certifikat udl\u00f8b" + "title": "Certifikatets udl\u00f8bsdato" } } \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/config_flow.py b/homeassistant/components/cert_expiry/config_flow.py index 78450d247b9..14532aea65f 100644 --- a/homeassistant/components/cert_expiry/config_flow.py +++ b/homeassistant/components/cert_expiry/config_flow.py @@ -2,13 +2,14 @@ import logging import socket import ssl + import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_PORT, CONF_NAME, CONF_HOST +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT from homeassistant.core import HomeAssistant, callback -from .const import DOMAIN, DEFAULT_PORT, DEFAULT_NAME +from .const import DEFAULT_NAME, DEFAULT_PORT, DOMAIN from .helper import get_cert _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/cert_expiry/sensor.py b/homeassistant/components/cert_expiry/sensor.py index 3022c7bd42b..3a76575dfdd 100644 --- a/homeassistant/components/cert_expiry/sensor.py +++ b/homeassistant/components/cert_expiry/sensor.py @@ -1,24 +1,24 @@ """Counter for the days until an HTTPS (TLS) certificate will expire.""" +from datetime import datetime, timedelta import logging import socket import ssl -from datetime import datetime, timedelta import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( - CONF_NAME, CONF_HOST, + CONF_NAME, CONF_PORT, EVENT_HOMEASSISTANT_START, ) from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from .const import DOMAIN, DEFAULT_NAME, DEFAULT_PORT +from .const import DEFAULT_NAME, DEFAULT_PORT, DOMAIN from .helper import get_cert _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/channels/const.py b/homeassistant/components/channels/const.py new file mode 100644 index 00000000000..5ae7fdebb0b --- /dev/null +++ b/homeassistant/components/channels/const.py @@ -0,0 +1,5 @@ +"""Constants for the Channels component.""" +DOMAIN = "channels" +SERVICE_SEEK_FORWARD = "seek_forward" +SERVICE_SEEK_BACKWARD = "seek_backward" +SERVICE_SEEK_BY = "seek_by" diff --git a/homeassistant/components/channels/media_player.py b/homeassistant/components/channels/media_player.py index 6d978a5451e..e4acc2f907c 100644 --- a/homeassistant/components/channels/media_player.py +++ b/homeassistant/components/channels/media_player.py @@ -6,7 +6,6 @@ import voluptuous as vol from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( - DOMAIN, MEDIA_TYPE_CHANNEL, MEDIA_TYPE_EPISODE, MEDIA_TYPE_MOVIE, @@ -31,6 +30,8 @@ from homeassistant.const import ( ) import homeassistant.helpers.config_validation as cv +from .const import DOMAIN, SERVICE_SEEK_BACKWARD, SERVICE_SEEK_BY, SERVICE_SEEK_FORWARD + _LOGGER = logging.getLogger(__name__) DATA_CHANNELS = "channels" @@ -56,9 +57,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( } ) -SERVICE_SEEK_FORWARD = "channels_seek_forward" -SERVICE_SEEK_BACKWARD = "channels_seek_backward" -SERVICE_SEEK_BY = "channels_seek_by" # Service call validation schemas ATTR_SECONDS = "seconds" diff --git a/homeassistant/components/channels/services.yaml b/homeassistant/components/channels/services.yaml index e69de29bb2d..cbb1dd201a6 100644 --- a/homeassistant/components/channels/services.yaml +++ b/homeassistant/components/channels/services.yaml @@ -0,0 +1,23 @@ +seek_forward: + description: Seek forward by a set number of seconds. + fields: + entity_id: + description: Name of entity for the instance of Channels to seek in. + example: 'media_player.family_room_channels' + +seek_backward: + description: Seek backward by a set number of seconds. + fields: + entity_id: + description: Name of entity for the instance of Channels to seek in. + example: 'media_player.family_room_channels' + +seek_by: + description: Seek by an inputted number of seconds. + fields: + entity_id: + description: Name of entity for the instance of Channels to seek in. + example: 'media_player.family_room_channels' + seconds: + description: Number of seconds to seek by. Negative numbers seek backwards. + example: 120 diff --git a/homeassistant/components/cisco_mobility_express/device_tracker.py b/homeassistant/components/cisco_mobility_express/device_tracker.py index 702ebdfa611..db504e3d19b 100644 --- a/homeassistant/components/cisco_mobility_express/device_tracker.py +++ b/homeassistant/components/cisco_mobility_express/device_tracker.py @@ -89,5 +89,5 @@ class CiscoMEDeviceScanner(DeviceScanner): """Check the Cisco ME controller for devices.""" self.last_results = self.controller.get_associated_devices() _LOGGER.debug( - "Cisco Mobility Express controller returned:" " %s", self.last_results + "Cisco Mobility Express controller returned: %s", self.last_results ) diff --git a/homeassistant/components/cisco_webex_teams/notify.py b/homeassistant/components/cisco_webex_teams/notify.py index 6f80fa138d4..7be53d1fb6c 100644 --- a/homeassistant/components/cisco_webex_teams/notify.py +++ b/homeassistant/components/cisco_webex_teams/notify.py @@ -54,5 +54,5 @@ class CiscoWebexTeamsNotificationService(BaseNotificationService): self.client.messages.create(roomId=self.room, html=f"{title}{message}") except ApiError as api_error: _LOGGER.error( - "Could not send CiscoWebexTeams notification. " "Error: %s", api_error + "Could not send CiscoWebexTeams notification. Error: %s", api_error ) diff --git a/homeassistant/components/citybikes/sensor.py b/homeassistant/components/citybikes/sensor.py index cb2647487ea..8e0b883b726 100644 --- a/homeassistant/components/citybikes/sensor.py +++ b/homeassistant/components/citybikes/sensor.py @@ -57,7 +57,7 @@ SCAN_INTERVAL = timedelta(minutes=5) # Timely, and doesn't suffocate the API STATIONS_URI = "v2/networks/{uid}?fields=network.stations" CITYBIKES_ATTRIBUTION = ( - "Information provided by the CityBikes Project " "(https://citybik.es/#about)" + "Information provided by the CityBikes Project (https://citybik.es/#about)" ) CITYBIKES_NETWORKS = "citybikes_networks" @@ -143,9 +143,7 @@ async def async_citybikes_request(hass, uri, schema): except ValueError: _LOGGER.error("Received non-JSON data from CityBikes API endpoint") except vol.Invalid as err: - _LOGGER.error( - "Received unexpected JSON from CityBikes" " API endpoint: %s", err - ) + _LOGGER.error("Received unexpected JSON from CityBikes API endpoint: %s", err) raise CityBikesRequestError diff --git a/homeassistant/components/clementine/media_player.py b/homeassistant/components/clementine/media_player.py index 37ed97915c7..9e05b831359 100644 --- a/homeassistant/components/clementine/media_player.py +++ b/homeassistant/components/clementine/media_player.py @@ -3,9 +3,10 @@ from datetime import timedelta import logging import time +from clementineremote import ClementineRemote import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, @@ -56,7 +57,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Clementine platform.""" - from clementineremote import ClementineRemote host = config.get(CONF_HOST) port = config.get(CONF_PORT) diff --git a/homeassistant/components/clickatell/notify.py b/homeassistant/components/clickatell/notify.py index 26f2f30aeff..d59a553a4f6 100644 --- a/homeassistant/components/clickatell/notify.py +++ b/homeassistant/components/clickatell/notify.py @@ -4,11 +4,10 @@ import logging import requests import voluptuous as vol +from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService from homeassistant.const import CONF_API_KEY, CONF_RECIPIENT import homeassistant.helpers.config_validation as cv -from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService - _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "clickatell" diff --git a/homeassistant/components/clicksend/notify.py b/homeassistant/components/clicksend/notify.py index 87fc217ac42..42136e9a09c 100644 --- a/homeassistant/components/clicksend/notify.py +++ b/homeassistant/components/clicksend/notify.py @@ -6,6 +6,7 @@ from aiohttp.hdrs import CONTENT_TYPE import requests import voluptuous as vol +from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService from homeassistant.const import ( CONF_API_KEY, CONF_RECIPIENT, @@ -15,8 +16,6 @@ from homeassistant.const import ( ) import homeassistant.helpers.config_validation as cv -from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService - _LOGGER = logging.getLogger(__name__) BASE_API_URL = "https://rest.clicksend.com/v3" diff --git a/homeassistant/components/clicksend_tts/notify.py b/homeassistant/components/clicksend_tts/notify.py index ba30c61e937..400e72a7d0c 100644 --- a/homeassistant/components/clicksend_tts/notify.py +++ b/homeassistant/components/clicksend_tts/notify.py @@ -6,6 +6,7 @@ from aiohttp.hdrs import CONTENT_TYPE import requests import voluptuous as vol +from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService from homeassistant.const import ( CONF_API_KEY, CONF_RECIPIENT, @@ -14,8 +15,6 @@ from homeassistant.const import ( ) import homeassistant.helpers.config_validation as cv -from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService - _LOGGER = logging.getLogger(__name__) BASE_API_URL = "https://rest.clicksend.com/v3" diff --git a/homeassistant/components/climate/.translations/bg.json b/homeassistant/components/climate/.translations/bg.json new file mode 100644 index 00000000000..ac1b05b096a --- /dev/null +++ b/homeassistant/components/climate/.translations/bg.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "set_hvac_mode": "\u041f\u0440\u043e\u043c\u044f\u043d\u0430 \u043d\u0430 \u0440\u0435\u0436\u0438\u043c \u043d\u0430 \u041e\u0412\u041a \u043d\u0430 {entity_name}", + "set_preset_mode": "\u041f\u0440\u043e\u043c\u0435\u043d\u0438 \u043f\u0440\u0435\u0434\u0432\u0430\u0440\u0438\u0442\u0435\u043b\u043d\u043e \u0437\u0430\u0434\u0430\u0434\u0435\u043d \u0440\u0435\u0436\u0438\u043c \u043d\u0430 {entity_name}" + }, + "condition_type": { + "is_hvac_mode": "{entity_name} \u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d \u043d\u0430 \u0441\u043f\u0435\u0446\u0438\u0444\u0438\u0447\u0435\u043d \u041e\u0412\u041a \u0440\u0435\u0436\u0438\u043c", + "is_preset_mode": "{entity_name} \u0435 \u0432 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d \u043f\u0440\u0435\u0434\u0432\u0430\u0440\u0438\u0442\u0435\u043b\u043d\u043e \u0437\u0430\u0434\u0430\u0434\u0435\u043d \u0440\u0435\u0436\u0438\u043c" + }, + "trigger_type": { + "current_humidity_changed": "{entity_name} \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0430\u0442\u0430 \u0432\u043b\u0430\u0436\u043d\u043e\u0441\u0442 \u0441\u0435 \u043f\u0440\u043e\u043c\u0435\u043d\u0438", + "current_temperature_changed": "{entity_name} \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0430\u0442\u0430 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 \u0441\u0435 \u043f\u0440\u043e\u043c\u0435\u043d\u0438", + "hvac_mode_changed": "{entity_name} \u0420\u0435\u0436\u0438\u043c \u043d\u0430 \u041e\u0412\u041a \u0441\u0435 \u043f\u0440\u043e\u043c\u0435\u043d\u0438" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climate/.translations/ca.json b/homeassistant/components/climate/.translations/ca.json index 743729041ab..bde91c26b7e 100644 --- a/homeassistant/components/climate/.translations/ca.json +++ b/homeassistant/components/climate/.translations/ca.json @@ -4,7 +4,7 @@ "set_hvac_mode": "Canvia el mode HVAC de {entity_name}", "set_preset_mode": "Canvia la configuraci\u00f3 preestablerta de {entity_name}" }, - "condtion_type": { + "condition_type": { "is_hvac_mode": "{entity_name} est\u00e0 configurat/ada en un mode HVAC espec\u00edfic", "is_preset_mode": "{entity_name} est\u00e0 configurat/ada en un mode preestablert espec\u00edfic" }, diff --git a/homeassistant/components/climate/.translations/da.json b/homeassistant/components/climate/.translations/da.json new file mode 100644 index 00000000000..78731dd1577 --- /dev/null +++ b/homeassistant/components/climate/.translations/da.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "set_hvac_mode": "Skift af klimaanl\u00e6gstilstand p\u00e5 {entity_name}", + "set_preset_mode": "Skift af forudindstilling p\u00e5 {entity_name}" + }, + "condition_type": { + "is_hvac_mode": "{entity_name} er indstillet til en bestemt klimaanl\u00e6gstilstand", + "is_preset_mode": "{entity_name} er indstillet til en bestemt forudindstillet tilstand" + }, + "trigger_type": { + "current_humidity_changed": "{entity_name} m\u00e5lte luftfugtighed \u00e6ndret", + "current_temperature_changed": "{entity_name} m\u00e5lte temperatur \u00e6ndret", + "hvac_mode_changed": "{entity_name} klimaanl\u00e6gstilstand \u00e6ndret" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climate/.translations/de.json b/homeassistant/components/climate/.translations/de.json new file mode 100644 index 00000000000..75ffe328fc8 --- /dev/null +++ b/homeassistant/components/climate/.translations/de.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "trigger_type": { + "current_humidity_changed": "Gemessene Luftfeuchtigkeit von {entity_name} ge\u00e4ndert", + "current_temperature_changed": "Gemessene Temperatur von {entity_name} ge\u00e4ndert" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climate/.translations/en.json b/homeassistant/components/climate/.translations/en.json index 942d9a2761f..2a56426e988 100644 --- a/homeassistant/components/climate/.translations/en.json +++ b/homeassistant/components/climate/.translations/en.json @@ -4,7 +4,7 @@ "set_hvac_mode": "Change HVAC mode on {entity_name}", "set_preset_mode": "Change preset on {entity_name}" }, - "condtion_type": { + "condition_type": { "is_hvac_mode": "{entity_name} is set to a specific HVAC mode", "is_preset_mode": "{entity_name} is set to a specific preset mode" }, diff --git a/homeassistant/components/climate/.translations/es.json b/homeassistant/components/climate/.translations/es.json index baae9b97436..e873427e694 100644 --- a/homeassistant/components/climate/.translations/es.json +++ b/homeassistant/components/climate/.translations/es.json @@ -4,7 +4,7 @@ "set_hvac_mode": "Cambiar el modo HVAC de {entity_name}.", "set_preset_mode": "Cambiar la configuraci\u00f3n prefijada de {entity_name}" }, - "condtion_type": { + "condition_type": { "is_hvac_mode": "{entity_name} est\u00e1 configurado en un modo HVAC espec\u00edfico", "is_preset_mode": "{entity_name} se establece en un modo predeterminado espec\u00edfico" }, diff --git a/homeassistant/components/climate/.translations/fr.json b/homeassistant/components/climate/.translations/fr.json index d82a3644493..0358a60f180 100644 --- a/homeassistant/components/climate/.translations/fr.json +++ b/homeassistant/components/climate/.translations/fr.json @@ -1,8 +1,13 @@ { "device_automation": { "action_type": { + "set_hvac_mode": "Changer le mode HVAC sur {entity_name}.", "set_preset_mode": "Changer les pr\u00e9r\u00e9glages de {entity_name}" }, + "condition_type": { + "is_hvac_mode": "{entity_name} est d\u00e9fini sur un mode HVAC sp\u00e9cifique", + "is_preset_mode": "{entity_name} est d\u00e9fini sur un mode pr\u00e9d\u00e9fini sp\u00e9cifique" + }, "trigger_type": { "current_humidity_changed": "Changement d'humidit\u00e9 mesur\u00e9e pour {entity_name}", "current_temperature_changed": "Changement de temp\u00e9rature mesur\u00e9e pour {entity_name}", diff --git a/homeassistant/components/climate/.translations/it.json b/homeassistant/components/climate/.translations/it.json index 34ecbf5e9f2..25a09b7d66d 100644 --- a/homeassistant/components/climate/.translations/it.json +++ b/homeassistant/components/climate/.translations/it.json @@ -4,7 +4,7 @@ "set_hvac_mode": "Cambia modalit\u00e0 HVAC su {entity_name}", "set_preset_mode": "Modifica preimpostazione su {entity_name}" }, - "condtion_type": { + "condition_type": { "is_hvac_mode": "{entity_name} \u00e8 impostato su una modalit\u00e0 HVAC specifica", "is_preset_mode": "{entity_name} \u00e8 impostato su una modalit\u00e0 preimpostata specifica" }, diff --git a/homeassistant/components/climate/.translations/ko.json b/homeassistant/components/climate/.translations/ko.json new file mode 100644 index 00000000000..299172958e8 --- /dev/null +++ b/homeassistant/components/climate/.translations/ko.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "set_hvac_mode": "{entity_name} \uc758 HVAC \ubaa8\ub4dc \ubcc0\uacbd", + "set_preset_mode": "{entity_name} \uc758 \uc0ac\uc804 \uc124\uc815 \ubcc0\uacbd" + }, + "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 \uc0ac\uc804 \uc124\uc815 \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" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climate/.translations/lb.json b/homeassistant/components/climate/.translations/lb.json index 72ab7efc623..cfb49f29f05 100644 --- a/homeassistant/components/climate/.translations/lb.json +++ b/homeassistant/components/climate/.translations/lb.json @@ -4,7 +4,7 @@ "set_hvac_mode": "HVAC Modus \u00e4nnere fir {entity_name}", "set_preset_mode": "Preset \u00e4nnere fir {entity_name}" }, - "condtion_type": { + "condition_type": { "is_hvac_mode": "\n{entity_name} ass op e spezifesche HVAC Modus gesat", "is_preset_mode": "{entity_name} ass op e spezifesche preset Modus gesat" }, diff --git a/homeassistant/components/climate/.translations/nl.json b/homeassistant/components/climate/.translations/nl.json new file mode 100644 index 00000000000..87e16c1c885 --- /dev/null +++ b/homeassistant/components/climate/.translations/nl.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "set_hvac_mode": "Wijzig de HVAC-modus op {entity_name}", + "set_preset_mode": "Wijzig voorinstelling op {entity_name}" + }, + "condition_type": { + "is_hvac_mode": "{entity_name} is ingesteld op een specifieke HVAC-modus", + "is_preset_mode": "{entity_name} is ingesteld op een specifieke vooraf ingestelde modus" + }, + "trigger_type": { + "current_humidity_changed": "{entity_name} gemeten vochtigheid veranderd", + "current_temperature_changed": "{entity_name} gemeten temperatuur veranderd", + "hvac_mode_changed": "{entity_name} HVAC-modus gewijzigd" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climate/.translations/no.json b/homeassistant/components/climate/.translations/no.json index 2d95c63a6ae..bc6e97b9aa5 100644 --- a/homeassistant/components/climate/.translations/no.json +++ b/homeassistant/components/climate/.translations/no.json @@ -4,7 +4,7 @@ "set_hvac_mode": "Endre HVAC-modus p\u00e5 {entity_name}", "set_preset_mode": "Endre forh\u00e5ndsinnstilling p\u00e5 {entity_name}" }, - "condtion_type": { + "condition_type": { "is_hvac_mode": "{entity_name} er satt til en spesifikk HVAC-modus", "is_preset_mode": "{entity_name} er satt til en spesifikk forh\u00e5ndsinnstilt modus" }, diff --git a/homeassistant/components/climate/.translations/pl.json b/homeassistant/components/climate/.translations/pl.json index c5b0c483ca9..f2a09eee3ef 100644 --- a/homeassistant/components/climate/.translations/pl.json +++ b/homeassistant/components/climate/.translations/pl.json @@ -4,7 +4,7 @@ "set_hvac_mode": "zmie\u0144 tryb HVAC na {entity_name}", "set_preset_mode": "zmie\u0144 ustawienia dla {entity_name}" }, - "condtion_type": { + "condition_type": { "is_hvac_mode": "na {entity_name} jest ustawiony okre\u015blony tryb HVAC", "is_preset_mode": "na {entity_name} jest okre\u015blone ustawienie" }, diff --git a/homeassistant/components/climate/.translations/ru.json b/homeassistant/components/climate/.translations/ru.json index 045f96137d2..6a9c52be209 100644 --- a/homeassistant/components/climate/.translations/ru.json +++ b/homeassistant/components/climate/.translations/ru.json @@ -4,7 +4,7 @@ "set_hvac_mode": "\u0421\u043c\u0435\u043d\u0438\u0442\u044c \u0440\u0435\u0436\u0438\u043c \u0440\u0430\u0431\u043e\u0442\u044b \u043e\u0431\u044a\u0435\u043a\u0442\u0430 \"{entity_name}\"", "set_preset_mode": "\u0421\u043c\u0435\u043d\u0438\u0442\u044c \u043d\u0430\u0431\u043e\u0440 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a \u043e\u0431\u044a\u0435\u043a\u0442\u0430 \"{entity_name}\"" }, - "condtion_type": { + "condition_type": { "is_hvac_mode": "{entity_name} \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u0432 \u0437\u0430\u0434\u0430\u043d\u043d\u043e\u043c \u0440\u0435\u0436\u0438\u043c\u0435 \u0440\u0430\u0431\u043e\u0442\u044b", "is_preset_mode": "{entity_name} \u0432 \u0440\u0435\u0436\u0438\u043c\u0435 \u043f\u0440\u0435\u0434\u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u043e\u0433\u043e \u043d\u0430\u0431\u043e\u0440\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a" }, diff --git a/homeassistant/components/climate/.translations/sl.json b/homeassistant/components/climate/.translations/sl.json index 4ba4cb02a4b..ecaf24fed80 100644 --- a/homeassistant/components/climate/.translations/sl.json +++ b/homeassistant/components/climate/.translations/sl.json @@ -4,7 +4,7 @@ "set_hvac_mode": "Spremeni na\u010din HVAC na {entity_name}", "set_preset_mode": "Spremenite prednastavitev na {entity_name}" }, - "condtion_type": { + "condition_type": { "is_hvac_mode": "{entity_name} je nastavljen na dolo\u010den na\u010din HVAC", "is_preset_mode": "{entity_name} je nastavljen na dolo\u010den prednastavljeni na\u010din" }, diff --git a/homeassistant/components/climate/.translations/zh-Hant.json b/homeassistant/components/climate/.translations/zh-Hant.json index 1d39eecc056..17e6c955046 100644 --- a/homeassistant/components/climate/.translations/zh-Hant.json +++ b/homeassistant/components/climate/.translations/zh-Hant.json @@ -4,7 +4,7 @@ "set_hvac_mode": "\u8b8a\u66f4 {entity_name} HVAC \u6a21\u5f0f", "set_preset_mode": "\u8b8a\u66f4 {entity_name} \u8a2d\u5b9a\u6a21\u5f0f" }, - "condtion_type": { + "condition_type": { "is_hvac_mode": "{entity_name} \u8a2d\u5b9a\u70ba\u6307\u5b9a HVAC \u6a21\u5f0f", "is_preset_mode": "{entity_name} \u8a2d\u5b9a\u70ba\u6307\u5b9a\u8a2d\u5b9a\u6a21\u5f0f" }, diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index f0d350fc0ba..e2bf555cc49 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -1,4 +1,5 @@ """Provides functionality to interact with climate devices.""" +from abc import abstractmethod from datetime import timedelta import functools as ft import logging @@ -18,9 +19,9 @@ from homeassistant.const import ( ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa: F401 - ENTITY_SERVICE_SCHEMA, PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, + make_entity_service_schema, ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent @@ -67,8 +68,8 @@ from .const import ( SUPPORT_PRESET_MODE, SUPPORT_SWING_MODE, SUPPORT_TARGET_HUMIDITY, - SUPPORT_TARGET_TEMPERATURE_RANGE, SUPPORT_TARGET_TEMPERATURE, + SUPPORT_TARGET_TEMPERATURE_RANGE, ) DEFAULT_MIN_TEMP = 7 @@ -83,38 +84,19 @@ CONVERTIBLE_ATTRIBUTE = [ATTR_TEMPERATURE, ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEM _LOGGER = logging.getLogger(__name__) -SET_AUX_HEAT_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_AUX_HEAT): cv.boolean} -) -SET_TEMPERATURE_SCHEMA = vol.Schema( - vol.All( - cv.has_at_least_one_key( - ATTR_TEMPERATURE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW - ), - ENTITY_SERVICE_SCHEMA.extend( - { - vol.Exclusive(ATTR_TEMPERATURE, "temperature"): vol.Coerce(float), - vol.Inclusive(ATTR_TARGET_TEMP_HIGH, "temperature"): vol.Coerce(float), - vol.Inclusive(ATTR_TARGET_TEMP_LOW, "temperature"): vol.Coerce(float), - vol.Optional(ATTR_HVAC_MODE): vol.In(HVAC_MODES), - } - ), - ) -) -SET_FAN_MODE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_FAN_MODE): cv.string} -) -SET_PRESET_MODE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_PRESET_MODE): cv.string} -) -SET_HVAC_MODE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_HVAC_MODE): vol.In(HVAC_MODES)} -) -SET_HUMIDITY_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_HUMIDITY): vol.Coerce(float)} -) -SET_SWING_MODE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_SWING_MODE): cv.string} + +SET_TEMPERATURE_SCHEMA = vol.All( + cv.has_at_least_one_key( + ATTR_TEMPERATURE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW + ), + make_entity_service_schema( + { + vol.Exclusive(ATTR_TEMPERATURE, "temperature"): vol.Coerce(float), + vol.Inclusive(ATTR_TARGET_TEMP_HIGH, "temperature"): vol.Coerce(float), + vol.Inclusive(ATTR_TARGET_TEMP_LOW, "temperature"): vol.Coerce(float), + vol.Optional(ATTR_HVAC_MODE): vol.In(HVAC_MODES), + } + ), ) @@ -125,32 +107,40 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: ) await component.async_setup(config) + component.async_register_entity_service(SERVICE_TURN_ON, {}, "async_turn_on") + component.async_register_entity_service(SERVICE_TURN_OFF, {}, "async_turn_off") component.async_register_entity_service( - SERVICE_TURN_ON, ENTITY_SERVICE_SCHEMA, "async_turn_on" + SERVICE_SET_HVAC_MODE, + {vol.Required(ATTR_HVAC_MODE): vol.In(HVAC_MODES)}, + "async_set_hvac_mode", ) component.async_register_entity_service( - SERVICE_TURN_OFF, ENTITY_SERVICE_SCHEMA, "async_turn_off" + SERVICE_SET_PRESET_MODE, + {vol.Required(ATTR_PRESET_MODE): cv.string}, + "async_set_preset_mode", ) component.async_register_entity_service( - SERVICE_SET_HVAC_MODE, SET_HVAC_MODE_SCHEMA, "async_set_hvac_mode" + SERVICE_SET_AUX_HEAT, + {vol.Required(ATTR_AUX_HEAT): cv.boolean}, + async_service_aux_heat, ) component.async_register_entity_service( - SERVICE_SET_PRESET_MODE, SET_PRESET_MODE_SCHEMA, "async_set_preset_mode" + SERVICE_SET_TEMPERATURE, SET_TEMPERATURE_SCHEMA, async_service_temperature_set, ) component.async_register_entity_service( - SERVICE_SET_AUX_HEAT, SET_AUX_HEAT_SCHEMA, async_service_aux_heat + SERVICE_SET_HUMIDITY, + {vol.Required(ATTR_HUMIDITY): vol.Coerce(float)}, + "async_set_humidity", ) component.async_register_entity_service( - SERVICE_SET_TEMPERATURE, SET_TEMPERATURE_SCHEMA, async_service_temperature_set + SERVICE_SET_FAN_MODE, + {vol.Required(ATTR_FAN_MODE): cv.string}, + "async_set_fan_mode", ) component.async_register_entity_service( - SERVICE_SET_HUMIDITY, SET_HUMIDITY_SCHEMA, "async_set_humidity" - ) - component.async_register_entity_service( - SERVICE_SET_FAN_MODE, SET_FAN_MODE_SCHEMA, "async_set_fan_mode" - ) - component.async_register_entity_service( - SERVICE_SET_SWING_MODE, SET_SWING_MODE_SCHEMA, "async_set_swing_mode" + SERVICE_SET_SWING_MODE, + {vol.Required(ATTR_SWING_MODE): cv.string}, + "async_set_swing_mode", ) return True @@ -270,20 +260,20 @@ class ClimateDevice(Entity): return None @property + @abstractmethod def hvac_mode(self) -> str: """Return hvac operation ie. heat, cool mode. Need to be one of HVAC_MODE_*. """ - raise NotImplementedError() @property + @abstractmethod def hvac_modes(self) -> List[str]: """Return the list of available hvac operation modes. Need to be a subset of HVAC_MODES. """ - raise NotImplementedError() @property def hvac_action(self) -> Optional[str]: diff --git a/homeassistant/components/climate/device_action.py b/homeassistant/components/climate/device_action.py index b53109f69cb..6f7725ac835 100644 --- a/homeassistant/components/climate/device_action.py +++ b/homeassistant/components/climate/device_action.py @@ -1,17 +1,19 @@ """Provides device automations for Climate.""" -from typing import Optional, List +from typing import List, Optional + import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, - CONF_DOMAIN, - CONF_TYPE, CONF_DEVICE_ID, + CONF_DOMAIN, CONF_ENTITY_ID, + CONF_TYPE, ) -from homeassistant.core import HomeAssistant, Context +from homeassistant.core import Context, HomeAssistant from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv + from . import DOMAIN, const ACTION_TYPES = {"set_hvac_mode", "set_preset_mode"} @@ -59,14 +61,15 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: CONF_TYPE: "set_hvac_mode", } ) - actions.append( - { - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "set_preset_mode", - } - ) + if state.attributes["supported_features"] & const.SUPPORT_PRESET_MODE: + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "set_preset_mode", + } + ) return actions diff --git a/homeassistant/components/climate/device_condition.py b/homeassistant/components/climate/device_condition.py index c923f3123f1..cf393a035ec 100644 --- a/homeassistant/components/climate/device_condition.py +++ b/homeassistant/components/climate/device_condition.py @@ -1,19 +1,21 @@ """Provide the device automations for Climate.""" from typing import Dict, List + import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, CONF_CONDITION, - CONF_DOMAIN, - CONF_TYPE, CONF_DEVICE_ID, + CONF_DOMAIN, CONF_ENTITY_ID, + CONF_TYPE, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import condition, config_validation as cv, entity_registry -from homeassistant.helpers.typing import ConfigType, TemplateVarsType from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA +from homeassistant.helpers.typing import ConfigType, TemplateVarsType + from . import DOMAIN, const CONDITION_TYPES = {"is_hvac_mode", "is_preset_mode"} @@ -61,7 +63,7 @@ async def async_get_conditions( } ) - if state and const.ATTR_PRESET_MODES in state.attributes: + if state and state.attributes["supported_features"] & const.SUPPORT_PRESET_MODE: conditions.append( { CONF_CONDITION: "device", diff --git a/homeassistant/components/climate/device_trigger.py b/homeassistant/components/climate/device_trigger.py index e814bdc88de..4c5dcb0ee04 100644 --- a/homeassistant/components/climate/device_trigger.py +++ b/homeassistant/components/climate/device_trigger.py @@ -1,26 +1,28 @@ """Provides device automations for Climate.""" from typing import List + import voluptuous as vol -from homeassistant.const import ( - CONF_DOMAIN, - CONF_TYPE, - CONF_PLATFORM, - CONF_DEVICE_ID, - CONF_ENTITY_ID, - CONF_FOR, - CONF_ABOVE, - CONF_BELOW, -) -from homeassistant.core import HomeAssistant, CALLBACK_TYPE -from homeassistant.helpers import config_validation as cv, entity_registry -from homeassistant.helpers.typing import ConfigType from homeassistant.components.automation import ( - state as state_automation, - numeric_state as numeric_state_automation, AutomationActionType, + numeric_state as numeric_state_automation, + state as state_automation, ) from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from homeassistant.const import ( + CONF_ABOVE, + CONF_BELOW, + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_ENTITY_ID, + CONF_FOR, + 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, const TRIGGER_TYPES = { diff --git a/homeassistant/components/climate/reproduce_state.py b/homeassistant/components/climate/reproduce_state.py index 34e72a27c92..82ca4f4e85c 100644 --- a/homeassistant/components/climate/reproduce_state.py +++ b/homeassistant/components/climate/reproduce_state.py @@ -8,20 +8,20 @@ from homeassistant.helpers.typing import HomeAssistantType from .const import ( ATTR_AUX_HEAT, + ATTR_HUMIDITY, + ATTR_HVAC_MODE, + ATTR_PRESET_MODE, + ATTR_SWING_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, - ATTR_PRESET_MODE, - ATTR_HVAC_MODE, - ATTR_SWING_MODE, - ATTR_HUMIDITY, + DOMAIN, HVAC_MODES, SERVICE_SET_AUX_HEAT, - SERVICE_SET_TEMPERATURE, - SERVICE_SET_PRESET_MODE, - SERVICE_SET_HVAC_MODE, - SERVICE_SET_SWING_MODE, SERVICE_SET_HUMIDITY, - DOMAIN, + SERVICE_SET_HVAC_MODE, + SERVICE_SET_PRESET_MODE, + SERVICE_SET_SWING_MODE, + SERVICE_SET_TEMPERATURE, ) diff --git a/homeassistant/components/climate/services.yaml b/homeassistant/components/climate/services.yaml index f10e1b4bd69..34e89d57346 100644 --- a/homeassistant/components/climate/services.yaml +++ b/homeassistant/components/climate/services.yaml @@ -9,6 +9,7 @@ set_aux_heat: aux_heat: description: New value of axillary heater. example: true + set_preset_mode: description: Set preset mode for climate device. fields: @@ -18,6 +19,7 @@ set_preset_mode: preset_mode: description: New value of preset mode example: 'away' + set_temperature: description: Set target temperature of climate device. fields: @@ -36,6 +38,7 @@ set_temperature: hvac_mode: description: HVAC operation mode to set temperature to. example: 'heat' + set_humidity: description: Set target humidity of climate device. fields: @@ -45,6 +48,7 @@ set_humidity: humidity: description: New target humidity for climate device. example: 60 + set_fan_mode: description: Set fan operation for climate device. fields: @@ -54,6 +58,7 @@ set_fan_mode: fan_mode: description: New value of fan mode. example: On Low + set_hvac_mode: description: Set HVAC operation mode for climate device. fields: @@ -63,6 +68,7 @@ set_hvac_mode: hvac_mode: description: New value of operation mode. example: heat + set_swing_mode: description: Set swing operation for climate device. fields: @@ -72,29 +78,6 @@ set_swing_mode: swing_mode: description: New value of swing mode. -mill_set_room_temperature: - description: Set Mill room temperatures. - fields: - room_name: - description: Name of room to change. - example: 'kitchen' - away_temp: - description: Away temp. - example: 12 - comfort_temp: - description: Comfort temp. - example: 22 - sleep_temp: - description: Sleep temp. - example: 17 - -nuheat_resume_program: - description: Resume the programmed schedule. - fields: - entity_id: - description: Name(s) of entities to change. - example: 'climate.kitchen' - turn_on: description: Turn climate device on. fields: diff --git a/homeassistant/components/climate/strings.json b/homeassistant/components/climate/strings.json index a2ceeff2143..ff071aed083 100644 --- a/homeassistant/components/climate/strings.json +++ b/homeassistant/components/climate/strings.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_hvac_mode": "{entity_name} is set to a specific HVAC mode", "is_preset_mode": "{entity_name} is set to a specific preset mode" }, diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index 763f6214185..6d9b70051f5 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -4,7 +4,6 @@ import logging from hass_nabucasa import Cloud import voluptuous as vol -from homeassistant.auth.const import GROUP_ID_ADMIN from homeassistant.components.alexa import const as alexa_const from homeassistant.components.google_assistant import const as ga_c from homeassistant.const import ( @@ -186,19 +185,6 @@ async def async_setup(hass, config): prefs = CloudPreferences(hass) await prefs.async_initialize() - # Cloud user - user = None - if prefs.cloud_user: - # Fetch the user. It can happen that the user no longer exists if - # an image was restored without restoring the cloud prefs. - user = await hass.auth.async_get_user(prefs.cloud_user) - - if user is None: - user = await hass.auth.async_create_system_user( - "Home Assistant Cloud", [GROUP_ID_ADMIN] - ) - await prefs.async_update(cloud_user=user.id) - # Initialize Cloud websession = hass.helpers.aiohttp_client.async_get_clientsession() client = CloudClient(hass, prefs, websession, alexa_conf, google_conf) diff --git a/homeassistant/components/cloud/account_link.py b/homeassistant/components/cloud/account_link.py index 9ec1fe634d7..1d0de26918d 100644 --- a/homeassistant/components/cloud/account_link.py +++ b/homeassistant/components/cloud/account_link.py @@ -7,7 +7,7 @@ from hass_nabucasa import account_link from homeassistant.const import MAJOR_VERSION, MINOR_VERSION, PATCH_VERSION from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import event, config_entry_oauth2_flow +from homeassistant.helpers import config_entry_oauth2_flow, event from .const import DOMAIN diff --git a/homeassistant/components/cloud/alexa_config.py b/homeassistant/components/cloud/alexa_config.py index a1432f196bf..45e1fab1101 100644 --- a/homeassistant/components/cloud/alexa_config.py +++ b/homeassistant/components/cloud/alexa_config.py @@ -7,24 +7,23 @@ import aiohttp import async_timeout from hass_nabucasa import cloud_api -from homeassistant.core import callback +from homeassistant.components.alexa import ( + config as alexa_config, + entities as alexa_entities, + errors as alexa_errors, + state_report as alexa_state_report, +) from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES +from homeassistant.core import callback from homeassistant.helpers import entity_registry from homeassistant.helpers.event import async_call_later from homeassistant.util.dt import utcnow -from homeassistant.components.alexa import ( - config as alexa_config, - errors as alexa_errors, - entities as alexa_entities, - state_report as alexa_state_report, -) - from .const import ( CONF_ENTITY_CONFIG, CONF_FILTER, - PREF_SHOULD_EXPOSE, DEFAULT_SHOULD_EXPOSE, + PREF_SHOULD_EXPOSE, RequireRelink, ) diff --git a/homeassistant/components/cloud/binary_sensor.py b/homeassistant/components/cloud/binary_sensor.py index 2192eec8923..056105f8071 100644 --- a/homeassistant/components/cloud/binary_sensor.py +++ b/homeassistant/components/cloud/binary_sensor.py @@ -6,7 +6,6 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import DISPATCHER_REMOTE_UPDATE, DOMAIN - WAIT_UNTIL_CHANGE = 3 diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index c7626777943..24947ed7952 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -1,27 +1,26 @@ """Interface implementation for cloud client.""" import asyncio +import logging from pathlib import Path from typing import Any, Dict -import logging import aiohttp from hass_nabucasa.client import CloudClient as Interface -from homeassistant.core import callback -from homeassistant.components.google_assistant import smart_home as ga -from homeassistant.helpers.typing import HomeAssistantType -from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.util.aiohttp import MockRequest from homeassistant.components.alexa import ( - smart_home as alexa_sh, errors as alexa_errors, + smart_home as alexa_sh, ) +from homeassistant.components.google_assistant import smart_home as ga +from homeassistant.core import Context, callback +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.util.aiohttp import MockRequest -from . import utils, alexa_config, google_config +from . import alexa_config, google_config, utils from .const import DISPATCHER_REMOTE_UPDATE from .prefs import CloudPreferences - _LOGGER = logging.getLogger(__name__) @@ -44,7 +43,6 @@ class CloudClient(Interface): self.alexa_user_config = alexa_user_config self._alexa_config = None self._google_config = None - self.cloud = None @property def base_path(self) -> Path: @@ -92,23 +90,23 @@ class CloudClient(Interface): return self._alexa_config - @property - def google_config(self) -> google_config.CloudGoogleConfig: + async def get_google_config(self) -> google_config.CloudGoogleConfig: """Return Google config.""" if not self._google_config: assert self.cloud is not None + + cloud_user = await self._prefs.get_cloud_user() + self._google_config = google_config.CloudGoogleConfig( - self._hass, self.google_user_config, self._prefs, self.cloud + self._hass, self.google_user_config, cloud_user, self._prefs, self.cloud ) + await self._google_config.async_initialize() return self._google_config - async def async_initialize(self, cloud) -> None: - """Initialize the client.""" - self.cloud = cloud - - if not self.cloud.is_logged_in: - return + async def logged_in(self) -> None: + """When user logs in.""" + await self.prefs.async_set_username(self.cloud.username) if self.alexa_config.enabled and self.alexa_config.should_report_state: try: @@ -116,14 +114,18 @@ class CloudClient(Interface): except alexa_errors.NoTokenAvailable: pass - if self.google_config.enabled: - self.google_config.async_enable_local_sdk() + if self._prefs.google_enabled: + gconf = await self.get_google_config() - if self.google_config.should_report_state: - self.google_config.async_enable_report_state() + gconf.async_enable_local_sdk() + + if gconf.should_report_state: + gconf.async_enable_report_state() async def cleanups(self) -> None: """Cleanup some stuff after logout.""" + await self.prefs.async_set_username(None) + self._google_config = None @callback @@ -141,8 +143,13 @@ class CloudClient(Interface): 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() return await alexa_sh.async_handle_message( - self._hass, self.alexa_config, payload, enabled=self._prefs.alexa_enabled + self._hass, + self.alexa_config, + payload, + context=Context(user_id=cloud_user), + enabled=self._prefs.alexa_enabled, ) async def async_google_message(self, payload: Dict[Any, Any]) -> Dict[Any, Any]: @@ -150,8 +157,10 @@ class CloudClient(Interface): if not self._prefs.google_enabled: return ga.turned_off_response(payload) + gconf = await self.get_google_config() + return await ga.async_handle_message( - self._hass, self.google_config, self.prefs.cloud_user, payload + self._hass, gconf, gconf.cloud_user, payload ) async def async_webhook_message(self, payload: Dict[Any, Any]) -> Dict[Any, Any]: diff --git a/homeassistant/components/cloud/const.py b/homeassistant/components/cloud/const.py index 9a2dccf8d7c..406263c85f8 100644 --- a/homeassistant/components/cloud/const.py +++ b/homeassistant/components/cloud/const.py @@ -17,6 +17,7 @@ PREF_DISABLE_2FA = "disable_2fa" PREF_ALIASES = "aliases" PREF_SHOULD_EXPOSE = "should_expose" PREF_GOOGLE_LOCAL_WEBHOOK_ID = "google_local_webhook_id" +PREF_USERNAME = "username" DEFAULT_SHOULD_EXPOSE = True DEFAULT_DISABLE_2FA = False DEFAULT_ALEXA_REPORT_STATE = False diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index 582fa007550..1ff87bf95f5 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -5,16 +5,16 @@ import logging import async_timeout from hass_nabucasa.google_report_state import ErrorResponse -from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES from homeassistant.components.google_assistant.helpers import AbstractConfig +from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES from homeassistant.helpers import entity_registry from .const import ( - PREF_SHOULD_EXPOSE, - DEFAULT_SHOULD_EXPOSE, CONF_ENTITY_CONFIG, - PREF_DISABLE_2FA, DEFAULT_DISABLE_2FA, + DEFAULT_SHOULD_EXPOSE, + PREF_DISABLE_2FA, + PREF_SHOULD_EXPOSE, ) _LOGGER = logging.getLogger(__name__) @@ -23,10 +23,11 @@ _LOGGER = logging.getLogger(__name__) class CloudGoogleConfig(AbstractConfig): """HA Cloud Configuration for Google Assistant.""" - def __init__(self, hass, config, prefs, cloud): + def __init__(self, hass, config, cloud_user, prefs, cloud): """Initialize the Google config.""" super().__init__(hass) self._config = config + self._user = cloud_user self._prefs = prefs self._cloud = cloud self._cur_entity_prefs = self._prefs.google_entity_configs @@ -41,12 +42,7 @@ class CloudGoogleConfig(AbstractConfig): @property def enabled(self): """Return if Google is enabled.""" - return self._prefs.google_enabled - - @property - def agent_user_id(self): - """Return Agent User Id to use for query responses.""" - return self._cloud.claims["cognito:username"] + return self._cloud.is_logged_in and self._prefs.google_enabled @property def entity_config(self): @@ -61,7 +57,7 @@ class CloudGoogleConfig(AbstractConfig): @property def should_report_state(self): """Return if states should be proactively reported.""" - return self._prefs.google_report_state + return self._cloud.is_logged_in and self._prefs.google_report_state @property def local_sdk_webhook_id(self): @@ -74,7 +70,12 @@ class CloudGoogleConfig(AbstractConfig): @property def local_sdk_user_id(self): """Return the user ID to be used for actions received via the local SDK.""" - return self._prefs.cloud_user + return self._user + + @property + def cloud_user(self): + """Return Cloud User account.""" + return self._user def should_expose(self, state): """If a state object should be exposed.""" @@ -98,14 +99,14 @@ class CloudGoogleConfig(AbstractConfig): entity_config = entity_configs.get(state.entity_id, {}) return not entity_config.get(PREF_DISABLE_2FA, DEFAULT_DISABLE_2FA) - async def async_report_state(self, message): + async def async_report_state(self, message, agent_user_id: str): """Send a state report to Google.""" try: await self._cloud.google_report_state.async_send_message(message) except ErrorResponse as err: _LOGGER.warning("Error reporting state - %s: %s", err.code, err.message) - async def _async_request_sync_devices(self): + async def _async_request_sync_devices(self, agent_user_id: str): """Trigger a sync with Google.""" if self._sync_entities_lock.locked(): return 200 @@ -126,13 +127,6 @@ class CloudGoogleConfig(AbstractConfig): _LOGGER.debug("Finished requesting syncing: %s", req.status) return req.status - async def async_deactivate_report_state(self): - """Turn off report state and disable further state reporting. - - Called when the user disconnects their account from Google. - """ - await self._prefs.async_update(google_report_state=False) - async def _async_prefs_updated(self, prefs): """Handle updated preferences.""" if self.should_report_state != self.is_reporting_state: @@ -143,7 +137,7 @@ class CloudGoogleConfig(AbstractConfig): # State reporting is reported as a property on entities. # So when we change it, we need to sync all entities. - await self.async_sync_entities() + await self.async_sync_entities_all() # If entity prefs are the same or we have filter in config.yaml, # don't sync. @@ -151,7 +145,7 @@ class CloudGoogleConfig(AbstractConfig): self._cur_entity_prefs is not prefs.google_entity_configs and self._config["filter"].empty_filter ): - self.async_schedule_google_sync() + self.async_schedule_google_sync_all() if self.enabled and not self.is_local_sdk_active: self.async_enable_local_sdk() @@ -167,4 +161,4 @@ class CloudGoogleConfig(AbstractConfig): # Schedule a sync if a change was made to an entity that Google knows about if self._should_expose_entity_id(entity_id): - await self.async_sync_entities() + await self.async_sync_entities_all() diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index d969612ce8e..b97feb7c6f4 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -174,7 +174,8 @@ class GoogleActionsSyncView(HomeAssistantView): """Trigger a Google Actions sync.""" hass = request.app["hass"] cloud: Cloud = hass.data[DOMAIN] - status = await cloud.client.google_config.async_sync_entities() + gconf = await cloud.client.get_google_config() + status = await gconf.async_sync_entities(gconf.cloud_user) return self.json({}, status_code=status) @@ -192,11 +193,7 @@ class CloudLoginView(HomeAssistantView): """Handle login request.""" hass = request.app["hass"] cloud = hass.data[DOMAIN] - - with async_timeout.timeout(REQUEST_TIMEOUT): - await hass.async_add_job(cloud.auth.login, data["email"], data["password"]) - - hass.async_add_job(cloud.iot.connect) + await cloud.login(data["email"], data["password"]) return self.json({"success": True}) @@ -477,7 +474,8 @@ async def websocket_remote_disconnect(hass, connection, msg): async def google_assistant_list(hass, connection, msg): """List all google assistant entities.""" cloud = hass.data[DOMAIN] - entities = google_helpers.async_get_entities(hass, cloud.client.google_config) + gconf = await cloud.client.get_google_config() + entities = google_helpers.async_get_entities(hass, gconf) result = [] @@ -585,7 +583,7 @@ async def alexa_sync(hass, connection, msg): connection.send_error( msg["id"], "alexa_relink", - "Please go to the Alexa app and re-link the Home Assistant " "skill.", + "Please go to the Alexa app and re-link the Home Assistant skill.", ) return diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index 2feef55835e..accc4a0c0f9 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -2,7 +2,8 @@ "domain": "cloud", "name": "Cloud", "documentation": "https://www.home-assistant.io/integrations/cloud", - "requirements": ["hass-nabucasa==0.29"], + "requirements": ["hass-nabucasa==0.30"], "dependencies": ["http", "webhook"], + "after_dependencies": ["alexa", "google_assistant"], "codeowners": ["@home-assistant/cloud"] } diff --git a/homeassistant/components/cloud/prefs.py b/homeassistant/components/cloud/prefs.py index 0599b00a8bd..a7d1b59fd39 100644 --- a/homeassistant/components/cloud/prefs.py +++ b/homeassistant/components/cloud/prefs.py @@ -1,28 +1,32 @@ """Preference management for cloud.""" from ipaddress import ip_address +from typing import Optional +from homeassistant.auth.const import GROUP_ID_ADMIN +from homeassistant.auth.models import User from homeassistant.core import callback from homeassistant.util.logging import async_create_catching_coro from .const import ( + DEFAULT_ALEXA_REPORT_STATE, + DEFAULT_GOOGLE_REPORT_STATE, DOMAIN, + PREF_ALEXA_ENTITY_CONFIGS, + PREF_ALEXA_REPORT_STATE, + PREF_ALIASES, + PREF_CLOUD_USER, + PREF_CLOUDHOOKS, + PREF_DISABLE_2FA, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE, PREF_ENABLE_REMOTE, - PREF_GOOGLE_SECURE_DEVICES_PIN, - PREF_CLOUDHOOKS, - PREF_CLOUD_USER, PREF_GOOGLE_ENTITY_CONFIGS, - PREF_OVERRIDE_NAME, - PREF_DISABLE_2FA, - PREF_ALIASES, - PREF_SHOULD_EXPOSE, - PREF_ALEXA_ENTITY_CONFIGS, - PREF_ALEXA_REPORT_STATE, - DEFAULT_ALEXA_REPORT_STATE, - PREF_GOOGLE_REPORT_STATE, PREF_GOOGLE_LOCAL_WEBHOOK_ID, - DEFAULT_GOOGLE_REPORT_STATE, + PREF_GOOGLE_REPORT_STATE, + PREF_GOOGLE_SECURE_DEVICES_PIN, + PREF_OVERRIDE_NAME, + PREF_SHOULD_EXPOSE, + PREF_USERNAME, InvalidTrustedNetworks, InvalidTrustedProxies, ) @@ -47,16 +51,7 @@ class CloudPreferences: prefs = await self._store.async_load() if prefs is None: - prefs = { - PREF_ENABLE_ALEXA: True, - PREF_ENABLE_GOOGLE: True, - PREF_ENABLE_REMOTE: False, - PREF_GOOGLE_SECURE_DEVICES_PIN: None, - PREF_GOOGLE_ENTITY_CONFIGS: {}, - PREF_ALEXA_ENTITY_CONFIGS: {}, - PREF_CLOUDHOOKS: {}, - PREF_CLOUD_USER: None, - } + prefs = self._empty_config("") self._prefs = prefs @@ -166,6 +161,27 @@ class CloudPreferences: updated_entities = {**entities, entity_id: updated_entity} await self.async_update(alexa_entity_configs=updated_entities) + async def async_set_username(self, username): + """Set the username that is logged in.""" + # Logging out. + if username is None: + user = await self._load_cloud_user() + + if user is not None: + await self._hass.auth.async_remove_user(user) + await self._save_prefs({**self._prefs, PREF_CLOUD_USER: None}) + return + + cur_username = self._prefs.get(PREF_USERNAME) + + if cur_username == username: + return + + if cur_username is None: + await self._save_prefs({**self._prefs, PREF_USERNAME: username}) + else: + await self._save_prefs(self._empty_config(username)) + def as_dict(self): """Return dictionary version.""" return { @@ -178,7 +194,6 @@ class CloudPreferences: PREF_ALEXA_REPORT_STATE: self.alexa_report_state, PREF_GOOGLE_REPORT_STATE: self.google_report_state, PREF_CLOUDHOOKS: self.cloudhooks, - PREF_CLOUD_USER: self.cloud_user, } @property @@ -239,10 +254,29 @@ class CloudPreferences: """Return the published cloud webhooks.""" return self._prefs.get(PREF_CLOUDHOOKS, {}) - @property - def cloud_user(self) -> str: + async def get_cloud_user(self) -> str: """Return ID from Home Assistant Cloud system user.""" - return self._prefs.get(PREF_CLOUD_USER) + user = await self._load_cloud_user() + + if user: + return user.id + + user = await self._hass.auth.async_create_system_user( + "Home Assistant Cloud", [GROUP_ID_ADMIN] + ) + await self.async_update(cloud_user=user.id) + return user.id + + async def _load_cloud_user(self) -> Optional[User]: + """Load cloud user if available.""" + user_id = self._prefs.get(PREF_CLOUD_USER) + + if user_id is None: + return None + + # Fetch the user. It can happen that the user no longer exists if + # an image was restored without restoring the cloud prefs. + return await self._hass.auth.async_get_user(user_id) @property def _has_local_trusted_network(self) -> bool: @@ -283,3 +317,19 @@ class CloudPreferences: for listener in self._listeners: self._hass.async_create_task(async_create_catching_coro(listener(self))) + + @callback + def _empty_config(self, username): + """Return an empty config.""" + return { + PREF_ENABLE_ALEXA: True, + PREF_ENABLE_GOOGLE: True, + PREF_ENABLE_REMOTE: False, + PREF_GOOGLE_SECURE_DEVICES_PIN: None, + PREF_GOOGLE_ENTITY_CONFIGS: {}, + PREF_ALEXA_ENTITY_CONFIGS: {}, + PREF_CLOUDHOOKS: {}, + PREF_CLOUD_USER: None, + PREF_USERNAME: username, + PREF_GOOGLE_LOCAL_WEBHOOK_ID: self._hass.components.webhook.async_generate_id(), + } diff --git a/homeassistant/components/cloud/tts.py b/homeassistant/components/cloud/tts.py index 338b97d2bd9..ea769c6a054 100644 --- a/homeassistant/components/cloud/tts.py +++ b/homeassistant/components/cloud/tts.py @@ -1,7 +1,7 @@ """Support for the cloud for text to speech service.""" -from hass_nabucasa.voice import VoiceError from hass_nabucasa import Cloud +from hass_nabucasa.voice import VoiceError import voluptuous as vol from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider diff --git a/homeassistant/components/cloud/utils.py b/homeassistant/components/cloud/utils.py index 5040baada9a..36599b42ad3 100644 --- a/homeassistant/components/cloud/utils.py +++ b/homeassistant/components/cloud/utils.py @@ -1,7 +1,7 @@ """Helper functions for cloud components.""" from typing import Any, Dict -from aiohttp import web, payload +from aiohttp import payload, web def aiohttp_serialize_response(response: web.Response) -> Dict[str, Any]: diff --git a/homeassistant/components/cloudflare/services.yaml b/homeassistant/components/cloudflare/services.yaml index e69de29bb2d..23ffdd14d5f 100644 --- a/homeassistant/components/cloudflare/services.yaml +++ b/homeassistant/components/cloudflare/services.yaml @@ -0,0 +1,2 @@ +update_records: + description: Manually trigger update to Cloudflare records. diff --git a/homeassistant/components/coinbase/__init__.py b/homeassistant/components/coinbase/__init__.py index 67869e6b88c..d52c0867e24 100644 --- a/homeassistant/components/coinbase/__init__.py +++ b/homeassistant/components/coinbase/__init__.py @@ -94,5 +94,5 @@ class CoinbaseData: self.exchange_rates = self.client.get_exchange_rates() except AuthenticationError as coinbase_error: _LOGGER.error( - "Authentication error connecting" " to coinbase: %s", coinbase_error + "Authentication error connecting to coinbase: %s", coinbase_error ) diff --git a/homeassistant/components/comfoconnect/__init__.py b/homeassistant/components/comfoconnect/__init__.py index efdbf020f1a..f1fd67cc4bb 100644 --- a/homeassistant/components/comfoconnect/__init__.py +++ b/homeassistant/components/comfoconnect/__init__.py @@ -1,12 +1,7 @@ """Support to control a Zehnder ComfoAir Q350/450/600 ventilation unit.""" import logging -from pycomfoconnect import ( - SENSOR_TEMPERATURE_EXTRACT, - SENSOR_TEMPERATURE_OUTDOOR, - Bridge, - ComfoConnect, -) +from pycomfoconnect import Bridge, ComfoConnect import voluptuous as vol from homeassistant.const import ( @@ -24,14 +19,7 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = "comfoconnect" -SIGNAL_COMFOCONNECT_UPDATE_RECEIVED = "comfoconnect_update_received" - -ATTR_CURRENT_TEMPERATURE = "current_temperature" -ATTR_CURRENT_HUMIDITY = "current_humidity" -ATTR_OUTSIDE_TEMPERATURE = "outside_temperature" -ATTR_OUTSIDE_HUMIDITY = "outside_humidity" -ATTR_AIR_FLOW_SUPPLY = "air_flow_supply" -ATTR_AIR_FLOW_EXHAUST = "air_flow_exhaust" +SIGNAL_COMFOCONNECT_UPDATE_RECEIVED = "comfoconnect_update_received_{}" CONF_USER_AGENT = "user_agent" @@ -105,6 +93,7 @@ class ComfoConnectBridge: self.data = {} self.name = name self.hass = hass + self.unique_id = bridge.uuid.hex() self.comfoconnect = ComfoConnect( bridge=bridge, @@ -125,13 +114,8 @@ class ComfoConnectBridge: self.comfoconnect.disconnect() def sensor_callback(self, var, value): - """Call function for sensor updates.""" - _LOGGER.debug("Got value from bridge: %d = %d", var, value) - - if var in [SENSOR_TEMPERATURE_EXTRACT, SENSOR_TEMPERATURE_OUTDOOR]: - self.data[var] = value / 10 - else: - self.data[var] = value - - # Notify listeners that we have received an update - dispatcher_send(self.hass, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, var) + """Notify listeners that we have received an update.""" + _LOGGER.debug("Received update for %s: %s", var, value) + dispatcher_send( + self.hass, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED.format(var), value + ) diff --git a/homeassistant/components/comfoconnect/fan.py b/homeassistant/components/comfoconnect/fan.py index 34e784d61eb..432b25ac602 100644 --- a/homeassistant/components/comfoconnect/fan.py +++ b/homeassistant/components/comfoconnect/fan.py @@ -43,24 +43,34 @@ class ComfoConnectFan(FanEntity): async def async_added_to_hass(self): """Register for sensor updates.""" + _LOGGER.debug("Registering for fan speed") + async_dispatcher_connect( + self.hass, + SIGNAL_COMFOCONNECT_UPDATE_RECEIVED.format(SENSOR_FAN_SPEED_MODE), + self._handle_update, + ) await self.hass.async_add_executor_job( self._ccb.comfoconnect.register_sensor, SENSOR_FAN_SPEED_MODE ) - async_dispatcher_connect( - self.hass, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, self._handle_update - ) - def _handle_update(self, var): + def _handle_update(self, value): """Handle update callbacks.""" - if var == SENSOR_FAN_SPEED_MODE: - _LOGGER.debug("Received update for %s", var) - self.schedule_update_ha_state() + _LOGGER.debug( + "Handle update for fan speed (%d): %s", SENSOR_FAN_SPEED_MODE, value + ) + self._ccb.data[SENSOR_FAN_SPEED_MODE] = value + self.schedule_update_ha_state() @property def should_poll(self) -> bool: """Do not poll.""" return False + @property + def unique_id(self): + """Return a unique_id for this entity.""" + return self._ccb.unique_id + @property def name(self): """Return the name of the fan.""" diff --git a/homeassistant/components/comfoconnect/sensor.py b/homeassistant/components/comfoconnect/sensor.py index a1f16ed9631..3e3507ea48d 100644 --- a/homeassistant/components/comfoconnect/sensor.py +++ b/homeassistant/components/comfoconnect/sensor.py @@ -2,93 +2,214 @@ import logging from pycomfoconnect import ( + SENSOR_BYPASS_STATE, + SENSOR_DAYS_TO_REPLACE_FILTER, + SENSOR_FAN_EXHAUST_DUTY, SENSOR_FAN_EXHAUST_FLOW, + SENSOR_FAN_EXHAUST_SPEED, + SENSOR_FAN_SUPPLY_DUTY, SENSOR_FAN_SUPPLY_FLOW, + SENSOR_FAN_SUPPLY_SPEED, + SENSOR_HUMIDITY_EXHAUST, SENSOR_HUMIDITY_EXTRACT, SENSOR_HUMIDITY_OUTDOOR, + SENSOR_HUMIDITY_SUPPLY, + SENSOR_POWER_CURRENT, + SENSOR_TEMPERATURE_EXHAUST, SENSOR_TEMPERATURE_EXTRACT, SENSOR_TEMPERATURE_OUTDOOR, + SENSOR_TEMPERATURE_SUPPLY, ) +import voluptuous as vol -from homeassistant.const import CONF_RESOURCES, TEMP_CELSIUS +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + CONF_RESOURCES, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_TEMPERATURE, + POWER_WATT, + TEMP_CELSIUS, +) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from . import ( - ATTR_AIR_FLOW_EXHAUST, - ATTR_AIR_FLOW_SUPPLY, - ATTR_CURRENT_HUMIDITY, - ATTR_CURRENT_TEMPERATURE, - ATTR_OUTSIDE_HUMIDITY, - ATTR_OUTSIDE_TEMPERATURE, - DOMAIN, - SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, - ComfoConnectBridge, -) +from . import DOMAIN, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, ComfoConnectBridge + +ATTR_AIR_FLOW_EXHAUST = "air_flow_exhaust" +ATTR_AIR_FLOW_SUPPLY = "air_flow_supply" +ATTR_BYPASS_STATE = "bypass_state" +ATTR_CURRENT_HUMIDITY = "current_humidity" +ATTR_CURRENT_TEMPERATURE = "current_temperature" +ATTR_DAYS_TO_REPLACE_FILTER = "days_to_replace_filter" +ATTR_EXHAUST_FAN_DUTY = "exhaust_fan_duty" +ATTR_EXHAUST_FAN_SPEED = "exhaust_fan_speed" +ATTR_EXHAUST_HUMIDITY = "exhaust_humidity" +ATTR_EXHAUST_TEMPERATURE = "exhaust_temperature" +ATTR_OUTSIDE_HUMIDITY = "outside_humidity" +ATTR_OUTSIDE_TEMPERATURE = "outside_temperature" +ATTR_POWER_CURRENT = "power_usage" +ATTR_SUPPLY_FAN_DUTY = "supply_fan_duty" +ATTR_SUPPLY_FAN_SPEED = "supply_fan_speed" +ATTR_SUPPLY_HUMIDITY = "supply_humidity" +ATTR_SUPPLY_TEMPERATURE = "supply_temperature" _LOGGER = logging.getLogger(__name__) -SENSOR_TYPES = {} +ATTR_ICON = "icon" +ATTR_ID = "id" +ATTR_LABEL = "label" +ATTR_MULTIPLIER = "multiplier" +ATTR_UNIT = "unit" + +SENSOR_TYPES = { + ATTR_CURRENT_TEMPERATURE: { + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_LABEL: "Inside Temperature", + ATTR_UNIT: TEMP_CELSIUS, + ATTR_ICON: "mdi:thermometer", + ATTR_ID: SENSOR_TEMPERATURE_EXTRACT, + ATTR_MULTIPLIER: 0.1, + }, + ATTR_CURRENT_HUMIDITY: { + ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + ATTR_LABEL: "Inside Humidity", + ATTR_UNIT: "%", + ATTR_ICON: "mdi:water-percent", + ATTR_ID: SENSOR_HUMIDITY_EXTRACT, + }, + ATTR_OUTSIDE_TEMPERATURE: { + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_LABEL: "Outside Temperature", + ATTR_UNIT: TEMP_CELSIUS, + ATTR_ICON: "mdi:thermometer", + ATTR_ID: SENSOR_TEMPERATURE_OUTDOOR, + ATTR_MULTIPLIER: 0.1, + }, + ATTR_OUTSIDE_HUMIDITY: { + ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + ATTR_LABEL: "Outside Humidity", + ATTR_UNIT: "%", + ATTR_ICON: "mdi:water-percent", + ATTR_ID: SENSOR_HUMIDITY_OUTDOOR, + }, + ATTR_SUPPLY_TEMPERATURE: { + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_LABEL: "Supply Temperature", + ATTR_UNIT: TEMP_CELSIUS, + ATTR_ICON: "mdi:thermometer", + ATTR_ID: SENSOR_TEMPERATURE_SUPPLY, + ATTR_MULTIPLIER: 0.1, + }, + ATTR_SUPPLY_HUMIDITY: { + ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + ATTR_LABEL: "Supply Humidity", + ATTR_UNIT: "%", + ATTR_ICON: "mdi:water-percent", + ATTR_ID: SENSOR_HUMIDITY_SUPPLY, + }, + ATTR_SUPPLY_FAN_SPEED: { + ATTR_DEVICE_CLASS: None, + ATTR_LABEL: "Supply Fan Speed", + ATTR_UNIT: "rpm", + ATTR_ICON: "mdi:fan", + ATTR_ID: SENSOR_FAN_SUPPLY_SPEED, + }, + ATTR_SUPPLY_FAN_DUTY: { + ATTR_DEVICE_CLASS: None, + ATTR_LABEL: "Supply Fan Duty", + ATTR_UNIT: "%", + ATTR_ICON: "mdi:fan", + ATTR_ID: SENSOR_FAN_SUPPLY_DUTY, + }, + ATTR_EXHAUST_FAN_SPEED: { + ATTR_DEVICE_CLASS: None, + ATTR_LABEL: "Exhaust Fan Speed", + ATTR_UNIT: "rpm", + ATTR_ICON: "mdi:fan", + ATTR_ID: SENSOR_FAN_EXHAUST_SPEED, + }, + ATTR_EXHAUST_FAN_DUTY: { + ATTR_DEVICE_CLASS: None, + ATTR_LABEL: "Exhaust Fan Duty", + ATTR_UNIT: "%", + ATTR_ICON: "mdi:fan", + ATTR_ID: SENSOR_FAN_EXHAUST_DUTY, + }, + ATTR_EXHAUST_TEMPERATURE: { + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_LABEL: "Exhaust Temperature", + ATTR_UNIT: TEMP_CELSIUS, + ATTR_ICON: "mdi:thermometer", + ATTR_ID: SENSOR_TEMPERATURE_EXHAUST, + ATTR_MULTIPLIER: 0.1, + }, + ATTR_EXHAUST_HUMIDITY: { + ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + ATTR_LABEL: "Exhaust Humidity", + ATTR_UNIT: "%", + ATTR_ICON: "mdi:water-percent", + ATTR_ID: SENSOR_HUMIDITY_EXHAUST, + }, + ATTR_AIR_FLOW_SUPPLY: { + ATTR_DEVICE_CLASS: None, + ATTR_LABEL: "Supply airflow", + ATTR_UNIT: "m³/h", + ATTR_ICON: "mdi:fan", + ATTR_ID: SENSOR_FAN_SUPPLY_FLOW, + }, + ATTR_AIR_FLOW_EXHAUST: { + ATTR_DEVICE_CLASS: None, + ATTR_LABEL: "Exhaust airflow", + ATTR_UNIT: "m³/h", + ATTR_ICON: "mdi:fan", + ATTR_ID: SENSOR_FAN_EXHAUST_FLOW, + }, + ATTR_BYPASS_STATE: { + ATTR_DEVICE_CLASS: None, + ATTR_LABEL: "Bypass State", + ATTR_UNIT: "%", + ATTR_ICON: "mdi:camera-iris", + ATTR_ID: SENSOR_BYPASS_STATE, + }, + ATTR_DAYS_TO_REPLACE_FILTER: { + ATTR_DEVICE_CLASS: None, + ATTR_LABEL: "Days to replace filter", + ATTR_UNIT: "days", + ATTR_ICON: "mdi:calendar", + ATTR_ID: SENSOR_DAYS_TO_REPLACE_FILTER, + }, + ATTR_POWER_CURRENT: { + ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, + ATTR_LABEL: "Power usage", + ATTR_UNIT: POWER_WATT, + ATTR_ICON: "mdi:flash", + ATTR_ID: SENSOR_POWER_CURRENT, + }, +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_RESOURCES, default=[]): vol.All( + cv.ensure_list, [vol.In(SENSOR_TYPES)] + ), + } +) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the ComfoConnect fan platform.""" - - global SENSOR_TYPES - SENSOR_TYPES = { - ATTR_CURRENT_TEMPERATURE: [ - "Inside Temperature", - TEMP_CELSIUS, - "mdi:thermometer", - SENSOR_TEMPERATURE_EXTRACT, - ], - ATTR_CURRENT_HUMIDITY: [ - "Inside Humidity", - "%", - "mdi:water-percent", - SENSOR_HUMIDITY_EXTRACT, - ], - ATTR_OUTSIDE_TEMPERATURE: [ - "Outside Temperature", - TEMP_CELSIUS, - "mdi:thermometer", - SENSOR_TEMPERATURE_OUTDOOR, - ], - ATTR_OUTSIDE_HUMIDITY: [ - "Outside Humidity", - "%", - "mdi:water-percent", - SENSOR_HUMIDITY_OUTDOOR, - ], - ATTR_AIR_FLOW_SUPPLY: [ - "Supply airflow", - "m³/h", - "mdi:air-conditioner", - SENSOR_FAN_SUPPLY_FLOW, - ], - ATTR_AIR_FLOW_EXHAUST: [ - "Exhaust airflow", - "m³/h", - "mdi:air-conditioner", - SENSOR_FAN_EXHAUST_FLOW, - ], - } - ccb = hass.data[DOMAIN] sensors = [] for resource in config[CONF_RESOURCES]: - sensor_type = resource.lower() - - if sensor_type not in SENSOR_TYPES: - _LOGGER.warning("Sensor type: %s is not a valid sensor", sensor_type) - continue - sensors.append( ComfoConnectSensor( - name=f"{ccb.name} {SENSOR_TYPES[sensor_type][0]}", + name=f"{ccb.name} {SENSOR_TYPES[resource][ATTR_LABEL]}", ccb=ccb, - sensor_type=sensor_type, + sensor_type=resource, ) ) @@ -102,23 +223,35 @@ class ComfoConnectSensor(Entity): """Initialize the ComfoConnect sensor.""" self._ccb = ccb self._sensor_type = sensor_type - self._sensor_id = SENSOR_TYPES[self._sensor_type][3] + self._sensor_id = SENSOR_TYPES[self._sensor_type][ATTR_ID] self._name = name async def async_added_to_hass(self): """Register for sensor updates.""" + _LOGGER.debug( + "Registering for sensor %s (%d)", self._sensor_type, self._sensor_id, + ) + async_dispatcher_connect( + self.hass, + SIGNAL_COMFOCONNECT_UPDATE_RECEIVED.format(self._sensor_id), + self._handle_update, + ) await self.hass.async_add_executor_job( self._ccb.comfoconnect.register_sensor, self._sensor_id ) - async_dispatcher_connect( - self.hass, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, self._handle_update - ) - def _handle_update(self, var): + def _handle_update(self, value): """Handle update callbacks.""" - if var == self._sensor_id: - _LOGGER.debug("Received update for %s", var) - self.schedule_update_ha_state() + _LOGGER.debug( + "Handle update for sensor %s (%d): %s", + self._sensor_type, + self._sensor_id, + value, + ) + self._ccb.data[self._sensor_id] = round( + value * SENSOR_TYPES[self._sensor_type].get(ATTR_MULTIPLIER, 1), 2 + ) + self.schedule_update_ha_state() @property def state(self): @@ -133,6 +266,11 @@ class ComfoConnectSensor(Entity): """Do not poll.""" return False + @property + def unique_id(self): + """Return a unique_id for this entity.""" + return f"{self._ccb.unique_id}-{self._sensor_type}" + @property def name(self): """Return the name of the sensor.""" @@ -140,10 +278,15 @@ class ComfoConnectSensor(Entity): @property def icon(self): - """Return the icon to use in the frontend, if any.""" - return SENSOR_TYPES[self._sensor_type][2] + """Return the icon to use in the frontend.""" + return SENSOR_TYPES[self._sensor_type][ATTR_ICON] @property def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return SENSOR_TYPES[self._sensor_type][1] + """Return the unit of measurement of this entity.""" + return SENSOR_TYPES[self._sensor_type][ATTR_UNIT] + + @property + def device_class(self): + """Return the device_class.""" + return SENSOR_TYPES[self._sensor_type][ATTR_DEVICE_CLASS] diff --git a/homeassistant/components/command_line/cover.py b/homeassistant/components/command_line/cover.py index c4413e78a00..1d996614caa 100644 --- a/homeassistant/components/command_line/cover.py +++ b/homeassistant/components/command_line/cover.py @@ -4,15 +4,15 @@ import subprocess import voluptuous as vol -from homeassistant.components.cover import CoverDevice, PLATFORM_SCHEMA +from homeassistant.components.cover import PLATFORM_SCHEMA, CoverDevice from homeassistant.const import ( CONF_COMMAND_CLOSE, CONF_COMMAND_OPEN, CONF_COMMAND_STATE, CONF_COMMAND_STOP, CONF_COVERS, - CONF_VALUE_TEMPLATE, CONF_FRIENDLY_NAME, + CONF_VALUE_TEMPLATE, ) import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/command_line/notify.py b/homeassistant/components/command_line/notify.py index e2581c8f065..21653171f34 100644 --- a/homeassistant/components/command_line/notify.py +++ b/homeassistant/components/command_line/notify.py @@ -4,11 +4,10 @@ import subprocess import voluptuous as vol +from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService from homeassistant.const import CONF_COMMAND, CONF_NAME import homeassistant.helpers.config_validation as cv -from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService - _LOGGER = logging.getLogger(__name__) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( diff --git a/homeassistant/components/command_line/switch.py b/homeassistant/components/command_line/switch.py index 937e859197a..62dcbe2f15a 100644 --- a/homeassistant/components/command_line/switch.py +++ b/homeassistant/components/command_line/switch.py @@ -4,21 +4,20 @@ import subprocess import voluptuous as vol -import homeassistant.helpers.config_validation as cv - from homeassistant.components.switch import ( - SwitchDevice, - PLATFORM_SCHEMA, ENTITY_ID_FORMAT, + PLATFORM_SCHEMA, + SwitchDevice, ) from homeassistant.const import ( - CONF_FRIENDLY_NAME, - CONF_SWITCHES, - CONF_VALUE_TEMPLATE, CONF_COMMAND_OFF, CONF_COMMAND_ON, CONF_COMMAND_STATE, + CONF_FRIENDLY_NAME, + CONF_SWITCHES, + CONF_VALUE_TEMPLATE, ) +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/concord232/alarm_control_panel.py b/homeassistant/components/concord232/alarm_control_panel.py index 37bbf052838..81a54a182d4 100644 --- a/homeassistant/components/concord232/alarm_control_panel.py +++ b/homeassistant/components/concord232/alarm_control_panel.py @@ -8,6 +8,10 @@ import voluptuous as vol import homeassistant.components.alarm_control_panel as alarm from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, +) from homeassistant.const import ( CONF_CODE, CONF_HOST, @@ -85,6 +89,11 @@ class Concord232Alarm(alarm.AlarmControlPanel): """Return the state of the device.""" return self._state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY + def update(self): """Update values from API.""" try: diff --git a/homeassistant/components/config/__init__.py b/homeassistant/components/config/__init__.py index 5a66c1fc5d4..5873cdc3271 100644 --- a/homeassistant/components/config/__init__.py +++ b/homeassistant/components/config/__init__.py @@ -6,11 +6,11 @@ import os import voluptuous as vol from homeassistant.components.http import HomeAssistantView -from homeassistant.const import EVENT_COMPONENT_LOADED, CONF_ID +from homeassistant.const import CONF_ID, EVENT_COMPONENT_LOADED from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import ATTR_COMPONENT -from homeassistant.util.yaml import load_yaml, dump +from homeassistant.util.yaml import dump, load_yaml DOMAIN = "config" SECTIONS = ( diff --git a/homeassistant/components/config/area_registry.py b/homeassistant/components/config/area_registry.py index 9c8853ac782..81daf35339e 100644 --- a/homeassistant/components/config/area_registry.py +++ b/homeassistant/components/config/area_registry.py @@ -9,7 +9,6 @@ from homeassistant.components.websocket_api.decorators import ( from homeassistant.core import callback from homeassistant.helpers.area_registry import async_get_registry - WS_TYPE_LIST = "config/area_registry/list" SCHEMA_WS_LIST = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( {vol.Required("type"): WS_TYPE_LIST} diff --git a/homeassistant/components/config/auth.py b/homeassistant/components/config/auth.py index 977bae36083..361367ffb4d 100644 --- a/homeassistant/components/config/auth.py +++ b/homeassistant/components/config/auth.py @@ -3,7 +3,6 @@ import voluptuous as vol from homeassistant.components import websocket_api - WS_TYPE_LIST = "config/auth/list" SCHEMA_WS_LIST = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( {vol.Required("type"): WS_TYPE_LIST} diff --git a/homeassistant/components/config/auth_provider_homeassistant.py b/homeassistant/components/config/auth_provider_homeassistant.py index 817675db238..dec7fb24d27 100644 --- a/homeassistant/components/config/auth_provider_homeassistant.py +++ b/homeassistant/components/config/auth_provider_homeassistant.py @@ -4,7 +4,6 @@ import voluptuous as vol from homeassistant.auth.providers import homeassistant as auth_ha from homeassistant.components import websocket_api - WS_TYPE_CREATE = "config/auth_provider/homeassistant/create" SCHEMA_WS_CREATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( { diff --git a/homeassistant/components/config/automation.py b/homeassistant/components/config/automation.py index 0e9b4053b7b..d7bb1ef9883 100644 --- a/homeassistant/components/config/automation.py +++ b/homeassistant/components/config/automation.py @@ -4,8 +4,8 @@ import uuid from homeassistant.components.automation import DOMAIN, PLATFORM_SCHEMA from homeassistant.components.automation.config import async_validate_config_item -from homeassistant.const import CONF_ID, SERVICE_RELOAD from homeassistant.config import AUTOMATION_CONFIG_PATH +from homeassistant.const import CONF_ID, SERVICE_RELOAD import homeassistant.helpers.config_validation as cv from . import EditIdBasedConfigView diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index 81065665e34..dbf0ee8f283 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -33,6 +33,7 @@ async def async_setup(hass): hass.components.websocket_api.async_register_command(config_entries_progress) hass.components.websocket_api.async_register_command(system_options_list) hass.components.websocket_api.async_register_command(system_options_update) + hass.components.websocket_api.async_register_command(ignore_config_flow) return True @@ -284,3 +285,37 @@ async def system_options_update(hass, connection, msg): hass.config_entries.async_update_entry(entry, system_options=changes) connection.send_result(msg["id"], entry.system_options.as_dict()) + + +@websocket_api.require_admin +@websocket_api.async_response +@websocket_api.websocket_command({"type": "config_entries/ignore_flow", "flow_id": str}) +async def ignore_config_flow(hass, connection, msg): + """Ignore a config flow.""" + flow = next( + ( + flw + for flw in hass.config_entries.flow.async_progress() + if flw["flow_id"] == msg["flow_id"] + ), + None, + ) + + if flow is None: + connection.send_error( + msg["id"], websocket_api.const.ERR_NOT_FOUND, "Config entry not found" + ) + return + + if "unique_id" not in flow["context"]: + connection.send_error( + msg["id"], "no_unique_id", "Specified flow has no unique ID." + ) + return + + await hass.config_entries.flow.async_init( + flow["handler"], + context={"source": config_entries.SOURCE_IGNORE}, + data={"unique_id": flow["context"]["unique_id"]}, + ) + connection.send_result(msg["id"]) diff --git a/homeassistant/components/config/core.py b/homeassistant/components/config/core.py index 073f8f23d6c..e9ceb7eac57 100644 --- a/homeassistant/components/config/core.py +++ b/homeassistant/components/config/core.py @@ -2,10 +2,10 @@ import voluptuous as vol +from homeassistant.components import websocket_api from homeassistant.components.http import HomeAssistantView from homeassistant.config import async_check_ha_config_file -from homeassistant.components import websocket_api -from homeassistant.const import CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL +from homeassistant.const import CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_METRIC from homeassistant.helpers import config_validation as cv from homeassistant.util import location diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index 125b2260f08..458a9dd3ecb 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -1,15 +1,15 @@ """HTTP views to interact with the entity registry.""" import voluptuous as vol -from homeassistant.core import callback -from homeassistant.helpers.entity_registry import async_get_registry from homeassistant.components import websocket_api from homeassistant.components.websocket_api.const import ERR_NOT_FOUND from homeassistant.components.websocket_api.decorators import ( async_response, require_admin, ) +from homeassistant.core import callback from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.entity_registry import async_get_registry async def async_setup(hass): diff --git a/homeassistant/components/config/group.py b/homeassistant/components/config/group.py index d104cd2e1df..d95891af655 100644 --- a/homeassistant/components/config/group.py +++ b/homeassistant/components/config/group.py @@ -1,7 +1,7 @@ """Provide configuration end points for Groups.""" from homeassistant.components.group import DOMAIN, GROUP_SCHEMA -from homeassistant.const import SERVICE_RELOAD from homeassistant.config import GROUP_CONFIG_PATH +from homeassistant.const import SERVICE_RELOAD import homeassistant.helpers.config_validation as cv from . import EditKeyBasedConfigView diff --git a/homeassistant/components/config/scene.py b/homeassistant/components/config/scene.py index 6e77dae0826..79a30177e47 100644 --- a/homeassistant/components/config/scene.py +++ b/homeassistant/components/config/scene.py @@ -3,8 +3,8 @@ from collections import OrderedDict import uuid from homeassistant.components.scene import DOMAIN, PLATFORM_SCHEMA -from homeassistant.const import CONF_ID, SERVICE_RELOAD from homeassistant.config import SCENE_CONFIG_PATH +from homeassistant.const import CONF_ID, SERVICE_RELOAD import homeassistant.helpers.config_validation as cv from . import EditIdBasedConfigView diff --git a/homeassistant/components/config/script.py b/homeassistant/components/config/script.py index e63651d8f2a..032774de473 100644 --- a/homeassistant/components/config/script.py +++ b/homeassistant/components/config/script.py @@ -1,7 +1,7 @@ """Provide configuration end points for scripts.""" from homeassistant.components.script import DOMAIN, SCRIPT_ENTRY_SCHEMA -from homeassistant.const import SERVICE_RELOAD from homeassistant.config import SCRIPT_CONFIG_PATH +from homeassistant.const import SERVICE_RELOAD import homeassistant.helpers.config_validation as cv from . import EditKeyBasedConfigView diff --git a/homeassistant/components/configurator/__init__.py b/homeassistant/components/configurator/__init__.py index f3b2a41e917..78333d96355 100644 --- a/homeassistant/components/configurator/__init__.py +++ b/homeassistant/components/configurator/__init__.py @@ -9,14 +9,14 @@ the user has submitted configuration information. import functools as ft import logging -from homeassistant.core import callback as async_callback from homeassistant.const import ( - EVENT_TIME_CHANGED, - ATTR_FRIENDLY_NAME, ATTR_ENTITY_PICTURE, + ATTR_FRIENDLY_NAME, + EVENT_TIME_CHANGED, ) -from homeassistant.loader import bind_hass +from homeassistant.core import callback as async_callback from homeassistant.helpers.entity import async_generate_entity_id +from homeassistant.loader import bind_hass from homeassistant.util.async_ import run_callback_threadsafe _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/conversation/__init__.py b/homeassistant/components/conversation/__init__.py index a82034a4237..158a365981b 100644 --- a/homeassistant/components/conversation/__init__.py +++ b/homeassistant/components/conversation/__init__.py @@ -11,7 +11,7 @@ from homeassistant.helpers import config_validation as cv, intent from homeassistant.loader import bind_hass from .agent import AbstractConversationAgent -from .default_agent import async_register, DefaultAgent +from .default_agent import DefaultAgent, async_register _LOGGER = logging.getLogger(__name__) @@ -50,26 +50,17 @@ def async_set_agent(hass: core.HomeAssistant, agent: AbstractConversationAgent): hass.data[DATA_AGENT] = agent -async def get_agent(hass: core.HomeAssistant) -> AbstractConversationAgent: - """Get agent.""" - agent = hass.data.get(DATA_AGENT) - if agent is None: - agent = hass.data[DATA_AGENT] = DefaultAgent(hass) - await agent.async_initialize(hass.data.get(DATA_CONFIG)) - return agent - - async def async_setup(hass, config): """Register the process service.""" - hass.data[DATA_CONFIG] = config async def handle_service(service): """Parse text into commands.""" text = service.data[ATTR_TEXT] _LOGGER.debug("Processing: <%s>", text) + agent = await _get_agent(hass) try: - await process(hass, text, service.context.id) + await agent.async_process(text, service.context) except intent.IntentHandleError as err: _LOGGER.error("Error processing %s: %s", text, err) @@ -84,27 +75,6 @@ async def async_setup(hass, config): return True -async def process(hass: core.HomeAssistant, text: str, conversation_id: str): - """Process text and get intent.""" - agent = await get_agent(hass) - return await agent.async_process(text, conversation_id) - - -async def get_intent(hass: core.HomeAssistant, text: str, conversation_id: str): - """Process text and get intent.""" - try: - intent_result = await process(hass, text, conversation_id) - except intent.IntentHandleError as err: - intent_result = intent.IntentResponse() - intent_result.async_set_speech(str(err)) - - if intent_result is None: - intent_result = intent.IntentResponse() - intent_result.async_set_speech("Sorry, I didn't understand that") - - return intent_result - - @websocket_api.async_response @websocket_api.websocket_command( {"type": "conversation/process", "text": str, vol.Optional("conversation_id"): str} @@ -112,7 +82,10 @@ async def get_intent(hass: core.HomeAssistant, text: str, conversation_id: str): async def websocket_process(hass, connection, msg): """Process text.""" connection.send_result( - msg["id"], await get_intent(hass, msg["text"], msg.get("conversation_id")) + msg["id"], + await _async_converse( + hass, msg["text"], msg.get("conversation_id"), connection.context(msg) + ), ) @@ -120,7 +93,7 @@ async def websocket_process(hass, connection, msg): @websocket_api.websocket_command({"type": "conversation/agent/info"}) async def websocket_get_agent_info(hass, connection, msg): """Do we need onboarding.""" - agent = await get_agent(hass) + agent = await _get_agent(hass) connection.send_result( msg["id"], @@ -135,7 +108,7 @@ async def websocket_get_agent_info(hass, connection, msg): @websocket_api.websocket_command({"type": "conversation/onboarding/set", "shown": bool}) async def websocket_set_onboarding(hass, connection, msg): """Set onboarding status.""" - agent = await get_agent(hass) + agent = await _get_agent(hass) success = await agent.async_set_onboarding(msg["shown"]) @@ -157,8 +130,36 @@ class ConversationProcessView(http.HomeAssistantView): async def post(self, request, data): """Send a request for processing.""" hass = request.app["hass"] - intent_result = await get_intent( - hass, data["text"], data.get("conversation_id") + + intent_result = await _async_converse( + hass, data["text"], data.get("conversation_id"), self.context(request) ) return self.json(intent_result) + + +async def _get_agent(hass: core.HomeAssistant) -> AbstractConversationAgent: + """Get the active conversation agent.""" + agent = hass.data.get(DATA_AGENT) + if agent is None: + agent = hass.data[DATA_AGENT] = DefaultAgent(hass) + await agent.async_initialize(hass.data.get(DATA_CONFIG)) + return agent + + +async def _async_converse( + hass: core.HomeAssistant, text: str, conversation_id: str, context: core.Context +) -> intent.IntentResponse: + """Process text and get intent.""" + agent = await _get_agent(hass) + try: + intent_result = await agent.async_process(text, context, conversation_id) + except intent.IntentHandleError as err: + intent_result = intent.IntentResponse() + intent_result.async_set_speech(str(err)) + + if intent_result is None: + intent_result = intent.IntentResponse() + intent_result.async_set_speech("Sorry, I didn't understand that") + + return intent_result diff --git a/homeassistant/components/conversation/agent.py b/homeassistant/components/conversation/agent.py index 0c47d615645..c9c2ab46cf9 100644 --- a/homeassistant/components/conversation/agent.py +++ b/homeassistant/components/conversation/agent.py @@ -2,6 +2,7 @@ from abc import ABC, abstractmethod from typing import Optional +from homeassistant.core import Context from homeassistant.helpers import intent @@ -23,6 +24,6 @@ class AbstractConversationAgent(ABC): @abstractmethod async def async_process( - self, text: str, conversation_id: Optional[str] = None + self, text: str, context: Context, conversation_id: Optional[str] = None ) -> intent.IntentResponse: """Process a sentence.""" diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index c202cdf1e65..2f09cba2eb1 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -3,9 +3,12 @@ import logging import re from typing import Optional -from homeassistant import core -from homeassistant.components.cover import INTENT_CLOSE_COVER, INTENT_OPEN_COVER -from homeassistant.components.shopping_list import INTENT_ADD_ITEM, INTENT_LAST_ITEMS +from homeassistant import core, setup +from homeassistant.components.cover.intent import INTENT_CLOSE_COVER, INTENT_OPEN_COVER +from homeassistant.components.shopping_list.intent import ( + INTENT_ADD_ITEM, + INTENT_LAST_ITEMS, +) from homeassistant.const import EVENT_COMPONENT_LOADED from homeassistant.core import callback from homeassistant.helpers import intent @@ -58,6 +61,9 @@ class DefaultAgent(AbstractConversationAgent): async def async_initialize(self, config): """Initialize the default agent.""" + if "intent" not in self.hass.config.components: + await setup.async_setup_component(self.hass, "intent", {}) + config = config.get(DOMAIN, {}) intents = self.hass.data.setdefault(DOMAIN, {}) @@ -109,7 +115,7 @@ class DefaultAgent(AbstractConversationAgent): async_register(self.hass, intent_type, sentences) async def async_process( - self, text: str, conversation_id: Optional[str] = None + self, text: str, context: core.Context, conversation_id: Optional[str] = None ) -> intent.IntentResponse: """Process a sentence.""" intents = self.hass.data[DOMAIN] @@ -127,4 +133,5 @@ class DefaultAgent(AbstractConversationAgent): intent_type, {key: {"value": value} for key, value in match.groupdict().items()}, text, + context, ) diff --git a/homeassistant/components/coolmaster/.translations/da.json b/homeassistant/components/coolmaster/.translations/da.json index 8f50a0eb6dd..882bc5de359 100644 --- a/homeassistant/components/coolmaster/.translations/da.json +++ b/homeassistant/components/coolmaster/.translations/da.json @@ -1,9 +1,17 @@ { "config": { + "error": { + "connection_error": "Kunne ikke oprette forbindelse til CoolMasterNet-instansen. Tjek din v\u00e6rt.", + "no_units": "Kunne ikke finde nogen klimaanl\u00e6g i CoolMasterNet-v\u00e6rt." + }, "step": { "user": { "data": { - "heat_cool": "Underst\u00f8t automatisk varm/k\u00f8l tilstand", + "cool": "Underst\u00f8tter k\u00f8lingstilstand", + "dry": "Underst\u00f8tter t\u00f8rringstilstand", + "fan_only": "Underst\u00f8tter kun-bl\u00e6ser-tilstand", + "heat": "Underst\u00f8tter varmetilstand", + "heat_cool": "Underst\u00f8tter automatisk varm/k\u00f8l-tilstand", "host": "V\u00e6rt", "off": "Kan slukkes" }, diff --git a/homeassistant/components/coolmaster/.translations/ko.json b/homeassistant/components/coolmaster/.translations/ko.json index ff6ddf0acfe..4d96e606c7b 100644 --- a/homeassistant/components/coolmaster/.translations/ko.json +++ b/homeassistant/components/coolmaster/.translations/ko.json @@ -13,7 +13,7 @@ "heat": "\ub09c\ubc29 \ubaa8\ub4dc \uc9c0\uc6d0", "heat_cool": "\uc790\ub3d9 \ub0c9/\ub09c\ubc29 \ubaa8\ub4dc \uc9c0\uc6d0", "host": "\ud638\uc2a4\ud2b8", - "off": "\uc804\uc6d0\uc744 \ub04c \uc218 \uc788\uc2b4" + "off": "\uc804\uc6d0 \ub044\uae30 \ud5c8\uc6a9" }, "title": "CoolMasterNet \uc5f0\uacb0 \uc0c1\uc138\uc815\ubcf4\ub97c \uc124\uc815\ud574\uc8fc\uc138\uc694." } diff --git a/homeassistant/components/coolmaster/.translations/nl.json b/homeassistant/components/coolmaster/.translations/nl.json index 79a1e9fe1e6..e5b1683790f 100644 --- a/homeassistant/components/coolmaster/.translations/nl.json +++ b/homeassistant/components/coolmaster/.translations/nl.json @@ -1,10 +1,20 @@ { "config": { "error": { - "connection_error": "Kan geen verbinding maken met CoolMasterNet-instantie. Controleer uw host" + "connection_error": "Kan geen verbinding maken met CoolMasterNet-instantie. Controleer uw host", + "no_units": "Kon geen HVAC units vinden in CoolMasterNet host." }, "step": { "user": { + "data": { + "cool": "Ondersteuning afkoelen modus", + "dry": "Ondersteuning droog modus", + "fan_only": "Ondersteunt alleen ventilatormodus", + "heat": "Ondersteuning warmtemodus", + "heat_cool": "Ondersteuning van automatische warmte/koelmodus", + "host": "Host", + "off": "Kan uitgeschakeld worden" + }, "title": "Stel uw CoolMasterNet-verbindingsgegevens in." } }, diff --git a/homeassistant/components/coolmaster/config_flow.py b/homeassistant/components/coolmaster/config_flow.py index fe52ea17b28..e9cef562647 100644 --- a/homeassistant/components/coolmaster/config_flow.py +++ b/homeassistant/components/coolmaster/config_flow.py @@ -3,7 +3,7 @@ from pycoolmasternet import CoolMasterNet import voluptuous as vol -from homeassistant import core, config_entries +from homeassistant import config_entries, core from homeassistant.const import CONF_HOST, CONF_PORT # pylint: disable=unused-import diff --git a/homeassistant/components/counter/__init__.py b/homeassistant/components/counter/__init__.py index aca3461b4f7..98329bc417a 100644 --- a/homeassistant/components/counter/__init__.py +++ b/homeassistant/components/counter/__init__.py @@ -3,10 +3,8 @@ import logging import voluptuous as vol -from homeassistant.const import CONF_ICON, CONF_NAME, CONF_MAXIMUM, CONF_MINIMUM - +from homeassistant.const import CONF_ICON, CONF_MAXIMUM, CONF_MINIMUM, CONF_NAME import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity @@ -33,15 +31,6 @@ SERVICE_INCREMENT = "increment" SERVICE_RESET = "reset" SERVICE_CONFIGURE = "configure" -SERVICE_SCHEMA_CONFIGURE = ENTITY_SERVICE_SCHEMA.extend( - { - vol.Optional(ATTR_MINIMUM): vol.Any(None, vol.Coerce(int)), - vol.Optional(ATTR_MAXIMUM): vol.Any(None, vol.Coerce(int)), - vol.Optional(ATTR_STEP): cv.positive_int, - vol.Optional(ATTR_INITIAL): cv.positive_int, - vol.Optional(VALUE): cv.positive_int, - } -) CONFIG_SCHEMA = vol.Schema( { @@ -95,17 +84,19 @@ async def async_setup(hass, config): if not entities: return False + 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") component.async_register_entity_service( - SERVICE_INCREMENT, ENTITY_SERVICE_SCHEMA, "async_increment" - ) - component.async_register_entity_service( - SERVICE_DECREMENT, ENTITY_SERVICE_SCHEMA, "async_decrement" - ) - component.async_register_entity_service( - SERVICE_RESET, ENTITY_SERVICE_SCHEMA, "async_reset" - ) - component.async_register_entity_service( - SERVICE_CONFIGURE, SERVICE_SCHEMA_CONFIGURE, "async_configure" + SERVICE_CONFIGURE, + { + vol.Optional(ATTR_MINIMUM): vol.Any(None, vol.Coerce(int)), + vol.Optional(ATTR_MAXIMUM): vol.Any(None, vol.Coerce(int)), + vol.Optional(ATTR_STEP): cv.positive_int, + vol.Optional(ATTR_INITIAL): cv.positive_int, + vol.Optional(VALUE): cv.positive_int, + }, + "async_configure", ) await component.async_add_entities(entities) diff --git a/homeassistant/components/counter/reproduce_state.py b/homeassistant/components/counter/reproduce_state.py index ac5045d68e7..b37fcea719e 100644 --- a/homeassistant/components/counter/reproduce_state.py +++ b/homeassistant/components/counter/reproduce_state.py @@ -12,9 +12,9 @@ from . import ( ATTR_MAXIMUM, ATTR_MINIMUM, ATTR_STEP, - VALUE, DOMAIN, SERVICE_CONFIGURE, + VALUE, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/cover/.translations/da.json b/homeassistant/components/cover/.translations/da.json index e603723b564..64b89be5267 100644 --- a/homeassistant/components/cover/.translations/da.json +++ b/homeassistant/components/cover/.translations/da.json @@ -4,7 +4,17 @@ "is_closed": "{entity_name} er lukket", "is_closing": "{entity_name} lukker", "is_open": "{entity_name} er \u00e5ben", - "is_opening": "{entity_name} \u00e5bnes" + "is_opening": "{entity_name} \u00e5bnes", + "is_position": "Aktuel {entity_name} position er", + "is_tilt_position": "Aktuel {entity_name} vippeposition er" + }, + "trigger_type": { + "closed": "{entity_name} lukket", + "closing": "{entity_name} lukning", + "opened": "{entity_name} \u00e5bnet", + "opening": "{entity_name} \u00e5bning", + "position": "{entity_name} position \u00e6ndres", + "tilt_position": "{entity_name} vippeposition \u00e6ndres" } } } \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/hu.json b/homeassistant/components/cover/.translations/hu.json new file mode 100644 index 00000000000..d460c53109d --- /dev/null +++ b/homeassistant/components/cover/.translations/hu.json @@ -0,0 +1,20 @@ +{ + "device_automation": { + "condition_type": { + "is_closed": "{entity_name} z\u00e1rva van", + "is_closing": "{entity_name} z\u00e1r\u00f3dik", + "is_open": "{entity_name} nyitva van", + "is_opening": "{entity_name} ny\u00edlik", + "is_position": "{entity_name} jelenlegi poz\u00edci\u00f3ja", + "is_tilt_position": "{entity_name} jelenlegi d\u00f6nt\u00e9si poz\u00edci\u00f3ja" + }, + "trigger_type": { + "closed": "{entity_name} bez\u00e1r\u00f3dott", + "closing": "{entity_name} z\u00e1r\u00f3dik", + "opened": "{entity_name} kiny\u00edlt", + "opening": "{entity_name} ny\u00edlik", + "position": "{entity_name} poz\u00edci\u00f3ja v\u00e1ltozik", + "tilt_position": "{entity_name} d\u00f6nt\u00e9si poz\u00edci\u00f3ja v\u00e1ltozik" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/ko.json b/homeassistant/components/cover/.translations/ko.json index 6a59bb9f6ae..145938b6f24 100644 --- a/homeassistant/components/cover/.translations/ko.json +++ b/homeassistant/components/cover/.translations/ko.json @@ -1,20 +1,20 @@ { "device_automation": { "condition_type": { - "is_closed": "{entity_name} \uc774(\uac00) \ub2eb\ud614\uc2b5\ub2c8\ub2e4", - "is_closing": "{entity_name} \uc774(\uac00) \ub2eb\ud799\ub2c8\ub2e4", - "is_open": "{entity_name} \uc774(\uac00) \uc5f4\ub838\uc2b5\ub2c8\ub2e4", - "is_opening": "{entity_name} \uc774(\uac00) \uc5f4\ub9bd\ub2c8\ub2e4", - "is_position": "\ud604\uc7ac {entity_name} \uac1c\ud3d0 \uc704\uce58", - "is_tilt_position": "\ud604\uc7ac {entity_name} \uac1c\ud3d0 \uae30\uc6b8\uae30" + "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" }, "trigger_type": { - "closed": "{entity_name} \uc774(\uac00) \ub2eb\ud798", - "closing": "{entity_name} \uc774(\uac00) \ub2eb\ud788\ub294 \uc911", - "opened": "{entity_name} \uc774(\uac00) \uc5f4\ub9bc", - "opening": "{entity_name} \uc774(\uac00) \uc5f4\ub9ac\ub294 \uc911", - "position": "{entity_name} \uac1c\ud3d0 \uc704\uce58 \ubcc0\ud654", - "tilt_position": "{entity_name} \uac1c\ud3d0 \uae30\uc6b8\uae30 \ubcc0\ud654" + "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" } } } \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/ru.json b/homeassistant/components/cover/.translations/ru.json index 043e5a42d2a..ebe81486cf5 100644 --- a/homeassistant/components/cover/.translations/ru.json +++ b/homeassistant/components/cover/.translations/ru.json @@ -9,7 +9,9 @@ "is_tilt_position": "{entity_name} \u0432 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438 \u043d\u0430\u043a\u043b\u043e\u043d\u0430" }, "trigger_type": { + "closed": "{entity_name} \u0437\u0430\u043a\u0440\u044b\u0442\u043e", "closing": "{entity_name} \u0437\u0430\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", + "opened": "{entity_name} \u043e\u0442\u043a\u0440\u044b\u0442\u043e", "opening": "{entity_name} \u043e\u0442\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", "position": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435", "tilt_position": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u043d\u0430\u043a\u043b\u043e\u043d" diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index d57bf678a69..3c842067cca 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -6,33 +6,30 @@ from typing import Any import voluptuous as vol -from homeassistant.loader import bind_hass -from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.entity import Entity +from homeassistant.components import group +from homeassistant.const import ( + SERVICE_CLOSE_COVER, + SERVICE_CLOSE_COVER_TILT, + SERVICE_OPEN_COVER, + SERVICE_OPEN_COVER_TILT, + SERVICE_SET_COVER_POSITION, + SERVICE_SET_COVER_TILT_POSITION, + SERVICE_STOP_COVER, + SERVICE_STOP_COVER_TILT, + SERVICE_TOGGLE, + SERVICE_TOGGLE_COVER_TILT, + STATE_CLOSED, + STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, +) from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) -from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA -from homeassistant.components import group -from homeassistant.helpers import intent -from homeassistant.const import ( - SERVICE_OPEN_COVER, - SERVICE_CLOSE_COVER, - SERVICE_SET_COVER_POSITION, - SERVICE_STOP_COVER, - SERVICE_TOGGLE, - SERVICE_OPEN_COVER_TILT, - SERVICE_CLOSE_COVER_TILT, - SERVICE_STOP_COVER_TILT, - SERVICE_SET_COVER_TILT_POSITION, - SERVICE_TOGGLE_COVER_TILT, - STATE_OPEN, - STATE_CLOSED, - STATE_OPENING, - STATE_CLOSING, -) - +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.loader import bind_hass # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs @@ -83,21 +80,6 @@ ATTR_CURRENT_TILT_POSITION = "current_tilt_position" ATTR_POSITION = "position" ATTR_TILT_POSITION = "tilt_position" -INTENT_OPEN_COVER = "HassOpenCover" -INTENT_CLOSE_COVER = "HassCloseCover" - -COVER_SET_COVER_POSITION_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_POSITION): vol.All(vol.Coerce(int), vol.Range(min=0, max=100))} -) - -COVER_SET_COVER_TILT_POSITION_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - { - vol.Required(ATTR_TILT_POSITION): vol.All( - vol.Coerce(int), vol.Range(min=0, max=100) - ) - } -) - @bind_hass def is_closed(hass, entity_id=None): @@ -114,59 +96,50 @@ async def async_setup(hass, config): await component.async_setup(config) - component.async_register_entity_service( - SERVICE_OPEN_COVER, ENTITY_SERVICE_SCHEMA, "async_open_cover" - ) + component.async_register_entity_service(SERVICE_OPEN_COVER, {}, "async_open_cover") component.async_register_entity_service( - SERVICE_CLOSE_COVER, ENTITY_SERVICE_SCHEMA, "async_close_cover" + SERVICE_CLOSE_COVER, {}, "async_close_cover" ) component.async_register_entity_service( SERVICE_SET_COVER_POSITION, - COVER_SET_COVER_POSITION_SCHEMA, + { + vol.Required(ATTR_POSITION): vol.All( + vol.Coerce(int), vol.Range(min=0, max=100) + ) + }, "async_set_cover_position", ) + component.async_register_entity_service(SERVICE_STOP_COVER, {}, "async_stop_cover") + + component.async_register_entity_service(SERVICE_TOGGLE, {}, "async_toggle") + component.async_register_entity_service( - SERVICE_STOP_COVER, ENTITY_SERVICE_SCHEMA, "async_stop_cover" + SERVICE_OPEN_COVER_TILT, {}, "async_open_cover_tilt" ) component.async_register_entity_service( - SERVICE_TOGGLE, ENTITY_SERVICE_SCHEMA, "async_toggle" + SERVICE_CLOSE_COVER_TILT, {}, "async_close_cover_tilt" ) component.async_register_entity_service( - SERVICE_OPEN_COVER_TILT, ENTITY_SERVICE_SCHEMA, "async_open_cover_tilt" - ) - - component.async_register_entity_service( - SERVICE_CLOSE_COVER_TILT, ENTITY_SERVICE_SCHEMA, "async_close_cover_tilt" - ) - - component.async_register_entity_service( - SERVICE_STOP_COVER_TILT, ENTITY_SERVICE_SCHEMA, "async_stop_cover_tilt" + SERVICE_STOP_COVER_TILT, {}, "async_stop_cover_tilt" ) component.async_register_entity_service( SERVICE_SET_COVER_TILT_POSITION, - COVER_SET_COVER_TILT_POSITION_SCHEMA, + { + vol.Required(ATTR_TILT_POSITION): vol.All( + vol.Coerce(int), vol.Range(min=0, max=100) + ) + }, "async_set_cover_tilt_position", ) component.async_register_entity_service( - SERVICE_TOGGLE_COVER_TILT, ENTITY_SERVICE_SCHEMA, "async_toggle_tilt" - ) - - hass.helpers.intent.async_register( - intent.ServiceIntentHandler( - INTENT_OPEN_COVER, DOMAIN, SERVICE_OPEN_COVER, "Opened {}" - ) - ) - hass.helpers.intent.async_register( - intent.ServiceIntentHandler( - INTENT_CLOSE_COVER, DOMAIN, SERVICE_CLOSE_COVER, "Closed {}" - ) + SERVICE_TOGGLE_COVER_TILT, {}, "async_toggle_tilt" ) return True diff --git a/homeassistant/components/cover/device_condition.py b/homeassistant/components/cover/device_condition.py index 487f815afb5..ec6da84e5f6 100644 --- a/homeassistant/components/cover/device_condition.py +++ b/homeassistant/components/cover/device_condition.py @@ -1,5 +1,6 @@ """Provides device automations for Cover.""" from typing import Any, Dict, List + import voluptuous as vol from homeassistant.const import ( @@ -8,14 +9,14 @@ from homeassistant.const import ( CONF_ABOVE, CONF_BELOW, CONF_CONDITION, - CONF_DOMAIN, - CONF_TYPE, CONF_DEVICE_ID, + CONF_DOMAIN, CONF_ENTITY_ID, - STATE_OPEN, + CONF_TYPE, STATE_CLOSED, - STATE_OPENING, STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import ( @@ -24,8 +25,9 @@ from homeassistant.helpers import ( entity_registry, template, ) -from homeassistant.helpers.typing import ConfigType, TemplateVarsType from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA +from homeassistant.helpers.typing import ConfigType, TemplateVarsType + from . import ( DOMAIN, SUPPORT_CLOSE, diff --git a/homeassistant/components/cover/device_trigger.py b/homeassistant/components/cover/device_trigger.py index 4f256a87dc5..988427003e7 100644 --- a/homeassistant/components/cover/device_trigger.py +++ b/homeassistant/components/cover/device_trigger.py @@ -1,30 +1,32 @@ """Provides device automations for Cover.""" from typing import List + import voluptuous as vol +from homeassistant.components.automation import ( + AutomationActionType, + numeric_state as numeric_state_automation, + state as state_automation, +) +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, CONF_ABOVE, CONF_BELOW, - CONF_DOMAIN, - CONF_TYPE, - CONF_PLATFORM, CONF_DEVICE_ID, + CONF_DOMAIN, CONF_ENTITY_ID, + CONF_PLATFORM, + CONF_TYPE, STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING, ) -from homeassistant.core import HomeAssistant, CALLBACK_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 homeassistant.components.automation import ( - state as state_automation, - numeric_state as numeric_state_automation, - AutomationActionType, -) -from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA + from . import ( DOMAIN, SUPPORT_CLOSE, diff --git a/homeassistant/components/cover/intent.py b/homeassistant/components/cover/intent.py new file mode 100644 index 00000000000..36402025bfa --- /dev/null +++ b/homeassistant/components/cover/intent.py @@ -0,0 +1,22 @@ +"""Intents for the cover integration.""" +from homeassistant.core import HomeAssistant +from homeassistant.helpers import intent + +from . import DOMAIN, SERVICE_CLOSE_COVER, SERVICE_OPEN_COVER + +INTENT_OPEN_COVER = "HassOpenCover" +INTENT_CLOSE_COVER = "HassCloseCover" + + +async def async_setup_intents(hass: HomeAssistant) -> None: + """Set up the cover intents.""" + hass.helpers.intent.async_register( + intent.ServiceIntentHandler( + INTENT_OPEN_COVER, DOMAIN, SERVICE_OPEN_COVER, "Opened {}" + ) + ) + hass.helpers.intent.async_register( + intent.ServiceIntentHandler( + INTENT_CLOSE_COVER, DOMAIN, SERVICE_CLOSE_COVER, "Closed {}" + ) + ) diff --git a/homeassistant/components/cups/sensor.py b/homeassistant/components/cups/sensor.py index 4af51e911a1..7581891af6a 100644 --- a/homeassistant/components/cups/sensor.py +++ b/homeassistant/components/cups/sensor.py @@ -1,14 +1,14 @@ """Details about printers which are connected to CUPS.""" +from datetime import timedelta import importlib import logging -from datetime import timedelta import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA 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 _LOGGER = logging.getLogger(__name__) @@ -306,7 +306,6 @@ class MarkerSensor(Entity): self._attributes = self.data.attributes -# pylint: disable=no-name-in-module class CupsData: """Get the latest data from CUPS and update the state.""" diff --git a/homeassistant/components/currencylayer/sensor.py b/homeassistant/components/currencylayer/sensor.py index d4660d70286..cbad07c0284 100644 --- a/homeassistant/components/currencylayer/sensor.py +++ b/homeassistant/components/currencylayer/sensor.py @@ -5,15 +5,15 @@ import logging import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_API_KEY, - CONF_NAME, - CONF_BASE, - CONF_QUOTE, ATTR_ATTRIBUTION, + CONF_API_KEY, + CONF_BASE, + CONF_NAME, + CONF_QUOTE, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/daikin/__init__.py b/homeassistant/components/daikin/__init__.py index 7d476e17647..209bf71e594 100644 --- a/homeassistant/components/daikin/__init__.py +++ b/homeassistant/components/daikin/__init__.py @@ -5,6 +5,7 @@ import logging from aiohttp import ClientConnectionError from async_timeout import timeout +from pydaikin.appliance import Appliance import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry @@ -87,7 +88,6 @@ async def async_unload_entry(hass, config_entry): async def daikin_api_setup(hass, host): """Create a Daikin instance only once.""" - from pydaikin.appliance import Appliance session = hass.helpers.aiohttp_client.async_get_clientsession() try: diff --git a/homeassistant/components/daikin/climate.py b/homeassistant/components/daikin/climate.py index ddc5353250c..d46ea26d487 100644 --- a/homeassistant/components/daikin/climate.py +++ b/homeassistant/components/daikin/climate.py @@ -1,6 +1,7 @@ """Support for the Daikin HVAC.""" import logging +from pydaikin import appliance import voluptuous as vol from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice @@ -91,7 +92,6 @@ class DaikinClimate(ClimateDevice): def __init__(self, api): """Initialize the climate device.""" - from pydaikin import appliance self._api = api self._list = { diff --git a/homeassistant/components/daikin/config_flow.py b/homeassistant/components/daikin/config_flow.py index 36d8ef0d383..bd90a87db86 100644 --- a/homeassistant/components/daikin/config_flow.py +++ b/homeassistant/components/daikin/config_flow.py @@ -4,6 +4,7 @@ import logging from aiohttp import ClientError from async_timeout import timeout +from pydaikin.appliance import Appliance import voluptuous as vol from homeassistant import config_entries @@ -32,7 +33,6 @@ class FlowHandler(config_entries.ConfigFlow): async def _create_device(self, host): """Create device.""" - from pydaikin.appliance import Appliance try: device = Appliance( diff --git a/homeassistant/components/danfoss_air/__init__.py b/homeassistant/components/danfoss_air/__init__.py index adfb13e722c..b1dbf890eb9 100644 --- a/homeassistant/components/danfoss_air/__init__.py +++ b/homeassistant/components/danfoss_air/__init__.py @@ -2,6 +2,8 @@ from datetime import timedelta import logging +from pydanfossair.commands import ReadCommand +from pydanfossair.danfossclient import DanfossClient import voluptuous as vol from homeassistant.const import CONF_HOST @@ -40,8 +42,6 @@ class DanfossAir: """Initialize the Danfoss Air CCM connection.""" self._data = {} - from pydanfossair.danfossclient import DanfossClient - self._client = DanfossClient(host) def get_value(self, item): @@ -56,7 +56,6 @@ class DanfossAir: def update(self): """Use the data from Danfoss Air API.""" _LOGGER.debug("Fetching data from Danfoss Air CCM module") - from pydanfossair.commands import ReadCommand self._data[ReadCommand.exhaustTemperature] = self._client.command( ReadCommand.exhaustTemperature diff --git a/homeassistant/components/darksky/sensor.py b/homeassistant/components/darksky/sensor.py index cd8417e3e84..5b6da5d11bb 100644 --- a/homeassistant/components/darksky/sensor.py +++ b/homeassistant/components/darksky/sensor.py @@ -1,12 +1,11 @@ """Support for Dark Sky weather service.""" -import logging from datetime import timedelta +import logging import forecastio -import voluptuous as vol from requests.exceptions import ConnectionError as ConnectError, HTTPError, Timeout +import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( ATTR_ATTRIBUTION, @@ -15,9 +14,10 @@ from homeassistant.const import ( CONF_LONGITUDE, CONF_MONITORED_CONDITIONS, CONF_NAME, - UNIT_UV_INDEX, CONF_SCAN_INTERVAL, + UNIT_UV_INDEX, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -385,7 +385,7 @@ CONDITION_PICTURES = { ], "partly-cloudy-night": [ "/static/images/darksky/weather-cloudy.svg", - "mdi:weather-partly-cloudy", + "mdi:weather-night-partly-cloudy", ], } diff --git a/homeassistant/components/datadog/__init__.py b/homeassistant/components/datadog/__init__.py index 5517e41d5c6..adb8bb1f95c 100644 --- a/homeassistant/components/datadog/__init__.py +++ b/homeassistant/components/datadog/__init__.py @@ -1,6 +1,7 @@ """Support for sending data to Datadog.""" import logging +from datadog import initialize, statsd import voluptuous as vol from homeassistant.const import ( @@ -42,7 +43,6 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): """Set up the Datadog component.""" - from datadog import initialize, statsd conf = config[DOMAIN] host = conf.get(CONF_HOST) diff --git a/homeassistant/components/deconz/.translations/bg.json b/homeassistant/components/deconz/.translations/bg.json index e8dc5845c13..fb75fc81f5f 100644 --- a/homeassistant/components/deconz/.translations/bg.json +++ b/homeassistant/components/deconz/.translations/bg.json @@ -24,12 +24,12 @@ "init": { "data": { "host": "\u0410\u0434\u0440\u0435\u0441", - "port": "\u041f\u043e\u0440\u0442 (\u0441\u0442\u043e\u0439\u043d\u043e\u0441\u0442 \u043f\u043e \u043f\u043e\u0434\u0440\u0430\u0437\u0431\u0438\u0440\u0430\u043d\u0435: '80')" + "port": "\u041f\u043e\u0440\u0442" }, "title": "\u0414\u0435\u0444\u0438\u043d\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 deCONZ \u0448\u043b\u044e\u0437" }, "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\u0432\u043e\u0440\u0435\u0442\u0435 \u0441\u0438\u0441\u0442\u0435\u043c\u043d\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043d\u0430 deCONZ\n2. \u041d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0435 \u0431\u0443\u0442\u043e\u043d\u0430 \"Unlock Gateway\"", + "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\"", "title": "\u0412\u0440\u044a\u0437\u043a\u0430 \u0441 deCONZ" }, "options": { @@ -40,7 +40,7 @@ "title": "\u0414\u043e\u043f\u044a\u043b\u043d\u0438\u0442\u0435\u043b\u043d\u0438 \u043e\u043f\u0446\u0438\u0438 \u0437\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 deCONZ" } }, - "title": "deCONZ" + "title": "deCONZ Zigbee \u0448\u043b\u044e\u0437" }, "device_automation": { "trigger_subtype": { @@ -55,10 +55,17 @@ "left": "\u041b\u044f\u0432\u043e", "open": "\u041e\u0442\u0432\u0430\u0440\u044f\u043d\u0435", "right": "\u0414\u044f\u0441\u043d\u043e", + "side_1": "\u0421\u0442\u0440\u0430\u043d\u0430 1", + "side_2": "\u0421\u0442\u0440\u0430\u043d\u0430 2", + "side_3": "\u0421\u0442\u0440\u0430\u043d\u0430 3", + "side_4": "\u0421\u0442\u0440\u0430\u043d\u0430 4", + "side_5": "\u0421\u0442\u0440\u0430\u043d\u0430 5", + "side_6": "\u0421\u0442\u0440\u0430\u043d\u0430 6", "turn_off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0438", "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438" }, "trigger_type": { + "remote_awakened": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0441\u0435 \u0441\u044a\u0431\u0443\u0434\u0438", "remote_button_double_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u0434\u0432\u0443\u043a\u0440\u0430\u0442\u043d\u043e", "remote_button_long_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u043f\u0440\u043e\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e", "remote_button_long_release": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043e\u0442\u043f\u0443\u0441\u043d\u0430\u0442 \u0441\u043b\u0435\u0434 \u043f\u0440\u043e\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", @@ -69,7 +76,16 @@ "remote_button_short_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442", "remote_button_short_release": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043e\u0442\u043f\u0443\u0441\u043d\u0430\u0442", "remote_button_triple_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u0442\u0440\u0438\u043a\u0440\u0430\u0442\u043d\u043e", - "remote_gyro_activated": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u0440\u0430\u0437\u043a\u043b\u0430\u0442\u0435\u043d\u043e" + "remote_double_tap": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \"{subtype}\" \u0435 \u043f\u043e\u0447\u0443\u043a\u0430\u043d\u043e \u0434\u0432\u0430 \u043f\u044a\u0442\u0438", + "remote_falling": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u043f\u0430\u0434\u0430", + "remote_gyro_activated": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u0440\u0430\u0437\u043a\u043b\u0430\u0442\u0435\u043d\u043e", + "remote_moved": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u043f\u0440\u0435\u043c\u0435\u0441\u0442\u0435\u043d\u043e \u0441 \"{subtype}\" \u043d\u0430\u0433\u043e\u0440\u0435", + "remote_rotate_from_side_1": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u0437\u0430\u0432\u044a\u0440\u0442\u044f\u043d\u043e \u043e\u0442 \"\u0441\u0442\u0440\u0430\u043d\u0430 1\" \u043a\u044a\u043c \" {subtype} \"", + "remote_rotate_from_side_2": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u0437\u0430\u0432\u044a\u0440\u0442\u044f\u043d\u043e \u043e\u0442 \"\u0441\u0442\u0440\u0430\u043d\u0430 2\" \u043a\u044a\u043c \" {subtype} \"", + "remote_rotate_from_side_3": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u0437\u0430\u0432\u044a\u0440\u0442\u044f\u043d\u043e \u043e\u0442 \"\u0441\u0442\u0440\u0430\u043d\u0430 3\" \u043a\u044a\u043c \" {subtype} \"", + "remote_rotate_from_side_4": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u0437\u0430\u0432\u044a\u0440\u0442\u044f\u043d\u043e \u043e\u0442 \"\u0441\u0442\u0440\u0430\u043d\u0430 4\" \u043a\u044a\u043c \" {subtype} \"", + "remote_rotate_from_side_5": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u0437\u0430\u0432\u044a\u0440\u0442\u044f\u043d\u043e \u043e\u0442 \"\u0441\u0442\u0440\u0430\u043d\u0430 5\" \u043a\u044a\u043c \" {subtype} \"", + "remote_rotate_from_side_6": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u0437\u0430\u0432\u044a\u0440\u0442\u044f\u043d\u043e \u043e\u0442 \"\u0441\u0442\u0440\u0430\u043d\u0430 6\" \u043a\u044a\u043c \" {subtype} \"" } }, "options": { diff --git a/homeassistant/components/deconz/.translations/da.json b/homeassistant/components/deconz/.translations/da.json index ec9c4dc35b1..1f0828274f0 100644 --- a/homeassistant/components/deconz/.translations/da.json +++ b/homeassistant/components/deconz/.translations/da.json @@ -2,11 +2,11 @@ "config": { "abort": { "already_configured": "Bridge er allerede konfigureret", - "already_in_progress": "Bro konfiguration er allerede i gang.", - "no_bridges": "Ingen deConz bridge fundet", - "not_deconz_bridge": "Ikke en deCONZ bro", - "one_instance_only": "Komponenten underst\u00f8tter kun \u00e9n deCONZ forekomst", - "updated_instance": "Opdaterede deCONZ instans med ny v\u00e6rtsadresse" + "already_in_progress": "Konfigurationsflow for bro er allerede i gang.", + "no_bridges": "Ingen deConz-bridge fundet", + "not_deconz_bridge": "Ikke en deCONZ-bro", + "one_instance_only": "Komponenten underst\u00f8tter kun \u00e9n deCONZ-instans", + "updated_instance": "Opdaterede deCONZ-instans med ny v\u00e6rtadresse" }, "error": { "no_key": "Kunne ikke f\u00e5 en API-n\u00f8gle" @@ -16,28 +16,28 @@ "hassio_confirm": { "data": { "allow_clip_sensor": "Tillad import af virtuelle sensorer", - "allow_deconz_groups": "Tillad import af deCONZ grupper" + "allow_deconz_groups": "Tillad import af deCONZ-grupper" }, - "description": "Vil du konfigurere Home Assistant til at oprette forbindelse til deCONZ gateway leveret af Hass.io add-on {addon}?", - "title": "deCONZ Zigbee-gateway via Hass.io add-on" + "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" }, "init": { "data": { "host": "V\u00e6rt", "port": "Port" }, - "title": "Definer deCONZ gateway" + "title": "Definer deCONZ-gateway" }, "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\"", - "title": "Link med deCONZ" + "title": "Forbind med deCONZ" }, "options": { "data": { "allow_clip_sensor": "Tillad import af virtuelle sensorer", - "allow_deconz_groups": "Tillad importering af deCONZ grupper" + "allow_deconz_groups": "Tillad import af deCONZ-grupper" }, - "title": "Ekstra konfiguration valgmuligheder for deCONZ" + "title": "Ekstra konfigurationsindstillinger for deCONZ" } }, "title": "deCONZ Zigbee gateway" @@ -54,25 +54,53 @@ "dim_up": "D\u00e6mp op", "left": "Venstre", "open": "\u00c5ben", - "right": "H\u00f8jre" + "right": "H\u00f8jre", + "side_1": "Side 1", + "side_2": "Side 2", + "side_3": "Side 3", + "side_4": "Side 4", + "side_5": "Side 5", + "side_6": "Side 6", + "turn_off": "Sluk", + "turn_on": "T\u00e6nd" }, "trigger_type": { - "remote_gyro_activated": "Enhed rystet" + "remote_awakened": "Enheden v\u00e6kket", + "remote_button_double_press": "\"{subtype}\"-knappen er dobbeltklikket", + "remote_button_long_press": "\"{subtype}\"-knappen trykket p\u00e5 konstant", + "remote_button_long_release": "\"{subtype}\"-knappen frigivet efter langt tryk", + "remote_button_quadruple_press": "\"{subtype}\"-knappen firedobbelt-klikket", + "remote_button_quintuple_press": "\"{subtype}\"-knappen femdobbelt-klikket", + "remote_button_rotated": "Knap roteret \"{subtype}\"", + "remote_button_rotation_stopped": "Knaprotation \"{subtype}\" er stoppet", + "remote_button_short_press": "\"{subtype}\"-knappen trykket p\u00e5", + "remote_button_short_release": "\"{subtype}\"-knappen frigivet", + "remote_button_triple_press": "\"{subtype}\"-knappen tredobbeltklikkes", + "remote_double_tap": "Enheden \"{subtype}\" dobbelttappet", + "remote_falling": "Enheden er i frit fald", + "remote_gyro_activated": "Enhed rystet", + "remote_moved": "Enheden flyttede med \"{subtype}\" op", + "remote_rotate_from_side_1": "Enhed roteret fra \"side 1\" til \"{subtype}\"", + "remote_rotate_from_side_2": "Enhed roteret fra \"side 2\" til \"{subtype}\"", + "remote_rotate_from_side_3": "Enhed roteret fra \"side 3\" til \"{subtype}\"", + "remote_rotate_from_side_4": "Enhed roteret fra \"side 4\" til \"{subtype}\"", + "remote_rotate_from_side_5": "Enhed roteret fra \"side 5\" til \"{subtype}\"", + "remote_rotate_from_side_6": "Enhed roteret fra \"side 6\" til \"{subtype}\"" } }, "options": { "step": { "async_step_deconz_devices": { "data": { - "allow_clip_sensor": "Tillad deCONZ CLIP sensorer", - "allow_deconz_groups": "Tillad deCONZ lys grupper" + "allow_clip_sensor": "Tillad deCONZ CLIP-sensorer", + "allow_deconz_groups": "Tillad deCONZ-lysgrupper" }, "description": "Konfigurer synligheden af deCONZ-enhedstyper" }, "deconz_devices": { "data": { - "allow_clip_sensor": "Tillad deCONZ CLIP sensorer", - "allow_deconz_groups": "Tillad deCONZ lys grupper" + "allow_clip_sensor": "Tillad deCONZ CLIP-sensorer", + "allow_deconz_groups": "Tillad deCONZ-lysgrupper" }, "description": "Konfigurer synligheden af deCONZ-enhedstyper" } diff --git a/homeassistant/components/deconz/.translations/es.json b/homeassistant/components/deconz/.translations/es.json index 47fd99c48a2..adbe68153f7 100644 --- a/homeassistant/components/deconz/.translations/es.json +++ b/homeassistant/components/deconz/.translations/es.json @@ -72,7 +72,7 @@ "remote_button_quadruple_press": "Bot\u00f3n \"{subtype}\" pulsado cuatro veces consecutivas", "remote_button_quintuple_press": "Bot\u00f3n \"{subtype}\" pulsado cinco veces consecutivas", "remote_button_rotated": "Bot\u00f3n \"{subtype}\" girado", - "remote_button_rotation_stopped": "Bot\u00f3n rotativo \"{subtipo}\" detenido", + "remote_button_rotation_stopped": "Bot\u00f3n rotativo \"{subtype}\" detenido", "remote_button_short_press": "Bot\u00f3n \"{subtype}\" pulsado", "remote_button_short_release": "Bot\u00f3n \"{subtype}\" liberado", "remote_button_triple_press": "Bot\u00f3n \"{subtype}\" pulsado cuatro veces consecutivas", diff --git a/homeassistant/components/deconz/.translations/fr.json b/homeassistant/components/deconz/.translations/fr.json index 4d49bd18d1e..1a4232e0817 100644 --- a/homeassistant/components/deconz/.translations/fr.json +++ b/homeassistant/components/deconz/.translations/fr.json @@ -76,7 +76,16 @@ "remote_button_short_press": "Bouton \"{subtype}\" appuy\u00e9", "remote_button_short_release": "Bouton \"{subtype}\" rel\u00e2ch\u00e9", "remote_button_triple_press": "Bouton \"{subtype}\" triple cliqu\u00e9", - "remote_gyro_activated": "Appareil secou\u00e9" + "remote_double_tap": "Appareil \"{subtype}\" tapot\u00e9 deux fois", + "remote_falling": "Appareil en chute libre", + "remote_gyro_activated": "Appareil secou\u00e9", + "remote_moved": "Appareil d\u00e9plac\u00e9 avec \"{subtype}\" vers le haut", + "remote_rotate_from_side_1": "Appareil tourn\u00e9 de \"c\u00f4t\u00e9 1\" \u00e0 \"{subtype}\"", + "remote_rotate_from_side_2": "Appareil tourn\u00e9 de \"c\u00f4t\u00e9 2\" \u00e0 \"{subtype}\"", + "remote_rotate_from_side_3": "Appareil tourn\u00e9 de \"c\u00f4t\u00e9 3\" \u00e0 \"{subtype}\"", + "remote_rotate_from_side_4": "Appareil tourn\u00e9 de \"c\u00f4t\u00e9 4\" \u00e0 \"{subtype}\"", + "remote_rotate_from_side_5": "Appareil tourn\u00e9 de \"c\u00f4t\u00e9 5\" \u00e0 \"{subtype}\"", + "remote_rotate_from_side_6": "Appareil tourn\u00e9 de \"c\u00f4t\u00e9 6\" \u00e0 \"{subtype}\"" } }, "options": { diff --git a/homeassistant/components/deconz/.translations/it.json b/homeassistant/components/deconz/.translations/it.json index 33d49dfca46..99e5622129f 100644 --- a/homeassistant/components/deconz/.translations/it.json +++ b/homeassistant/components/deconz/.translations/it.json @@ -18,8 +18,8 @@ "allow_clip_sensor": "Consenti l'importazione di sensori virtuali", "allow_deconz_groups": "Consenti l'importazione di gruppi deCONZ" }, - "description": "Vuoi configurare Home Assistant per connettersi al gateway deCONZ fornito dal componente aggiuntivo hass.io {addon} ?", - "title": "Gateway Zigbee deCONZ tramite l'add-on Hass.io" + "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" }, "init": { "data": { diff --git a/homeassistant/components/deconz/.translations/ko.json b/homeassistant/components/deconz/.translations/ko.json index 61725316b13..2c9b864dfc1 100644 --- a/homeassistant/components/deconz/.translations/ko.json +++ b/homeassistant/components/deconz/.translations/ko.json @@ -55,21 +55,37 @@ "left": "\uc67c\ucabd", "open": "\uc5f4\uae30", "right": "\uc624\ub978\ucabd", + "side_1": "\uba74 1", + "side_2": "\uba74 2", + "side_3": "\uba74 3", + "side_4": "\uba74 4", + "side_5": "\uba74 5", + "side_6": "\uba74 6", "turn_off": "\ub044\uae30", "turn_on": "\ucf1c\uae30" }, "trigger_type": { - "remote_button_double_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub450 \ubc88 \ub204\ub984", - "remote_button_long_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \uacc4\uc18d \ub204\ub984", - "remote_button_long_release": "\"{subtype}\" \ubc84\ud2bc\uc744 \uae38\uac8c \ub20c\ub800\ub2e4\uac00 \ub5cc", - "remote_button_quadruple_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub124 \ubc88 \ub204\ub984", - "remote_button_quintuple_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub2e4\uc12f \ubc88 \ub204\ub984", - "remote_button_rotated": "\"{subtype}\" \ubc84\ud2bc\uc744 \ud68c\uc804", - "remote_button_rotation_stopped": "\"{subtype}\" \ubc84\ud2bc\uc744 \ud68c\uc804 \uc815\uc9c0", - "remote_button_short_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub204\ub984", - "remote_button_short_release": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub5cc", - "remote_button_triple_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \uc138 \ubc88 \ub204\ub984", - "remote_gyro_activated": "\uae30\uae30 \ud754\ub4e6" + "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_falling": "\uae30\uae30\uac00 \ub5a8\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_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" } }, "options": { diff --git a/homeassistant/components/deconz/.translations/ro.json b/homeassistant/components/deconz/.translations/ro.json new file mode 100644 index 00000000000..2d6fc6a39fb --- /dev/null +++ b/homeassistant/components/deconz/.translations/ro.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "init": { + "data": { + "port": "Port" + } + }, + "link": { + "description": "Debloca\u021bi gateway-ul DECONZ pentru a v\u0103 \u00eenregistra la Home Assistant. \n\n 1. Accesa\u021bi Set\u0103rile deCONZ - > Gateway - > Avansat \n 2. Ap\u0103sa\u021bi butonul \u201eAutentifica\u021bi aplica\u021bia\u201d" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index 1a4d9680c1e..0fdc5904c2d 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -8,7 +8,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import ATTR_DARK, ATTR_ON, NEW_SENSOR from .deconz_device import DeconzDevice -from .gateway import get_gateway_from_config_entry, DeconzEntityHandler +from .gateway import DeconzEntityHandler, get_gateway_from_config_entry ATTR_ORIENTATION = "orientation" ATTR_TILTANGLE = "tiltangle" diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index b9a299230ad..c84192456d1 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -1,13 +1,14 @@ """Config flow to configure deCONZ component.""" import asyncio +from urllib.parse import urlparse import async_timeout +from pydeconz.errors import RequestError, ResponseError +from pydeconz.utils import async_discovery, async_get_api_key, async_get_gateway_config import voluptuous as vol -from pydeconz.errors import ResponseError, RequestError -from pydeconz.utils import async_discovery, async_get_api_key, async_get_gateway_config - from homeassistant import config_entries +from homeassistant.components import ssdp from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT from homeassistant.core import callback from homeassistant.helpers import aiohttp_client @@ -26,7 +27,6 @@ from .const import ( DECONZ_MANUFACTURERURL = "http://www.dresden-elektronik.de" CONF_SERIAL = "serial" -ATTR_UUID = "udn" @callback @@ -159,31 +159,42 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): title="deCONZ-" + self.deconz_config[CONF_BRIDGEID], data=self.deconz_config ) - async def _update_entry(self, entry, host): + def _update_entry(self, entry, host, port, api_key=None): """Update existing entry.""" - if entry.data[CONF_HOST] == host: + if ( + entry.data[CONF_HOST] == host + and entry.data[CONF_PORT] == port + and (api_key is None or entry.data[CONF_API_KEY] == api_key) + ): return self.async_abort(reason="already_configured") entry.data[CONF_HOST] = host + entry.data[CONF_PORT] = port + + if api_key is not None: + entry.data[CONF_API_KEY] = api_key + self.hass.config_entries.async_update_entry(entry) return self.async_abort(reason="updated_instance") async def async_step_ssdp(self, discovery_info): """Handle a discovered deCONZ bridge.""" - from homeassistant.components.ssdp import ATTR_MANUFACTURERURL, ATTR_SERIAL - - if discovery_info[ATTR_MANUFACTURERURL] != DECONZ_MANUFACTURERURL: + if discovery_info[ssdp.ATTR_UPNP_MANUFACTURER_URL] != DECONZ_MANUFACTURERURL: return self.async_abort(reason="not_deconz_bridge") - uuid = discovery_info[ATTR_UUID].replace("uuid:", "") + uuid = discovery_info[ssdp.ATTR_UPNP_UDN].replace("uuid:", "") _LOGGER.debug("deCONZ gateway discovered (%s)", uuid) + parsed_url = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]) + for entry in self.hass.config_entries.async_entries(DOMAIN): if uuid == entry.data.get(CONF_UUID): - return await self._update_entry(entry, discovery_info[CONF_HOST]) + if entry.source == "hassio": + return self.async_abort(reason="already_configured") + return self._update_entry(entry, parsed_url.hostname, parsed_url.port) - bridgeid = discovery_info[ATTR_SERIAL] + bridgeid = discovery_info[ssdp.ATTR_UPNP_SERIAL] if any( bridgeid == flow["context"][CONF_BRIDGEID] for flow in self._async_in_progress() @@ -192,11 +203,11 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context[CONF_BRIDGEID] = bridgeid - self.context["title_placeholders"] = {"host": discovery_info[CONF_HOST]} + self.context["title_placeholders"] = {"host": parsed_url.hostname} self.deconz_config = { - CONF_HOST: discovery_info[CONF_HOST], - CONF_PORT: discovery_info[CONF_PORT], + CONF_HOST: parsed_url.hostname, + CONF_PORT: parsed_url.port, } return await self.async_step_link() @@ -211,7 +222,12 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if bridgeid in gateway_entries: entry = gateway_entries[bridgeid] - return await self._update_entry(entry, user_input[CONF_HOST]) + return self._update_entry( + entry, + user_input[CONF_HOST], + user_input[CONF_PORT], + user_input[CONF_API_KEY], + ) self._hassio_discovery = user_input diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index ad23a564272..a663f99bf73 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -26,10 +26,10 @@ SUPPORTED_PLATFORMS = [ "switch", ] -NEW_GROUP = "group" -NEW_LIGHT = "light" -NEW_SCENE = "scene" -NEW_SENSOR = "sensor" +NEW_GROUP = "groups" +NEW_LIGHT = "lights" +NEW_SCENE = "scenes" +NEW_SENSOR = "sensors" NEW_DEVICE = { NEW_GROUP: "deconz_new_group_{}", diff --git a/homeassistant/components/deconz/cover.py b/homeassistant/components/deconz/cover.py index bcd408c25a7..6e5e616fbb8 100644 --- a/homeassistant/components/deconz/cover.py +++ b/homeassistant/components/deconz/cover.py @@ -1,11 +1,11 @@ """Support for deCONZ covers.""" from homeassistant.components.cover import ( ATTR_POSITION, - CoverDevice, SUPPORT_CLOSE, SUPPORT_OPEN, - SUPPORT_STOP, SUPPORT_SET_POSITION, + SUPPORT_STOP, + CoverDevice, ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index 9d4a944d695..9c8a41453aa 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -2,7 +2,6 @@ import voluptuous as vol import homeassistant.components.automation.event as event - from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, @@ -148,6 +147,7 @@ TRADFRI_WIRELESS_DIMMER = { } AQARA_CUBE_MODEL = "lumi.sensor_cube" +AQARA_CUBE_MODEL_ALT1 = "lumi.sensor_cube.aqgl01" AQARA_CUBE = { (CONF_ROTATE_FROM_SIDE_1, CONF_SIDE_2): 6002, (CONF_ROTATE_FROM_SIDE_1, CONF_SIDE_3): 3002, @@ -216,6 +216,13 @@ AQARA_DOUBLE_WALL_SWITCH_WXKG02LM = { (CONF_SHORT_PRESS, CONF_BOTH_BUTTONS): 3002, } +AQARA_SINGLE_WALL_SWITCH_WXKG03LM_MODEL = "lumi.remote.b186acn01" +AQARA_SINGLE_WALL_SWITCH_WXKG03LM = { + (CONF_SHORT_PRESS, CONF_TURN_ON): 1002, + (CONF_LONG_PRESS, CONF_TURN_ON): 1001, + (CONF_DOUBLE_PRESS, CONF_TURN_ON): 1004, +} + AQARA_MINI_SWITCH_MODEL = "lumi.remote.b1acn01" AQARA_MINI_SWITCH = { (CONF_SHORT_PRESS, CONF_TURN_ON): 1002, @@ -245,6 +252,14 @@ AQARA_SQUARE_SWITCH = { (CONF_SHAKE, ""): 1007, } +AQARA_SQUARE_SWITCH_WXKG11LM_2016_MODEL = "lumi.sensor_switch.aq2" +AQARA_SQUARE_SWITCH_WXKG11LM_2016 = { + (CONF_SHORT_PRESS, CONF_TURN_ON): 1002, + (CONF_DOUBLE_PRESS, CONF_TURN_ON): 1004, + (CONF_TRIPLE_PRESS, CONF_TURN_ON): 1005, + (CONF_QUADRUPLE_PRESS, CONF_TURN_ON): 1006, +} + REMOTES = { HUE_DIMMER_REMOTE_MODEL_GEN1: HUE_DIMMER_REMOTE, HUE_DIMMER_REMOTE_MODEL_GEN2: HUE_DIMMER_REMOTE, @@ -255,11 +270,14 @@ REMOTES = { TRADFRI_REMOTE_MODEL: TRADFRI_REMOTE, TRADFRI_WIRELESS_DIMMER_MODEL: TRADFRI_WIRELESS_DIMMER, AQARA_CUBE_MODEL: AQARA_CUBE, + AQARA_CUBE_MODEL_ALT1: AQARA_CUBE, AQARA_DOUBLE_WALL_SWITCH_MODEL: AQARA_DOUBLE_WALL_SWITCH, AQARA_DOUBLE_WALL_SWITCH_WXKG02LM_MODEL: AQARA_DOUBLE_WALL_SWITCH_WXKG02LM, + AQARA_SINGLE_WALL_SWITCH_WXKG03LM_MODEL: AQARA_SINGLE_WALL_SWITCH_WXKG03LM, AQARA_MINI_SWITCH_MODEL: AQARA_MINI_SWITCH, AQARA_ROUND_SWITCH_MODEL: AQARA_ROUND_SWITCH, AQARA_SQUARE_SWITCH_MODEL: AQARA_SQUARE_SWITCH, + AQARA_SQUARE_SWITCH_WXKG11LM_2016_MODEL: AQARA_SQUARE_SWITCH_WXKG11LM_2016, } TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( @@ -290,7 +308,11 @@ async def async_validate_trigger_config(hass, config): trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) - if device.model not in REMOTES or trigger not in REMOTES[device.model]: + if ( + not device + or device.model not in REMOTES + or trigger not in REMOTES[device.model] + ): raise InvalidDeviceAutomationConfig return config diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index 75898b0fdab..083af2dca6f 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -1,12 +1,12 @@ """Representation of a deCONZ gateway.""" import asyncio -import async_timeout +import async_timeout from pydeconz import DeconzSession, errors -from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.const import CONF_HOST +from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT from homeassistant.core import callback +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import ( @@ -14,8 +14,8 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_send, ) from homeassistant.helpers.entity_registry import ( - async_get_registry, DISABLED_CONFIG_ENTRY, + async_get_registry, ) from .const import ( @@ -30,7 +30,6 @@ from .const import ( NEW_DEVICE, SUPPORTED_PLATFORMS, ) - from .errors import AuthenticationRequired, CannotConnect @@ -43,13 +42,13 @@ def get_gateway_from_config_entry(hass, config_entry): class DeconzGateway: """Manages a single deCONZ gateway.""" - def __init__(self, hass, config_entry): + def __init__(self, hass, config_entry) -> None: """Initialize the system.""" self.hass = hass self.config_entry = config_entry + self.available = True self.api = None - self.deconz_ids = {} self.events = [] self.listeners = [] @@ -78,7 +77,7 @@ class DeconzGateway: CONF_ALLOW_DECONZ_GROUPS, DEFAULT_ALLOW_DECONZ_GROUPS ) - async def async_update_device_registry(self): + async def async_update_device_registry(self) -> None: """Update device registry.""" device_registry = await self.hass.helpers.device_registry.async_get_registry() device_registry.async_get_or_create( @@ -91,7 +90,7 @@ class DeconzGateway: sw_version=self.api.config.swversion, ) - async def async_setup(self): + async def async_setup(self) -> bool: """Set up a deCONZ gateway.""" hass = self.hass @@ -106,8 +105,8 @@ class DeconzGateway: except CannotConnect: raise ConfigEntryNotReady - except Exception: # pylint: disable=broad-except - _LOGGER.error("Error connecting with deCONZ gateway") + except Exception as err: # pylint: disable=broad-except + _LOGGER.error("Error connecting with deCONZ gateway: %s", err) return False for component in SUPPORTED_PLATFORMS: @@ -125,7 +124,7 @@ class DeconzGateway: return True @staticmethod - async def async_new_address(hass, entry): + async def async_new_address(hass, entry) -> None: """Handle signals of gateway getting new address. This is a static method because a class method (bound method), @@ -138,23 +137,23 @@ class DeconzGateway: gateway.api.start() @property - def signal_reachable(self): + def signal_reachable(self) -> str: """Gateway specific event to signal a change in connection status.""" return f"deconz-reachable-{self.bridgeid}" @callback - def async_connection_status_callback(self, available): + def async_connection_status_callback(self, available) -> None: """Handle signals of gateway connection status.""" self.available = available async_dispatcher_send(self.hass, self.signal_reachable, True) @property - def signal_options_update(self): + def signal_options_update(self) -> str: """Event specific per deCONZ entry to signal new options.""" return f"deconz-options-{self.bridgeid}" @staticmethod - async def async_options_updated(hass, entry): + async def async_options_updated(hass, entry) -> None: """Triggered by config entry options updates.""" gateway = get_gateway_from_config_entry(hass, entry) @@ -162,12 +161,12 @@ class DeconzGateway: async_dispatcher_send(hass, gateway.signal_options_update, registry) @callback - def async_signal_new_device(self, device_type): + def async_signal_new_device(self, device_type) -> str: """Gateway specific event to signal new device.""" return NEW_DEVICE[device_type].format(self.bridgeid) @callback - def async_add_device_callback(self, device_type, device): + def async_add_device_callback(self, device_type, device) -> None: """Handle event of new device creation in deCONZ.""" if not isinstance(device, list): device = [device] @@ -176,7 +175,7 @@ class DeconzGateway: ) @callback - def shutdown(self, event): + def shutdown(self, event) -> None: """Wrap the call to deconz.close. Used as an argument to EventBus.async_listen_once. @@ -207,20 +206,21 @@ class DeconzGateway: async def get_gateway( hass, config, async_add_device_callback, async_connection_status_callback -): +) -> DeconzSession: """Create a gateway object and verify configuration.""" session = aiohttp_client.async_get_clientsession(hass) deconz = DeconzSession( - hass.loop, session, - **config, + config[CONF_HOST], + config[CONF_PORT], + config[CONF_API_KEY], async_add_device=async_add_device_callback, connection_status=async_connection_status_callback, ) try: with async_timeout.timeout(10): - await deconz.async_load_parameters() + await deconz.initialize() return deconz except errors.Unauthorized: @@ -235,7 +235,7 @@ async def get_gateway( class DeconzEntityHandler: """Platform entity handler to help with updating disabled by.""" - def __init__(self, gateway): + def __init__(self, gateway) -> None: """Create an entity handler.""" self.gateway = gateway self._entities = [] @@ -247,12 +247,12 @@ class DeconzEntityHandler: ) @callback - def add_entity(self, entity): + def add_entity(self, entity) -> None: """Add a new entity to handler.""" self._entities.append(entity) @callback - def update_entity_registry(self, entity_registry): + def update_entity_registry(self, entity_registry) -> None: """Update entity registry disabled by status.""" for entity in self._entities: diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index bf4b05089a8..af708a15391 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -29,7 +29,7 @@ from .const import ( SWITCH_TYPES, ) from .deconz_device import DeconzDevice -from .gateway import get_gateway_from_config_entry, DeconzEntityHandler +from .gateway import DeconzEntityHandler, get_gateway_from_config_entry async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -152,6 +152,8 @@ class DeconzLight(DeconzDevice, Light): if ATTR_TRANSITION in kwargs: data["transitiontime"] = int(kwargs[ATTR_TRANSITION] * 10) + elif "IKEA" in (self._device.manufacturer or ""): + data["transitiontime"] = 0 if ATTR_FLASH in kwargs: if kwargs[ATTR_FLASH] == FLASH_SHORT: diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index 64902002600..30b00600331 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/deconz", "requirements": [ - "pydeconz==64" + "pydeconz==65" ], "ssdp": [ { diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index 3a3dbceb46b..4ffaba9b499 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -11,7 +11,7 @@ from homeassistant.helpers.dispatcher import ( from .const import ATTR_DARK, ATTR_ON, NEW_SENSOR from .deconz_device import DeconzDevice from .deconz_event import DeconzEvent -from .gateway import get_gateway_from_config_entry, DeconzEntityHandler +from .gateway import DeconzEntityHandler, get_gateway_from_config_entry ATTR_CURRENT = "current" ATTR_POWER = "power" @@ -59,7 +59,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entity_handler.add_entity(new_sensor) entities.append(new_sensor) - if sensor.battery: + if sensor.battery is not None: new_battery = DeconzBattery(sensor, gateway) if new_battery.unique_id not in batteries: batteries.add(new_battery.unique_id) @@ -225,6 +225,9 @@ class DeconzBatteryHandler: @callback def create_tracker(self, sensor): """Create new tracker for battery state.""" + for tracker in self._trackers: + if sensor == tracker.sensor: + return self._trackers.add(DeconzSensorStateTracker(sensor, self.gateway)) @callback diff --git a/homeassistant/components/deconz/services.py b/homeassistant/components/deconz/services.py index 3498b46d879..9d133acdb1d 100644 --- a/homeassistant/components/deconz/services.py +++ b/homeassistant/components/deconz/services.py @@ -4,7 +4,15 @@ import voluptuous as vol from homeassistant.helpers import config_validation as cv from .config_flow import get_master_gateway -from .const import CONF_BRIDGEID, DOMAIN, _LOGGER +from .const import ( + _LOGGER, + CONF_BRIDGEID, + DOMAIN, + NEW_GROUP, + NEW_LIGHT, + NEW_SCENE, + NEW_SENSOR, +) DECONZ_SERVICES = "deconz_services" @@ -105,7 +113,7 @@ async def async_configure_service(hass, data): _LOGGER.error("Could not find the entity %s", entity_id) return - await gateway.api.async_put_state(field, data) + await gateway.api.request("put", field, json=data) async def async_refresh_devices_service(hass, data): @@ -119,10 +127,10 @@ async def async_refresh_devices_service(hass, data): scenes = set(gateway.api.scenes.keys()) sensors = set(gateway.api.sensors.keys()) - await gateway.api.async_load_parameters() + await gateway.api.refresh_state() gateway.async_add_device_callback( - "group", + NEW_GROUP, [ group for group_id, group in gateway.api.groups.items() @@ -131,7 +139,7 @@ async def async_refresh_devices_service(hass, data): ) gateway.async_add_device_callback( - "light", + NEW_LIGHT, [ light for light_id, light in gateway.api.lights.items() @@ -140,7 +148,7 @@ async def async_refresh_devices_service(hass, data): ) gateway.async_add_device_callback( - "scene", + NEW_SCENE, [ scene for scene_id, scene in gateway.api.scenes.items() @@ -149,7 +157,7 @@ async def async_refresh_devices_service(hass, data): ) gateway.async_add_device_callback( - "sensor", + NEW_SENSOR, [ sensor for sensor_id, sensor in gateway.api.sensors.items() diff --git a/homeassistant/components/decora/light.py b/homeassistant/components/decora/light.py index 6ca427f2476..f4035352e51 100644 --- a/homeassistant/components/decora/light.py +++ b/homeassistant/components/decora/light.py @@ -62,8 +62,7 @@ def retry(method): return method(device, *args, **kwargs) except (decora.decoraException, AttributeError, BTLEException): _LOGGER.warning( - "Decora connect error for device %s. " "Reconnecting...", - device.name, + "Decora connect error for device %s. Reconnecting...", device.name, ) # pylint: disable=protected-access device._switch.connect() diff --git a/homeassistant/components/decora_wifi/light.py b/homeassistant/components/decora_wifi/light.py index 77f3a6387ed..7d8aa104bb0 100644 --- a/homeassistant/components/decora_wifi/light.py +++ b/homeassistant/components/decora_wifi/light.py @@ -2,17 +2,22 @@ import logging +# pylint: disable=import-error +from decora_wifi import DecoraWiFiSession +from decora_wifi.models.person import Person +from decora_wifi.models.residence import Residence +from decora_wifi.models.residential_account import ResidentialAccount import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_TRANSITION, - Light, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_TRANSITION, + Light, ) -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -28,11 +33,6 @@ NOTIFICATION_TITLE = "myLeviton Decora Setup" def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Decora WiFi platform.""" - # pylint: disable=import-error, no-name-in-module - from decora_wifi import DecoraWiFiSession - from decora_wifi.models.person import Person - from decora_wifi.models.residential_account import ResidentialAccount - from decora_wifi.models.residence import Residence email = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) diff --git a/homeassistant/components/deluge/sensor.py b/homeassistant/components/deluge/sensor.py index 098484cf7ae..7df87490c60 100644 --- a/homeassistant/components/deluge/sensor.py +++ b/homeassistant/components/deluge/sensor.py @@ -1,21 +1,22 @@ """Support for monitoring the Deluge BitTorrent client API.""" import logging +from deluge_client import DelugeRPCClient, FailedToReconnectException import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_HOST, - CONF_PASSWORD, - CONF_USERNAME, - CONF_NAME, - CONF_PORT, CONF_MONITORED_VARIABLES, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, STATE_IDLE, ) -from homeassistant.helpers.entity import Entity 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 @@ -46,7 +47,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Deluge sensors.""" - from deluge_client import DelugeRPCClient name = config.get(CONF_NAME) host = config.get(CONF_HOST) @@ -103,7 +103,6 @@ class DelugeSensor(Entity): def update(self): """Get the latest data from Deluge and updates the state.""" - from deluge_client import FailedToReconnectException try: self.data = self.client.call( diff --git a/homeassistant/components/deluge/switch.py b/homeassistant/components/deluge/switch.py index 981ef129b47..7ac98f284c8 100644 --- a/homeassistant/components/deluge/switch.py +++ b/homeassistant/components/deluge/switch.py @@ -1,21 +1,22 @@ """Support for setting the Deluge BitTorrent client in Pause.""" import logging +from deluge_client import DelugeRPCClient, FailedToReconnectException import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA -from homeassistant.exceptions import PlatformNotReady from homeassistant.const import ( CONF_HOST, CONF_NAME, - CONF_PORT, CONF_PASSWORD, + CONF_PORT, CONF_USERNAME, STATE_OFF, STATE_ON, ) -from homeassistant.helpers.entity import ToggleEntity +from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import ToggleEntity _LOGGER = logging.getLogger(__name__) @@ -35,7 +36,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Deluge switch.""" - from deluge_client import DelugeRPCClient name = config.get(CONF_NAME) host = config.get(CONF_HOST) @@ -95,7 +95,6 @@ class DelugeSwitch(ToggleEntity): def update(self): """Get the latest data from deluge and updates the state.""" - from deluge_client import FailedToReconnectException try: torrent_list = self.deluge_client.call( diff --git a/homeassistant/components/demo/.translations/bg.json b/homeassistant/components/demo/.translations/bg.json new file mode 100644 index 00000000000..3b1f5f8a8d2 --- /dev/null +++ b/homeassistant/components/demo/.translations/bg.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "\u0414\u0435\u043c\u043e\u043d\u0441\u0442\u0440\u0430\u0446\u0438\u044f" + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/.translations/da.json b/homeassistant/components/demo/.translations/da.json new file mode 100644 index 00000000000..ef01fcb4f3c --- /dev/null +++ b/homeassistant/components/demo/.translations/da.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Demo" + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/.translations/de.json b/homeassistant/components/demo/.translations/de.json new file mode 100644 index 00000000000..ef01fcb4f3c --- /dev/null +++ b/homeassistant/components/demo/.translations/de.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Demo" + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/.translations/es.json b/homeassistant/components/demo/.translations/es.json new file mode 100644 index 00000000000..ef01fcb4f3c --- /dev/null +++ b/homeassistant/components/demo/.translations/es.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Demo" + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/.translations/hu.json b/homeassistant/components/demo/.translations/hu.json new file mode 100644 index 00000000000..51f0cd00642 --- /dev/null +++ b/homeassistant/components/demo/.translations/hu.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Dem\u00f3" + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/.translations/it.json b/homeassistant/components/demo/.translations/it.json new file mode 100644 index 00000000000..ef01fcb4f3c --- /dev/null +++ b/homeassistant/components/demo/.translations/it.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Demo" + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/.translations/ja.json b/homeassistant/components/demo/.translations/ja.json new file mode 100644 index 00000000000..529170b111d --- /dev/null +++ b/homeassistant/components/demo/.translations/ja.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "\u30c7\u30e2" + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/.translations/ko.json b/homeassistant/components/demo/.translations/ko.json new file mode 100644 index 00000000000..d20943c7b36 --- /dev/null +++ b/homeassistant/components/demo/.translations/ko.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "\ub370\ubaa8" + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/.translations/nl.json b/homeassistant/components/demo/.translations/nl.json new file mode 100644 index 00000000000..ef01fcb4f3c --- /dev/null +++ b/homeassistant/components/demo/.translations/nl.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Demo" + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/.translations/pt-BR.json b/homeassistant/components/demo/.translations/pt-BR.json new file mode 100644 index 00000000000..8183f28aed3 --- /dev/null +++ b/homeassistant/components/demo/.translations/pt-BR.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Demonstra\u00e7\u00e3o" + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index 05febfad603..b6845d9d6a4 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -4,8 +4,8 @@ import logging import time from homeassistant import bootstrap, config_entries -import homeassistant.core as ha from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START +import homeassistant.core as ha DOMAIN = "demo" _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/demo/alarm_control_panel.py b/homeassistant/components/demo/alarm_control_panel.py index d82edadf161..0323b68b1b0 100644 --- a/homeassistant/components/demo/alarm_control_panel.py +++ b/homeassistant/components/demo/alarm_control_panel.py @@ -1,16 +1,17 @@ """Demo platform that has two fake alarm control panels.""" import datetime + from homeassistant.components.manual.alarm_control_panel import ManualAlarm from homeassistant.const import ( + CONF_DELAY_TIME, + CONF_PENDING_TIME, + CONF_TRIGGER_TIME, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, - CONF_DELAY_TIME, - CONF_PENDING_TIME, - CONF_TRIGGER_TIME, ) diff --git a/homeassistant/components/demo/binary_sensor.py b/homeassistant/components/demo/binary_sensor.py index c1e42807f6d..0f6dfa9f357 100644 --- a/homeassistant/components/demo/binary_sensor.py +++ b/homeassistant/components/demo/binary_sensor.py @@ -1,5 +1,6 @@ """Demo platform that has two fake binary sensors.""" from homeassistant.components.binary_sensor import BinarySensorDevice + from . import DOMAIN diff --git a/homeassistant/components/demo/calendar.py b/homeassistant/components/demo/calendar.py index 4ae836466f0..42cb2b137a1 100644 --- a/homeassistant/components/demo/calendar.py +++ b/homeassistant/components/demo/calendar.py @@ -1,9 +1,8 @@ """Demo platform that has two fake binary sensors.""" import copy -import homeassistant.util.dt as dt_util - from homeassistant.components.calendar import CalendarEventDevice, get_date +import homeassistant.util.dt as dt_util def setup_platform(hass, config, add_entities, discovery_info=None): diff --git a/homeassistant/components/demo/climate.py b/homeassistant/components/demo/climate.py index f4affed7ced..0edcf618ba6 100644 --- a/homeassistant/components/demo/climate.py +++ b/homeassistant/components/demo/climate.py @@ -1,11 +1,13 @@ """Demo platform that offers a fake climate device.""" import logging + from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, CURRENT_HVAC_COOL, CURRENT_HVAC_HEAT, + HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, @@ -18,9 +20,9 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_HUMIDITY, SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, - HVAC_MODE_AUTO, ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT + from . import DOMAIN SUPPORT_FLAGS = 0 diff --git a/homeassistant/components/demo/const.py b/homeassistant/components/demo/const.py new file mode 100644 index 00000000000..e11b0b0731a --- /dev/null +++ b/homeassistant/components/demo/const.py @@ -0,0 +1,3 @@ +"""Constants for the Demo component.""" +DOMAIN = "demo" +SERVICE_RANDOMIZE_DEVICE_TRACKER_DATA = "randomize_device_tracker_data" diff --git a/homeassistant/components/demo/cover.py b/homeassistant/components/demo/cover.py index 20a8747aaa5..20e3a52aa8d 100644 --- a/homeassistant/components/demo/cover.py +++ b/homeassistant/components/demo/cover.py @@ -1,6 +1,4 @@ """Demo platform for the cover component.""" -from homeassistant.helpers.event import track_utc_time_change - from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION, @@ -8,6 +6,8 @@ from homeassistant.components.cover import ( SUPPORT_OPEN, CoverDevice, ) +from homeassistant.helpers.event import track_utc_time_change + from . import DOMAIN diff --git a/homeassistant/components/demo/device_tracker.py b/homeassistant/components/demo/device_tracker.py index fba8095efd6..02864111527 100644 --- a/homeassistant/components/demo/device_tracker.py +++ b/homeassistant/components/demo/device_tracker.py @@ -1,7 +1,7 @@ """Demo platform for the Device tracker component.""" import random -from homeassistant.components.device_tracker import DOMAIN +from .const import DOMAIN, SERVICE_RANDOMIZE_DEVICE_TRACKER_DATA def setup_scanner(hass, config, see, discovery_info=None): @@ -36,6 +36,6 @@ def setup_scanner(hass, config, see, discovery_info=None): battery=53, ) - hass.services.register(DOMAIN, "demo", observe) + hass.services.register(DOMAIN, SERVICE_RANDOMIZE_DEVICE_TRACKER_DATA, observe) return True diff --git a/homeassistant/components/demo/fan.py b/homeassistant/components/demo/fan.py index 500d5f6a5ce..966ba51cacb 100644 --- a/homeassistant/components/demo/fan.py +++ b/homeassistant/components/demo/fan.py @@ -1,6 +1,4 @@ """Demo fan platform that has a fake fan.""" -from homeassistant.const import STATE_OFF - from homeassistant.components.fan import ( SPEED_HIGH, SPEED_LOW, @@ -10,6 +8,7 @@ from homeassistant.components.fan import ( SUPPORT_SET_SPEED, FanEntity, ) +from homeassistant.const import STATE_OFF FULL_SUPPORT = SUPPORT_SET_SPEED | SUPPORT_OSCILLATE | SUPPORT_DIRECTION LIMITED_SUPPORT = SUPPORT_SET_SPEED diff --git a/homeassistant/components/demo/geo_location.py b/homeassistant/components/demo/geo_location.py index 6a7aa7ddce1..6fc8e9c2e89 100644 --- a/homeassistant/components/demo/geo_location.py +++ b/homeassistant/components/demo/geo_location.py @@ -5,9 +5,8 @@ from math import cos, pi, radians, sin import random from typing import Optional -from homeassistant.helpers.event import track_time_interval - from homeassistant.components.geo_location import GeolocationEvent +from homeassistant.helpers.event import track_time_interval _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/demo/image_processing.py b/homeassistant/components/demo/image_processing.py index 348045e47b2..9183609509e 100644 --- a/homeassistant/components/demo/image_processing.py +++ b/homeassistant/components/demo/image_processing.py @@ -1,10 +1,10 @@ """Support for the demo image processing.""" from homeassistant.components.image_processing import ( - ImageProcessingFaceEntity, - ATTR_CONFIDENCE, - ATTR_NAME, ATTR_AGE, + ATTR_CONFIDENCE, ATTR_GENDER, + ATTR_NAME, + ImageProcessingFaceEntity, ) from homeassistant.components.openalpr_local.image_processing import ( ImageProcessingAlprEntity, diff --git a/homeassistant/components/demo/lock.py b/homeassistant/components/demo/lock.py index 923469f045c..5074741d83d 100644 --- a/homeassistant/components/demo/lock.py +++ b/homeassistant/components/demo/lock.py @@ -1,7 +1,6 @@ """Demo lock platform that has two fake locks.""" -from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED - from homeassistant.components.lock import SUPPORT_OPEN, LockDevice +from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): diff --git a/homeassistant/components/demo/media_player.py b/homeassistant/components/demo/media_player.py index 9d7c3892af8..33fe4ee3647 100644 --- a/homeassistant/components/demo/media_player.py +++ b/homeassistant/components/demo/media_player.py @@ -340,7 +340,7 @@ class DemoMusicPlayer(AbstractDemoPlayer): @property def media_image_url(self): """Return the image url of current playing media.""" - return "https://graph.facebook.com/v2.5/107771475912710/" "picture?type=large" + return "https://graph.facebook.com/v2.5/107771475912710/picture?type=large" @property def media_title(self): diff --git a/homeassistant/components/demo/sensor.py b/homeassistant/components/demo/sensor.py index bf5df94e74c..d2b2464468b 100644 --- a/homeassistant/components/demo/sensor.py +++ b/homeassistant/components/demo/sensor.py @@ -1,11 +1,12 @@ """Demo platform that has a couple of fake sensors.""" from homeassistant.const import ( ATTR_BATTERY_LEVEL, - TEMP_CELSIUS, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, + TEMP_CELSIUS, ) from homeassistant.helpers.entity import Entity + from . import DOMAIN diff --git a/homeassistant/components/demo/services.yaml b/homeassistant/components/demo/services.yaml index e69de29bb2d..a8a96b21c19 100644 --- a/homeassistant/components/demo/services.yaml +++ b/homeassistant/components/demo/services.yaml @@ -0,0 +1,2 @@ +randomize_device_tracker_data: + description: Demonstrates using a device tracker to see where devices are located \ No newline at end of file diff --git a/homeassistant/components/demo/switch.py b/homeassistant/components/demo/switch.py index 23006cff875..5c651198f5c 100644 --- a/homeassistant/components/demo/switch.py +++ b/homeassistant/components/demo/switch.py @@ -1,6 +1,7 @@ """Demo platform that has two fake switches.""" from homeassistant.components.switch import SwitchDevice from homeassistant.const import DEVICE_DEFAULT_NAME + from . import DOMAIN diff --git a/homeassistant/components/demo/water_heater.py b/homeassistant/components/demo/water_heater.py index c3fff26c992..f9aca141245 100644 --- a/homeassistant/components/demo/water_heater.py +++ b/homeassistant/components/demo/water_heater.py @@ -1,12 +1,11 @@ """Demo platform that offers a fake water heater device.""" -from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT - from homeassistant.components.water_heater import ( SUPPORT_AWAY_MODE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, WaterHeaterDevice, ) +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT SUPPORT_FLAGS_HEATER = ( SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE | SUPPORT_AWAY_MODE diff --git a/homeassistant/components/denon/media_player.py b/homeassistant/components/denon/media_player.py index 7bed8423e8f..cd9d6e8feb7 100644 --- a/homeassistant/components/denon/media_player.py +++ b/homeassistant/components/denon/media_player.py @@ -4,7 +4,7 @@ import telnetlib import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, diff --git a/homeassistant/components/deutsche_bahn/sensor.py b/homeassistant/components/deutsche_bahn/sensor.py index ad7b40f78db..204518b2ce3 100644 --- a/homeassistant/components/deutsche_bahn/sensor.py +++ b/homeassistant/components/deutsche_bahn/sensor.py @@ -2,9 +2,8 @@ from datetime import timedelta import logging -import voluptuous as vol - import schiene +import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index 80e64033295..872a4af6cd6 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -1,22 +1,21 @@ """Helpers for device automations.""" import asyncio import logging -from typing import Any, List, MutableMapping from types import ModuleType +from typing import Any, List, MutableMapping import voluptuous as vol import voluptuous_serialize -from homeassistant.const import CONF_PLATFORM, CONF_DOMAIN, CONF_DEVICE_ID from homeassistant.components import websocket_api +from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_registry import async_entries_for_device -from homeassistant.loader import async_get_integration, IntegrationNotFound +from homeassistant.loader import IntegrationNotFound, async_get_integration from .exceptions import InvalidDeviceAutomationConfig - # mypy: allow-untyped-calls, allow-untyped-defs DOMAIN = "device_automation" diff --git a/homeassistant/components/device_automation/toggle_entity.py b/homeassistant/components/device_automation/toggle_entity.py index 5f01f4d9d71..7d84eb921e9 100644 --- a/homeassistant/components/device_automation/toggle_entity.py +++ b/homeassistant/components/device_automation/toggle_entity.py @@ -1,11 +1,11 @@ """Device automation helpers for toggle entity.""" from typing import Any, Dict, List + import voluptuous as vol -from homeassistant.core import Context, HomeAssistant, CALLBACK_TYPE from homeassistant.components.automation import ( - state as state_automation, AutomationActionType, + state as state_automation, ) from homeassistant.components.device_automation.const import ( CONF_IS_OFF, @@ -24,11 +24,12 @@ from homeassistant.const import ( CONF_PLATFORM, CONF_TYPE, ) -from homeassistant.helpers.entity_registry import async_entries_for_device +from homeassistant.core import CALLBACK_TYPE, Context, HomeAssistant from homeassistant.helpers import condition, config_validation as cv +from homeassistant.helpers.entity_registry import async_entries_for_device from homeassistant.helpers.typing import ConfigType, TemplateVarsType -from . import TRIGGER_BASE_SCHEMA +from . import TRIGGER_BASE_SCHEMA # mypy: allow-untyped-calls, allow-untyped-defs diff --git a/homeassistant/components/device_sun_light_trigger/__init__.py b/homeassistant/components/device_sun_light_trigger/__init__.py index 9a058cfacc1..64831cfac89 100644 --- a/homeassistant/components/device_sun_light_trigger/__init__.py +++ b/homeassistant/components/device_sun_light_trigger/__init__.py @@ -1,11 +1,9 @@ """Support to turn on lights based on the states.""" -import logging from datetime import timedelta +import logging import voluptuous as vol -from homeassistant.core import callback -import homeassistant.util.dt as dt_util from homeassistant.components.light import ( ATTR_PROFILE, ATTR_TRANSITION, @@ -20,12 +18,14 @@ from homeassistant.const import ( SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET, ) +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import ( async_track_point_in_utc_time, async_track_state_change, ) -from homeassistant.helpers.sun import is_up, get_astral_event_next -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.sun import get_astral_event_next, is_up +import homeassistant.util.dt as dt_util DOMAIN = "device_sun_light_trigger" CONF_DEVICE_GROUP = "device_group" diff --git a/homeassistant/components/device_tracker/.translations/bg.json b/homeassistant/components/device_tracker/.translations/bg.json index 471cbc6a53a..68affa5afd0 100644 --- a/homeassistant/components/device_tracker/.translations/bg.json +++ b/homeassistant/components/device_tracker/.translations/bg.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_home": "{entity_name} \u0435 \u0443 \u0434\u043e\u043c\u0430", "is_not_home": "{entity_name} \u043d\u0435 \u0435 \u0443 \u0434\u043e\u043c\u0430" } diff --git a/homeassistant/components/device_tracker/.translations/ca.json b/homeassistant/components/device_tracker/.translations/ca.json index de5aed41e3c..3a95841559b 100644 --- a/homeassistant/components/device_tracker/.translations/ca.json +++ b/homeassistant/components/device_tracker/.translations/ca.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_home": "{entity_name} \u00e9s a casa", "is_not_home": "{entity_name} no \u00e9s a casa" } diff --git a/homeassistant/components/device_tracker/.translations/cs.json b/homeassistant/components/device_tracker/.translations/cs.json index 778ea0208c4..7e82f1a34f8 100644 --- a/homeassistant/components/device_tracker/.translations/cs.json +++ b/homeassistant/components/device_tracker/.translations/cs.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_home": "{entity_name} je doma", "is_not_home": "{entity_name} nen\u00ed doma" } diff --git a/homeassistant/components/device_tracker/.translations/da.json b/homeassistant/components/device_tracker/.translations/da.json new file mode 100644 index 00000000000..d714b5b7d31 --- /dev/null +++ b/homeassistant/components/device_tracker/.translations/da.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condition_type": { + "is_home": "{entity_name} er hjemme", + "is_not_home": "{entity_name} er ikke hjemme" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/device_tracker/.translations/de.json b/homeassistant/components/device_tracker/.translations/de.json index 7e72bd5595a..90a81db6b90 100644 --- a/homeassistant/components/device_tracker/.translations/de.json +++ b/homeassistant/components/device_tracker/.translations/de.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_home": "{entity_name} ist Zuhause", "is_not_home": "{entity_name} ist nicht zu Hause" } diff --git a/homeassistant/components/device_tracker/.translations/en.json b/homeassistant/components/device_tracker/.translations/en.json index 25045e62b15..1022608477e 100644 --- a/homeassistant/components/device_tracker/.translations/en.json +++ b/homeassistant/components/device_tracker/.translations/en.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_home": "{entity_name} is home", "is_not_home": "{entity_name} is not home" } diff --git a/homeassistant/components/device_tracker/.translations/es.json b/homeassistant/components/device_tracker/.translations/es.json index 00bda928b56..cfbf7bcfe3e 100644 --- a/homeassistant/components/device_tracker/.translations/es.json +++ b/homeassistant/components/device_tracker/.translations/es.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_home": "{entity_name} est\u00e1 en casa", "is_not_home": "{entity_name} no est\u00e1 en casa" } diff --git a/homeassistant/components/device_tracker/.translations/fr.json b/homeassistant/components/device_tracker/.translations/fr.json index bf9033170c1..4c59d5ea1c8 100644 --- a/homeassistant/components/device_tracker/.translations/fr.json +++ b/homeassistant/components/device_tracker/.translations/fr.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_home": "{entity_name} est \u00e0 la maison", "is_not_home": "{entity_name} n'est pas \u00e0 la maison" } diff --git a/homeassistant/components/device_tracker/.translations/it.json b/homeassistant/components/device_tracker/.translations/it.json index e2d35296152..112afc6689f 100644 --- a/homeassistant/components/device_tracker/.translations/it.json +++ b/homeassistant/components/device_tracker/.translations/it.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_home": "{entity_name} \u00e8 in casa", "is_not_home": "{entity_name} non \u00e8 in casa" } diff --git a/homeassistant/components/device_tracker/.translations/ko.json b/homeassistant/components/device_tracker/.translations/ko.json index d258f67db22..1834767222a 100644 --- a/homeassistant/components/device_tracker/.translations/ko.json +++ b/homeassistant/components/device_tracker/.translations/ko.json @@ -1,8 +1,8 @@ { "device_automation": { - "condtion_type": { - "is_home": "{entity_name} \uc774(\uac00) \uc9d1\uc5d0 \uc788\uc2b5\ub2c8\ub2e4", - "is_not_home": "{entity_name} \uc774(\uac00) \uc678\ucd9c\uc911\uc785\ub2c8\ub2e4" + "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" } } } \ No newline at end of file diff --git a/homeassistant/components/device_tracker/.translations/lb.json b/homeassistant/components/device_tracker/.translations/lb.json index 98a066ef8e8..2c49f692662 100644 --- a/homeassistant/components/device_tracker/.translations/lb.json +++ b/homeassistant/components/device_tracker/.translations/lb.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_home": "{entity_name} ass doheem", "is_not_home": "{entity_name} ass net doheem" } diff --git a/homeassistant/components/device_tracker/.translations/nl.json b/homeassistant/components/device_tracker/.translations/nl.json index d4de8b1f66a..31ab788f171 100644 --- a/homeassistant/components/device_tracker/.translations/nl.json +++ b/homeassistant/components/device_tracker/.translations/nl.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_home": "{entity_name} is thuis", "is_not_home": "{entity_name} is niet thuis" } diff --git a/homeassistant/components/device_tracker/.translations/no.json b/homeassistant/components/device_tracker/.translations/no.json index 7034378b066..d714b5b7d31 100644 --- a/homeassistant/components/device_tracker/.translations/no.json +++ b/homeassistant/components/device_tracker/.translations/no.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_home": "{entity_name} er hjemme", "is_not_home": "{entity_name} er ikke hjemme" } diff --git a/homeassistant/components/device_tracker/.translations/pl.json b/homeassistant/components/device_tracker/.translations/pl.json index 8f0f7953a2d..3930031ad38 100644 --- a/homeassistant/components/device_tracker/.translations/pl.json +++ b/homeassistant/components/device_tracker/.translations/pl.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_home": "urz\u0105dzenie {entity_name} jest w domu", "is_not_home": "urz\u0105dzenie {entity_name} jest poza domem" } diff --git a/homeassistant/components/device_tracker/.translations/pt.json b/homeassistant/components/device_tracker/.translations/pt.json index 952eb4b1475..8a8f662183a 100644 --- a/homeassistant/components/device_tracker/.translations/pt.json +++ b/homeassistant/components/device_tracker/.translations/pt.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_home": "{entity_name} est\u00e1 em casa", "is_not_home": "{entity_name} n\u00e3o est\u00e1 em casa" } diff --git a/homeassistant/components/device_tracker/.translations/ru.json b/homeassistant/components/device_tracker/.translations/ru.json index 50a48ce942b..58767361fd4 100644 --- a/homeassistant/components/device_tracker/.translations/ru.json +++ b/homeassistant/components/device_tracker/.translations/ru.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_home": "{entity_name} \u0434\u043e\u043c\u0430", "is_not_home": "{entity_name} \u043d\u0435 \u0434\u043e\u043c\u0430" } diff --git a/homeassistant/components/device_tracker/.translations/sl.json b/homeassistant/components/device_tracker/.translations/sl.json index f4784fbc664..11d876883d3 100644 --- a/homeassistant/components/device_tracker/.translations/sl.json +++ b/homeassistant/components/device_tracker/.translations/sl.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_home": "{entity_name} je doma", "is_not_home": "{entity_name} ni doma" } diff --git a/homeassistant/components/device_tracker/.translations/zh-Hant.json b/homeassistant/components/device_tracker/.translations/zh-Hant.json index 4092031434c..456e09ebf0e 100644 --- a/homeassistant/components/device_tracker/.translations/zh-Hant.json +++ b/homeassistant/components/device_tracker/.translations/zh-Hant.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_home": "{entity_name} \u5728\u5bb6", "is_not_home": "{entity_name} \u4e0d\u5728\u5bb6" } diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 25e33d2a2db..a160e580c57 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -3,21 +3,19 @@ import asyncio import voluptuous as vol -from homeassistant.loader import bind_hass from homeassistant.components import group +from homeassistant.const import ATTR_GPS_ACCURACY, STATE_HOME from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import GPSType, ConfigType, HomeAssistantType - from homeassistant.helpers.event import async_track_utc_time_change -from homeassistant.const import ATTR_GPS_ACCURACY, STATE_HOME +from homeassistant.helpers.typing import ConfigType, GPSType, HomeAssistantType +from homeassistant.loader import bind_hass from . import legacy, setup from .config_entry import ( # noqa: F401 pylint: disable=unused-import async_setup_entry, async_unload_entry, ) -from .legacy import DeviceScanner # noqa: F401 pylint: disable=unused-import from .const import ( ATTR_ATTRIBUTES, ATTR_BATTERY, @@ -38,11 +36,12 @@ from .const import ( DEFAULT_TRACK_NEW, DOMAIN, PLATFORM_TYPE_LEGACY, - SOURCE_TYPE_BLUETOOTH_LE, SOURCE_TYPE_BLUETOOTH, + SOURCE_TYPE_BLUETOOTH_LE, SOURCE_TYPE_GPS, SOURCE_TYPE_ROUTER, ) +from .legacy import DeviceScanner # noqa: F401 pylint: disable=unused-import ENTITY_ID_ALL_DEVICES = group.ENTITY_ID_FORMAT.format("all_devices") diff --git a/homeassistant/components/device_tracker/config_entry.py b/homeassistant/components/device_tracker/config_entry.py index 9e53c2e0cea..6c5cacac591 100644 --- a/homeassistant/components/device_tracker/config_entry.py +++ b/homeassistant/components/device_tracker/config_entry.py @@ -3,12 +3,12 @@ from typing import Optional from homeassistant.components import zone from homeassistant.const import ( - STATE_NOT_HOME, - STATE_HOME, + ATTR_BATTERY_LEVEL, ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, - ATTR_BATTERY_LEVEL, + STATE_HOME, + STATE_NOT_HOME, ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent diff --git a/homeassistant/components/device_tracker/device_condition.py b/homeassistant/components/device_tracker/device_condition.py index 6379aca6c0b..9bdfc12db39 100644 --- a/homeassistant/components/device_tracker/device_condition.py +++ b/homeassistant/components/device_tracker/device_condition.py @@ -1,21 +1,23 @@ """Provides device automations for Device tracker.""" from typing import Dict, List + import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, CONF_CONDITION, - CONF_DOMAIN, - CONF_TYPE, CONF_DEVICE_ID, + CONF_DOMAIN, CONF_ENTITY_ID, - STATE_NOT_HOME, + CONF_TYPE, STATE_HOME, + STATE_NOT_HOME, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import condition, config_validation as cv, entity_registry -from homeassistant.helpers.typing import ConfigType, TemplateVarsType from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA +from homeassistant.helpers.typing import ConfigType, TemplateVarsType + from . import DOMAIN CONDITION_TYPES = {"is_home", "is_not_home"} diff --git a/homeassistant/components/device_tracker/services.yaml b/homeassistant/components/device_tracker/services.yaml index 938e9c8e324..51865034b00 100644 --- a/homeassistant/components/device_tracker/services.yaml +++ b/homeassistant/components/device_tracker/services.yaml @@ -24,40 +24,3 @@ see: battery: description: Battery level of device. example: '100' - -icloud_lost_iphone: - description: Service to play the lost iphone sound on an iDevice. - fields: - account_name: - description: Name of the account in the config that will be used to look for the device. This is optional, if it isn't given it will use all accounts. - example: 'bart' - device_name: - description: Name of the device that will play the sound. This is optional, if it isn't given it will play on all devices for the given account. - example: 'iphonebart' -icloud_set_interval: - description: Service to set the interval of an iDevice. - fields: - account_name: - description: Name of the account in the config that will be used to look for the device. This is optional, if it isn't given it will use all accounts. - example: 'bart' - device_name: - description: Name of the device that will get a new interval. This is optional, if it isn't given it will change the interval for all devices for the given account. - example: 'iphonebart' - interval: - description: The interval (in minutes) that the iDevice will have until the according device_tracker entity changes from zone or until this service is used again. This is optional, if it isn't given the interval of the device will revert back to the original interval based on the current state. - example: 1 -icloud_update: - description: Service to ask for an update of an iDevice. - fields: - account_name: - description: Name of the account in the config that will be used to look for the device. This is optional, if it isn't given it will use all accounts. - example: 'bart' - device_name: - description: Name of the device that will be updated. This is optional, if it isn't given it will update all devices for the given account. - example: 'iphonebart' -icloud_reset_account: - description: Service to restart an iCloud account. Helpful when not all devices are found after initializing or when you add a new device. - fields: - account_name: - description: Name of the account in the config that will be restarted. This is optional, if it isn't given it will restart all accounts. - example: 'bart' diff --git a/homeassistant/components/device_tracker/setup.py b/homeassistant/components/device_tracker/setup.py index 6c9f05dead7..42751b1a784 100644 --- a/homeassistant/components/device_tracker/setup.py +++ b/homeassistant/components/device_tracker/setup.py @@ -1,27 +1,26 @@ """Device tracker helpers.""" import asyncio -from typing import Dict, Any, Callable, Optional from types import ModuleType +from typing import Any, Callable, Dict, Optional import attr -from homeassistant.core import callback -from homeassistant.setup import async_prepare_setup_platform -from homeassistant.helpers import config_per_platform -from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from homeassistant.helpers.event import async_track_time_interval -from homeassistant.util import dt as dt_util from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE - +from homeassistant.core import callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import config_per_platform +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.setup import async_prepare_setup_platform +from homeassistant.util import dt as dt_util from .const import ( - DOMAIN, - PLATFORM_TYPE_LEGACY, CONF_SCAN_INTERVAL, + DOMAIN, + LOGGER, + PLATFORM_TYPE_LEGACY, SCAN_INTERVAL, SOURCE_TYPE_ROUTER, - LOGGER, ) diff --git a/homeassistant/components/device_tracker/strings.json b/homeassistant/components/device_tracker/strings.json index 7e0691654a0..285bac2cb4b 100644 --- a/homeassistant/components/device_tracker/strings.json +++ b/homeassistant/components/device_tracker/strings.json @@ -1,8 +1,8 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_home": "{entity_name} is home", "is_not_home": "{entity_name} is not home" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/dht/sensor.py b/homeassistant/components/dht/sensor.py index 648e0e1ed72..26b0493cb99 100644 --- a/homeassistant/components/dht/sensor.py +++ b/homeassistant/components/dht/sensor.py @@ -1,13 +1,13 @@ """Support for Adafruit DHT temperature and humidity sensor.""" -import logging 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.const import CONF_MONITORED_CONDITIONS, CONF_NAME, TEMP_FAHRENHEIT import homeassistant.helpers.config_validation as cv -from homeassistant.const import TEMP_FAHRENHEIT, CONF_NAME, CONF_MONITORED_CONDITIONS from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle from homeassistant.util.temperature import celsius_to_fahrenheit diff --git a/homeassistant/components/dialogflow/.translations/da.json b/homeassistant/components/dialogflow/.translations/da.json index 2fb203450a5..c682c07a8b9 100644 --- a/homeassistant/components/dialogflow/.translations/da.json +++ b/homeassistant/components/dialogflow/.translations/da.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "not_internet_accessible": "Dit Home Assistant system skal v\u00e6re tilg\u00e6ngeligt fra internettet for at modtage Dialogflow meddelelser.", + "not_internet_accessible": "Din Home Assistant-instans skal v\u00e6re tilg\u00e6ngelig fra internettet for at modtage Dialogflow-meddelelser", "one_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning" }, "create_entry": { - "default": "For at sende begivenheder til Home Assistant skal du konfigurere [Webhook integration med Dialogflow]({dialogflow_url}).\n\n Udfyld f\u00f8lgende oplysninger: \n\n - URL: `{webhook_url}`\n - Metode: POST\n - Indholdstype: application/json\n\n Se [dokumentationen]({docs_url}) for yderligere oplysninger." + "default": "For at sende h\u00e6ndelser til Home Assistant skal du konfigurere [webhook-integration med Dialogflow]({dialogflow_url}).\n\n Udfyld f\u00f8lgende oplysninger: \n\n - Webadresse: `{webhook_url}`\n - Metode: POST\n - Indholdstype: application/json\n\nSe [dokumentationen]({docs_url}) for yderligere oplysninger." }, "step": { "user": { diff --git a/homeassistant/components/dialogflow/.translations/ko.json b/homeassistant/components/dialogflow/.translations/ko.json index 91f15f1fb77..2010495d959 100644 --- a/homeassistant/components/dialogflow/.translations/ko.json +++ b/homeassistant/components/dialogflow/.translations/ko.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "Dialogflow \uc744 \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "Dialogflow \uc744 \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "Dialogflow Webhook \uc124\uc815" } }, diff --git a/homeassistant/components/dialogflow/__init__.py b/homeassistant/components/dialogflow/__init__.py index 45fee0f867e..ae3c0288aed 100644 --- a/homeassistant/components/dialogflow/__init__.py +++ b/homeassistant/components/dialogflow/__init__.py @@ -1,16 +1,15 @@ """Support for Dialogflow webhook.""" import logging -import voluptuous as vol from aiohttp import web +import voluptuous as vol from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import intent, template, config_entry_flow +from homeassistant.helpers import config_entry_flow, intent, template from .const import DOMAIN - _LOGGER = logging.getLogger(__name__) SOURCE = "Home Assistant Dialogflow" diff --git a/homeassistant/components/dialogflow/config_flow.py b/homeassistant/components/dialogflow/config_flow.py index 4f785392ffc..fee99898ccc 100644 --- a/homeassistant/components/dialogflow/config_flow.py +++ b/homeassistant/components/dialogflow/config_flow.py @@ -1,7 +1,7 @@ """Config flow for DialogFlow.""" from homeassistant.helpers import config_entry_flow -from .const import DOMAIN +from .const import DOMAIN config_entry_flow.register_webhook_flow( DOMAIN, diff --git a/homeassistant/components/digital_ocean/__init__.py b/homeassistant/components/digital_ocean/__init__.py index bdb0c348803..33663f121d1 100644 --- a/homeassistant/components/digital_ocean/__init__.py +++ b/homeassistant/components/digital_ocean/__init__.py @@ -1,13 +1,13 @@ """Support for Digital Ocean.""" -import logging from datetime import timedelta +import logging import digitalocean import voluptuous as vol from homeassistant.const import CONF_ACCESS_TOKEN -from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv +from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/digitalloggers/switch.py b/homeassistant/components/digitalloggers/switch.py index 10c8ce73a47..824af441688 100644 --- a/homeassistant/components/digitalloggers/switch.py +++ b/homeassistant/components/digitalloggers/switch.py @@ -1,17 +1,17 @@ """Support for Digital Loggers DIN III Relays.""" -import logging from datetime import timedelta +import logging import dlipower import voluptuous as vol -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import ( CONF_HOST, CONF_NAME, - CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT, + CONF_USERNAME, ) import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle diff --git a/homeassistant/components/directv/media_player.py b/homeassistant/components/directv/media_player.py index 2be2544cec1..cd4f910c707 100644 --- a/homeassistant/components/directv/media_player.py +++ b/homeassistant/components/directv/media_player.py @@ -1,9 +1,11 @@ """Support for the DirecTV receivers.""" import logging + +from DirectPy import DIRECTV import requests import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, MEDIA_TYPE_MOVIE, @@ -99,8 +101,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): # Attempt to discover additional RVU units _LOGGER.debug("Doing discovery of DirecTV devices on %s", host) - from DirectPy import DIRECTV - dtv = DIRECTV(host, DEFAULT_PORT) try: resp = dtv.get_locations() @@ -129,7 +129,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) else: _LOGGER.debug( - "Adding discovered device %s with" " client address %s", + "Adding discovered device %s with client address %s", str.title(loc["locationName"]), loc["clientAddr"], ) @@ -156,7 +156,6 @@ class DirecTvDevice(MediaPlayerDevice): def __init__(self, name, host, port, device): """Initialize the device.""" - from DirectPy import DIRECTV self.dtv = DIRECTV(host, port, device) self._name = name @@ -215,7 +214,7 @@ class DirecTvDevice(MediaPlayerDevice): except requests.RequestException as ex: _LOGGER.error( - "%s: Request error trying to update current status: " "%s", + "%s: Request error trying to update current status: %s", self.entity_id, ex, ) diff --git a/homeassistant/components/discord/manifest.json b/homeassistant/components/discord/manifest.json index d00d26d2b5e..b5bf7eab6cc 100644 --- a/homeassistant/components/discord/manifest.json +++ b/homeassistant/components/discord/manifest.json @@ -3,7 +3,7 @@ "name": "Discord", "documentation": "https://www.home-assistant.io/integrations/discord", "requirements": [ - "discord.py==1.2.4" + "discord.py==1.2.5" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/discord/notify.py b/homeassistant/components/discord/notify.py index f35cf5b0ce9..864b7da5e55 100644 --- a/homeassistant/components/discord/notify.py +++ b/homeassistant/components/discord/notify.py @@ -5,15 +5,14 @@ import os.path import discord import voluptuous as vol -from homeassistant.const import CONF_TOKEN -import homeassistant.helpers.config_validation as cv - from homeassistant.components.notify import ( ATTR_DATA, ATTR_TARGET, PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.const import CONF_TOKEN +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/discovery/__init__.py b/homeassistant/components/discovery/__init__.py index d372775cbf1..1e29d066f2d 100644 --- a/homeassistant/components/discovery/__init__.py +++ b/homeassistant/components/discovery/__init__.py @@ -6,18 +6,19 @@ 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. """ -import json from datetime import timedelta +import json import logging +from netdisco.discovery import NetworkDiscovery import voluptuous as vol from homeassistant import config_entries -from homeassistant.core import callback from homeassistant.const import EVENT_HOMEASSISTANT_START +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.discovery import async_discover, async_load_platform from homeassistant.helpers.event import async_track_point_in_utc_time -from homeassistant.helpers.discovery import async_load_platform, async_discover import homeassistant.util.dt as dt_util DOMAIN = "discovery" @@ -128,7 +129,6 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass, config): """Start a discovery service.""" - from netdisco.discovery import NetworkDiscovery logger = logging.getLogger(__name__) netdisco = NetworkDiscovery() diff --git a/homeassistant/components/dlib_face_detect/image_processing.py b/homeassistant/components/dlib_face_detect/image_processing.py index 6e5c49e7aba..430878ca44f 100644 --- a/homeassistant/components/dlib_face_detect/image_processing.py +++ b/homeassistant/components/dlib_face_detect/image_processing.py @@ -10,10 +10,12 @@ from homeassistant.components.image_processing import ( CONF_SOURCE, ImageProcessingFaceEntity, ) +from homeassistant.core import split_entity_id # pylint: disable=unused-import -from homeassistant.components.image_processing import PLATFORM_SCHEMA # noqa: F401 -from homeassistant.core import split_entity_id +from homeassistant.components.image_processing import ( # noqa: F401, isort:skip + PLATFORM_SCHEMA, +) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/dlib_face_identify/image_processing.py b/homeassistant/components/dlib_face_identify/image_processing.py index d5b55b6a68c..d6fbf106b0c 100644 --- a/homeassistant/components/dlib_face_identify/image_processing.py +++ b/homeassistant/components/dlib_face_identify/image_processing.py @@ -1,20 +1,20 @@ """Component that will help set the Dlib face detect processing.""" -import logging import io +import logging # pylint: disable=import-error import face_recognition import voluptuous as vol -from homeassistant.core import split_entity_id from homeassistant.components.image_processing import ( - ImageProcessingFaceEntity, - PLATFORM_SCHEMA, - CONF_SOURCE, + CONF_CONFIDENCE, CONF_ENTITY_ID, CONF_NAME, - CONF_CONFIDENCE, + CONF_SOURCE, + PLATFORM_SCHEMA, + ImageProcessingFaceEntity, ) +from homeassistant.core import split_entity_id import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/dlink/switch.py b/homeassistant/components/dlink/switch.py index 25091e14dbd..7fa391e8060 100644 --- a/homeassistant/components/dlink/switch.py +++ b/homeassistant/components/dlink/switch.py @@ -3,6 +3,7 @@ from datetime import timedelta import logging import urllib +from pyW215.pyW215 import SmartPlug import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice @@ -42,7 +43,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up a D-Link Smart Plug.""" - from pyW215.pyW215 import SmartPlug host = config.get(CONF_HOST) username = config.get(CONF_USERNAME) diff --git a/homeassistant/components/dlna_dmr/media_player.py b/homeassistant/components/dlna_dmr/media_player.py index 5dd7ab7a88a..a6ebf95424a 100644 --- a/homeassistant/components/dlna_dmr/media_player.py +++ b/homeassistant/components/dlna_dmr/media_player.py @@ -6,9 +6,12 @@ import logging from typing import Optional import aiohttp +from async_upnp_client import UpnpFactory +from async_upnp_client.aiohttp import AiohttpNotifyServer, AiohttpSessionRequester +from async_upnp_client.profiles.dlna import DeviceState, DmrDevice import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, MEDIA_TYPE_EPISODE, @@ -40,10 +43,10 @@ from homeassistant.const import ( ) from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.typing import HomeAssistantType import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt_util +from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import get_local_ip +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -121,8 +124,6 @@ async def async_start_event_handler( return hass_data["event_handler"] # start event handler - from async_upnp_client.aiohttp import AiohttpNotifyServer - server = AiohttpNotifyServer( requester, listen_port=server_port, @@ -163,8 +164,6 @@ async def async_setup_platform( hass.data[DLNA_DMR_DATA]["lock"] = asyncio.Lock() # build upnp/aiohttp requester - from async_upnp_client.aiohttp import AiohttpSessionRequester - session = async_get_clientsession(hass) requester = AiohttpSessionRequester(session, True) @@ -180,8 +179,6 @@ async def async_setup_platform( ) # create upnp device - from async_upnp_client import UpnpFactory - factory = UpnpFactory(requester, disable_state_variable_validation=True) try: upnp_device = await factory.async_create_device(url) @@ -189,8 +186,6 @@ async def async_setup_platform( raise PlatformNotReady() # wrap with DmrDevice - from async_upnp_client.profiles.dlna import DmrDevice - dlna_device = DmrDevice(upnp_device, event_handler) # create our own device @@ -203,7 +198,7 @@ class DlnaDmrDevice(MediaPlayerDevice): """Representation of a DLNA DMR device.""" def __init__(self, dmr_device, name=None): - """Initializer.""" + """Initialize DLNA DMR device.""" self._device = dmr_device self._name = name @@ -361,8 +356,6 @@ class DlnaDmrDevice(MediaPlayerDevice): await self._device.async_wait_for_can_play() # If already playing, no need to call Play - from async_upnp_client.profiles.dlna import DeviceState - if self._device.state == DeviceState.PLAYING: return @@ -403,8 +396,6 @@ class DlnaDmrDevice(MediaPlayerDevice): if not self._available: return STATE_OFF - from async_upnp_client.profiles.dlna import DeviceState - if self._device.state is None: return STATE_ON if self._device.state == DeviceState.PLAYING: diff --git a/homeassistant/components/dominos/__init__.py b/homeassistant/components/dominos/__init__.py index 59869ed0a97..78852fa2699 100644 --- a/homeassistant/components/dominos/__init__.py +++ b/homeassistant/components/dominos/__init__.py @@ -2,6 +2,8 @@ from datetime import timedelta import logging +from pizzapi import Address, Customer, Order +from pizzapi.address import StoreException import voluptuous as vol from homeassistant.components import http @@ -91,8 +93,6 @@ class Dominos: def __init__(self, hass, config): """Set up main service.""" conf = config[DOMAIN] - from pizzapi import Address, Customer - from pizzapi.address import StoreException self.hass = hass self.customer = Customer( @@ -127,8 +127,6 @@ class Dominos: @Throttle(MIN_TIME_BETWEEN_STORE_UPDATES) def update_closest_store(self): """Update the shared closest store (if open).""" - from pizzapi.address import StoreException - try: self.closest_store = self.address.closest_store() return True @@ -209,8 +207,6 @@ class DominosOrder(Entity): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Update the order state and refreshes the store.""" - from pizzapi.address import StoreException - try: self.dominos.update_closest_store() except StoreException: @@ -226,9 +222,6 @@ class DominosOrder(Entity): def order(self): """Create the order object.""" - from pizzapi import Order - from pizzapi.address import StoreException - if self.dominos.closest_store is None: raise StoreException @@ -246,8 +239,6 @@ class DominosOrder(Entity): def place(self): """Place the order.""" - from pizzapi.address import StoreException - try: order = self.order() order.place() diff --git a/homeassistant/components/doods/image_processing.py b/homeassistant/components/doods/image_processing.py index 7f2c3fd1d42..9525f9e8ddf 100644 --- a/homeassistant/components/doods/image_processing.py +++ b/homeassistant/components/doods/image_processing.py @@ -3,11 +3,10 @@ import io import logging import time -import voluptuous as vol from PIL import Image, ImageDraw from pydoods import PyDOODS +import voluptuous as vol -from homeassistant.const import CONF_TIMEOUT from homeassistant.components.image_processing import ( CONF_CONFIDENCE, CONF_ENTITY_ID, @@ -15,11 +14,12 @@ from homeassistant.components.image_processing import ( CONF_SOURCE, PLATFORM_SCHEMA, ImageProcessingEntity, - draw_box, ) +from homeassistant.const import CONF_TIMEOUT from homeassistant.core import split_entity_id from homeassistant.helpers import template import homeassistant.helpers.config_validation as cv +from homeassistant.util.pil import draw_box _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/doods/manifest.json b/homeassistant/components/doods/manifest.json index e0dcb48527f..551af839b5c 100644 --- a/homeassistant/components/doods/manifest.json +++ b/homeassistant/components/doods/manifest.json @@ -1,10 +1,8 @@ { - "domain": "doods", - "name": "DOODS - Distributed Outside Object Detection Service", - "documentation": "https://www.home-assistant.io/integrations/doods", - "requirements": [ - "pydoods==1.0.2" - ], - "dependencies": [], - "codeowners": [] + "domain": "doods", + "name": "DOODS - Distributed Outside Object Detection Service", + "documentation": "https://www.home-assistant.io/integrations/doods", + "requirements": ["pydoods==1.0.2", "pillow==6.2.1"], + "dependencies": [], + "codeowners": [] } diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py index ff0bbd71194..680ee1354eb 100644 --- a/homeassistant/components/doorbird/__init__.py +++ b/homeassistant/components/doorbird/__init__.py @@ -2,6 +2,7 @@ import logging from urllib.error import HTTPError +from doorbirdpy import DoorBird import voluptuous as vol from homeassistant.components.http import HomeAssistantView @@ -51,7 +52,6 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): """Set up the DoorBird component.""" - from doorbirdpy import DoorBird # Provide an endpoint for the doorstations to call to trigger events hass.http.register_view(DoorBirdRequestView) @@ -67,8 +67,14 @@ def setup(hass, config): token = doorstation_config.get(CONF_TOKEN) name = doorstation_config.get(CONF_NAME) or "DoorBird {}".format(index + 1) - device = DoorBird(device_ip, username, password) - status = device.ready() + try: + device = DoorBird(device_ip, username, password) + status = device.ready() + except OSError as oserr: + _LOGGER.error( + "Failed to setup doorbird at %s: %s; not retrying", device_ip, oserr + ) + continue if status[0]: doorstation = ConfiguredDoorBird(device, name, events, custom_url, token) @@ -264,7 +270,6 @@ class DoorBirdRequestView(HomeAssistantView): name = API_URL[1:].replace("/", ":") extra_urls = [API_URL + "/{event}"] - # pylint: disable=no-self-use async def get(self, request, event): """Respond to requests from the device.""" from aiohttp import web diff --git a/homeassistant/components/doorbird/camera.py b/homeassistant/components/doorbird/camera.py index 457c319d9e1..d9a802f071f 100644 --- a/homeassistant/components/doorbird/camera.py +++ b/homeassistant/components/doorbird/camera.py @@ -6,7 +6,7 @@ import logging import aiohttp import async_timeout -from homeassistant.components.camera import Camera, SUPPORT_STREAM +from homeassistant.components.camera import SUPPORT_STREAM, Camera from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.util.dt as dt_util diff --git a/homeassistant/components/doorbird/manifest.json b/homeassistant/components/doorbird/manifest.json index c9cdb32e18a..61225b86a44 100644 --- a/homeassistant/components/doorbird/manifest.json +++ b/homeassistant/components/doorbird/manifest.json @@ -2,11 +2,7 @@ "domain": "doorbird", "name": "Doorbird", "documentation": "https://www.home-assistant.io/integrations/doorbird", - "requirements": [ - "doorbirdpy==2.0.8" - ], - "dependencies": [], - "codeowners": [ - "@oblogic7" - ] + "requirements": ["doorbirdpy==2.0.8"], + "dependencies": ["http"], + "codeowners": ["@oblogic7"] } diff --git a/homeassistant/components/dovado/__init__.py b/homeassistant/components/dovado/__init__.py index a13c49cc61a..b8d18d90833 100644 --- a/homeassistant/components/dovado/__init__.py +++ b/homeassistant/components/dovado/__init__.py @@ -1,18 +1,18 @@ """Support for Dovado router.""" -import logging from datetime import timedelta +import logging import dovado import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( - CONF_USERNAME, - CONF_PASSWORD, CONF_HOST, + CONF_PASSWORD, CONF_PORT, + CONF_USERNAME, DEVICE_DEFAULT_NAME, ) +import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -21,11 +21,16 @@ DOMAIN = "dovado" CONFIG_SCHEMA = vol.Schema( { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_HOST): cv.string, - vol.Optional(CONF_PORT): cv.port, - } + DOMAIN: vol.Schema( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_HOST): cv.string, + vol.Optional(CONF_PORT): cv.port, + } + ) + }, + extra=vol.ALLOW_EXTRA, ) MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) @@ -36,10 +41,10 @@ def setup(hass, config): hass.data[DOMAIN] = DovadoData( dovado.Dovado( - config[CONF_USERNAME], - config[CONF_PASSWORD], - config.get(CONF_HOST), - config.get(CONF_PORT), + config[DOMAIN].get(CONF_USERNAME), + config[DOMAIN].get(CONF_PASSWORD), + config[DOMAIN].get(CONF_HOST), + config[DOMAIN].get(CONF_PORT), ) ) return True diff --git a/homeassistant/components/dsmr_reader/__init__.py b/homeassistant/components/dsmr_reader/__init__.py new file mode 100644 index 00000000000..946be91d1a5 --- /dev/null +++ b/homeassistant/components/dsmr_reader/__init__.py @@ -0,0 +1 @@ +"""The DSMR Reader component.""" diff --git a/homeassistant/components/dsmr_reader/definitions.py b/homeassistant/components/dsmr_reader/definitions.py new file mode 100644 index 00000000000..45bebfeda92 --- /dev/null +++ b/homeassistant/components/dsmr_reader/definitions.py @@ -0,0 +1,245 @@ +"""Definitions for DSMR Reader sensors added to MQTT.""" + + +def dsmr_transform(value): + """Transform DSMR version value to right format.""" + if value.isdigit(): + return float(value) / 10 + return value + + +def tariff_transform(value): + """Transform tariff from number to description.""" + if value == "1": + return "low" + return "high" + + +DEFINITIONS = { + "dsmr/reading/electricity_delivered_1": { + "name": "Low tariff usage", + "icon": "mdi:flash", + "unit": "kWh", + }, + "dsmr/reading/electricity_returned_1": { + "name": "Low tariff returned", + "icon": "mdi:flash-outline", + "unit": "kWh", + }, + "dsmr/reading/electricity_delivered_2": { + "name": "High tariff usage", + "icon": "mdi:flash", + "unit": "kWh", + }, + "dsmr/reading/electricity_returned_2": { + "name": "High tariff returned", + "icon": "mdi:flash-outline", + "unit": "kWh", + }, + "dsmr/reading/electricity_currently_delivered": { + "name": "Current power usage", + "icon": "mdi:flash", + "unit": "kW", + }, + "dsmr/reading/electricity_currently_returned": { + "name": "Current power return", + "icon": "mdi:flash-outline", + "unit": "kW", + }, + "dsmr/reading/phase_currently_delivered_l1": { + "name": "Current power usage L1", + "icon": "mdi:flash", + "unit": "kW", + }, + "dsmr/reading/phase_currently_delivered_l2": { + "name": "Current power usage L2", + "icon": "mdi:flash", + "unit": "kW", + }, + "dsmr/reading/phase_currently_delivered_l3": { + "name": "Current power usage L3", + "icon": "mdi:flash", + "unit": "kW", + }, + "dsmr/reading/phase_currently_returned_l1": { + "name": "Current power return L1", + "icon": "mdi:flash-outline", + "unit": "kW", + }, + "dsmr/reading/phase_currently_returned_l2": { + "name": "Current power return L2", + "icon": "mdi:flash-outline", + "unit": "kW", + }, + "dsmr/reading/phase_currently_returned_l3": { + "name": "Current power return L3", + "icon": "mdi:flash-outline", + "unit": "kW", + }, + "dsmr/reading/extra_device_delivered": { + "name": "Gas meter usage", + "icon": "mdi:fire", + "unit": "m3", + }, + "dsmr/reading/phase_voltage_l1": { + "name": "Current voltage L1", + "icon": "mdi:flash", + "unit": "V", + }, + "dsmr/reading/phase_voltage_l2": { + "name": "Current voltage L2", + "icon": "mdi:flash", + "unit": "V", + }, + "dsmr/reading/phase_voltage_l3": { + "name": "Current voltage L3", + "icon": "mdi:flash", + "unit": "V", + }, + "dsmr/consumption/gas/delivered": { + "name": "Gas usage", + "icon": "mdi:fire", + "unit": "m3", + }, + "dsmr/consumption/gas/currently_delivered": { + "name": "Current gas usage", + "icon": "mdi:fire", + "unit": "m3", + }, + "dsmr/consumption/gas/read_at": { + "name": "Gas meter read", + "icon": "mdi:clock", + "unit": "", + }, + "dsmr/day-consumption/electricity1": { + "name": "Low tariff usage", + "icon": "mdi:counter", + "unit": "kWh", + }, + "dsmr/day-consumption/electricity2": { + "name": "High tariff usage", + "icon": "mdi:counter", + "unit": "kWh", + }, + "dsmr/day-consumption/electricity1_returned": { + "name": "Low tariff return", + "icon": "mdi:counter", + "unit": "kWh", + }, + "dsmr/day-consumption/electricity2_returned": { + "name": "High tariff return", + "icon": "mdi:counter", + "unit": "kWh", + }, + "dsmr/day-consumption/electricity_merged": { + "name": "Power usage total", + "icon": "mdi:counter", + "unit": "kWh", + }, + "dsmr/day-consumption/electricity_returned_merged": { + "name": "Power return total", + "icon": "mdi:counter", + "unit": "kWh", + }, + "dsmr/day-consumption/electricity1_cost": { + "name": "Low tariff cost", + "icon": "mdi:currency-eur", + "unit": "€", + }, + "dsmr/day-consumption/electricity2_cost": { + "name": "High tariff cost", + "icon": "mdi:currency-eur", + "unit": "€", + }, + "dsmr/day-consumption/electricity_cost_merged": { + "name": "Power total cost", + "icon": "mdi:currency-eur", + "unit": "€", + }, + "dsmr/day-consumption/gas": { + "name": "Gas usage", + "icon": "mdi:counter", + "unit": "m3", + }, + "dsmr/day-consumption/gas_cost": { + "name": "Gas cost", + "icon": "mdi:currency-eur", + "unit": "€", + }, + "dsmr/day-consumption/total_cost": { + "name": "Total cost", + "icon": "mdi:currency-eur", + "unit": "€", + }, + "dsmr/day-consumption/energy_supplier_price_electricity_delivered_1": { + "name": "Low tariff delivered price", + "icon": "mdi:currency-eur", + "unit": "€", + }, + "dsmr/day-consumption/energy_supplier_price_electricity_delivered_2": { + "name": "High tariff delivered price", + "icon": "mdi:currency-eur", + "unit": "€", + }, + "dsmr/day-consumption/energy_supplier_price_electricity_returned_1": { + "name": "Low tariff returned price", + "icon": "mdi:currency-eur", + "unit": "€", + }, + "dsmr/day-consumption/energy_supplier_price_electricity_returned_2": { + "name": "High tariff returned price", + "icon": "mdi:currency-eur", + "unit": "€", + }, + "dsmr/day-consumption/energy_supplier_price_gas": { + "name": "Gas price", + "icon": "mdi:currency-eur", + "unit": "€", + }, + "dsmr/meter-stats/dsmr_version": { + "name": "DSMR version", + "icon": "mdi:alert-circle", + "transform": dsmr_transform, + }, + "dsmr/meter-stats/electricity_tariff": { + "name": "Electricity tariff", + "icon": "mdi:flash", + "transform": tariff_transform, + }, + "dsmr/meter-stats/power_failure_count": { + "name": "Power failure count", + "icon": "mdi:flash", + }, + "dsmr/meter-stats/long_power_failure_count": { + "name": "Long power failure count", + "icon": "mdi:flash", + }, + "dsmr/meter-stats/voltage_sag_count_l1": { + "name": "Voltage sag L1", + "icon": "mdi:flash", + }, + "dsmr/meter-stats/voltage_sag_count_l2": { + "name": "Voltage sag L2", + "icon": "mdi:flash", + }, + "dsmr/meter-stats/voltage_sag_count_l3": { + "name": "Voltage sag L3", + "icon": "mdi:flash", + }, + "dsmr/meter-stats/voltage_swell_count_l1": { + "name": "Voltage swell L1", + "icon": "mdi:flash", + }, + "dsmr/meter-stats/voltage_swell_count_l2": { + "name": "Voltage swell L2", + "icon": "mdi:flash", + }, + "dsmr/meter-stats/voltage_swell_count_l3": { + "name": "Voltage swell L3", + "icon": "mdi:flash", + }, + "dsmr/meter-stats/rejected_telegrams": { + "name": "Rejected telegrams", + "icon": "mdi:flash", + }, +} diff --git a/homeassistant/components/dsmr_reader/manifest.json b/homeassistant/components/dsmr_reader/manifest.json new file mode 100644 index 00000000000..f1c52e02c83 --- /dev/null +++ b/homeassistant/components/dsmr_reader/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "dsmr_reader", + "name": "DSMR Reader", + "documentation": "https://www.home-assistant.io/integrations/dsmr_reader", + "requirements": [], + "dependencies": [ + "mqtt" + ], + "codeowners": [ + "@depl0y" + ] +} diff --git a/homeassistant/components/dsmr_reader/sensor.py b/homeassistant/components/dsmr_reader/sensor.py new file mode 100644 index 00000000000..01c010c4971 --- /dev/null +++ b/homeassistant/components/dsmr_reader/sensor.py @@ -0,0 +1,78 @@ +"""Support for DSMR Reader through MQTT.""" +from homeassistant.components import mqtt +from homeassistant.core import callback +from homeassistant.helpers.entity import Entity +from homeassistant.util import slugify + +from .definitions import DEFINITIONS + +DOMAIN = "dsmr_reader" + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up DSMR Reader sensors.""" + + sensors = [] + for topic in DEFINITIONS: + sensors.append(DSMRSensor(topic)) + + async_add_entities(sensors) + + +class DSMRSensor(Entity): + """Representation of a DSMR sensor that is updated via MQTT.""" + + def __init__(self, topic): + """Initialize the sensor.""" + + self._definition = DEFINITIONS[topic] + + self._entity_id = slugify(topic.replace("/", "_")) + self._topic = topic + + self._name = self._definition.get("name", topic.split("/")[-1]) + self._unit_of_measurement = self._definition.get("unit") + self._icon = self._definition.get("icon") + self._transform = self._definition.get("transform") + self._state = None + + async def async_added_to_hass(self): + """Subscribe to MQTT events.""" + + @callback + def message_received(message): + """Handle new MQTT messages.""" + + if self._transform is not None: + self._state = self._transform(message.payload) + else: + self._state = message.payload + + self.async_schedule_update_ha_state() + + await mqtt.async_subscribe(self.hass, self._topic, message_received, 1) + + @property + def name(self): + """Return the name of the sensor supplied in constructor.""" + return self._name + + @property + def entity_id(self): + """Return the entity ID for this sensor.""" + return f"sensor.{self._entity_id}" + + @property + def state(self): + """Return the current state of the entity.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit_of_measurement of this sensor.""" + return self._unit_of_measurement + + @property + def icon(self): + """Return the icon of this sensor.""" + return self._icon diff --git a/homeassistant/components/dublin_bus_transport/sensor.py b/homeassistant/components/dublin_bus_transport/sensor.py index 203cfb1e27c..a5fe8fd6b30 100644 --- a/homeassistant/components/dublin_bus_transport/sensor.py +++ b/homeassistant/components/dublin_bus_transport/sensor.py @@ -7,17 +7,17 @@ https://data.gov.ie/dataset/real-time-passenger-information-rtpi-for-dublin-bus- For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.dublin_public_transport/ """ +from datetime import datetime, timedelta import logging -from datetime import timedelta, datetime import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt_util from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION +from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) _RESOURCE = "https://data.dublinked.ie/cgi-bin/rtpi/realtimebusinformation" diff --git a/homeassistant/components/duckdns/__init__.py b/homeassistant/components/duckdns/__init__.py index 171d17faff9..b3da1ec2752 100644 --- a/homeassistant/components/duckdns/__init__.py +++ b/homeassistant/components/duckdns/__init__.py @@ -1,14 +1,14 @@ """Integrate with DuckDNS.""" -import logging from asyncio import iscoroutinefunction from datetime import timedelta +import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_ACCESS_TOKEN, CONF_DOMAIN -from homeassistant.core import callback, CALLBACK_TYPE +from homeassistant.core import CALLBACK_TYPE, callback from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.loader import bind_hass from homeassistant.util import dt as dt_util diff --git a/homeassistant/components/duke_energy/sensor.py b/homeassistant/components/duke_energy/sensor.py index 998809decc0..cd30ae96caf 100644 --- a/homeassistant/components/duke_energy/sensor.py +++ b/homeassistant/components/duke_energy/sensor.py @@ -1,12 +1,13 @@ """Support for Duke Energy Gas and Electric meters.""" import logging +from pydukeenergy.api import DukeEnergy, DukeEnergyException import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD -from homeassistant.helpers.entity import Entity +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__) @@ -21,7 +22,6 @@ LAST_BILL_DAYS_BILLED = "last_bills_days_billed" def setup_platform(hass, config, add_entities, discovery_info=None): """Set up all Duke Energy meters.""" - from pydukeenergy.api import DukeEnergy, DukeEnergyException try: duke = DukeEnergy( diff --git a/homeassistant/components/dunehd/media_player.py b/homeassistant/components/dunehd/media_player.py index 95e8cac3dbd..bb32bff2a15 100644 --- a/homeassistant/components/dunehd/media_player.py +++ b/homeassistant/components/dunehd/media_player.py @@ -1,7 +1,8 @@ """DuneHD implementation of the media player.""" +from pdunehd import DuneHDPlayer import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, @@ -46,7 +47,6 @@ DUNEHD_PLAYER_SUPPORT = ( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the DuneHD media player platform.""" - from pdunehd import DuneHDPlayer sources = config.get(CONF_SOURCES, {}) host = config.get(CONF_HOST) diff --git a/homeassistant/components/dwd_weather_warnings/sensor.py b/homeassistant/components/dwd_weather_warnings/sensor.py index 4d7ad04e382..695b839d18c 100644 --- a/homeassistant/components/dwd_weather_warnings/sensor.py +++ b/homeassistant/components/dwd_weather_warnings/sensor.py @@ -12,19 +12,20 @@ Unwetterwarnungen (Stufe 3) Warnungen vor markantem Wetter (Stufe 2) Wetterwarnungen (Stufe 1) """ -import logging -import json from datetime import timedelta +import json +import logging import voluptuous as vol +from homeassistant.components.rest.sensor import RestData +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS, CONF_NAME +from homeassistant.helpers.aiohttp_client import SERVER_SOFTWARE as HA_USER_AGENT import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, CONF_MONITORED_CONDITIONS from homeassistant.util import Throttle import homeassistant.util.dt as dt_util -from homeassistant.components.rest.sensor import RestData _LOGGER = logging.getLogger(__name__) @@ -184,7 +185,10 @@ class DwdWeatherWarningsAPI: "jsonp=loadWarnings", ) - self._rest = RestData("GET", resource, None, None, None, True) + # a User-Agent is necessary for this rest api endpoint (#29496) + headers = {"User-Agent": HA_USER_AGENT} + + self._rest = RestData("GET", resource, None, headers, None, True) self.region_name = region_name self.region_id = None self.region_state = None diff --git a/homeassistant/components/dyson/__init__.py b/homeassistant/components/dyson/__init__.py index 7f247be6bcc..fbe7897e6bb 100644 --- a/homeassistant/components/dyson/__init__.py +++ b/homeassistant/components/dyson/__init__.py @@ -1,11 +1,12 @@ """Support for Dyson Pure Cool Link devices.""" import logging +from libpurecool.dyson import DysonAccount import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_DEVICES, CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -43,8 +44,6 @@ def setup(hass, config): if DYSON_DEVICES not in hass.data: hass.data[DYSON_DEVICES] = [] - from libpurecool.dyson import DysonAccount - dyson_account = DysonAccount( config[DOMAIN].get(CONF_USERNAME), config[DOMAIN].get(CONF_PASSWORD), @@ -90,7 +89,7 @@ def setup(hass, config): # Not yet reliable for device in dyson_devices: _LOGGER.info( - "Trying to connect to device %s with timeout=%i " "and retry=%i", + "Trying to connect to device %s with timeout=%i and retry=%i", device, timeout, retry, diff --git a/homeassistant/components/dyson/air_quality.py b/homeassistant/components/dyson/air_quality.py index 0276e47ed61..647fb236707 100644 --- a/homeassistant/components/dyson/air_quality.py +++ b/homeassistant/components/dyson/air_quality.py @@ -1,7 +1,11 @@ """Support for Dyson Pure Cool Air Quality Sensors.""" import logging -from homeassistant.components.air_quality import AirQualityEntity, DOMAIN +from libpurecool.dyson_pure_cool import DysonPureCool +from libpurecool.dyson_pure_state_v2 import DysonEnvironmentalSensorV2State + +from homeassistant.components.air_quality import DOMAIN, AirQualityEntity + from . import DYSON_DEVICES ATTRIBUTION = "Dyson purifier air quality sensor" @@ -15,7 +19,6 @@ ATTR_VOC = "volatile_organic_compounds" def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Dyson Sensors.""" - from libpurecool.dyson_pure_cool import DysonPureCool if discovery_info is None: return @@ -47,8 +50,6 @@ class DysonAirSensor(AirQualityEntity): def on_message(self, message): """Handle new messages which are received from the fan.""" - from libpurecool.dyson_pure_state_v2 import DysonEnvironmentalSensorV2State - _LOGGER.debug( "%s: Message received for %s device: %s", DOMAIN, self.name, message ) diff --git a/homeassistant/components/dyson/climate.py b/homeassistant/components/dyson/climate.py index 90c19e9de88..df97358d550 100644 --- a/homeassistant/components/dyson/climate.py +++ b/homeassistant/components/dyson/climate.py @@ -1,20 +1,20 @@ """Support for Dyson Pure Hot+Cool link fan.""" import logging -from libpurecool.const import HeatMode, HeatState, FocusMode, HeatTarget -from libpurecool.dyson_pure_state import DysonPureHotCoolState +from libpurecool.const import FocusMode, HeatMode, HeatState, HeatTarget from libpurecool.dyson_pure_hotcool_link import DysonPureHotCoolLink +from libpurecool.dyson_pure_state import DysonPureHotCoolState from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( CURRENT_HVAC_COOL, CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, + FAN_DIFFUSE, + FAN_FOCUS, HVAC_MODE_COOL, HVAC_MODE_HEAT, SUPPORT_FAN_MODE, - FAN_FOCUS, - FAN_DIFFUSE, SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS diff --git a/homeassistant/components/dyson/fan.py b/homeassistant/components/dyson/fan.py index 341919935d0..1fdbed0d204 100644 --- a/homeassistant/components/dyson/fan.py +++ b/homeassistant/components/dyson/fan.py @@ -5,18 +5,24 @@ https://home-assistant.io/components/fan.dyson/ """ import logging +from libpurecool.const import FanMode, FanSpeed, NightMode, Oscillation +from libpurecool.dyson_pure_cool import DysonPureCool +from libpurecool.dyson_pure_cool_link import DysonPureCoolLink +from libpurecool.dyson_pure_state import DysonPureCoolState +from libpurecool.dyson_pure_state_v2 import DysonPureCoolV2State import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.fan import ( + SPEED_HIGH, + SPEED_LOW, + SPEED_MEDIUM, SUPPORT_OSCILLATE, SUPPORT_SET_SPEED, FanEntity, - SPEED_LOW, - SPEED_MEDIUM, - SPEED_HIGH, ) from homeassistant.const import ATTR_ENTITY_ID +import homeassistant.helpers.config_validation as cv + from . import DYSON_DEVICES _LOGGER = logging.getLogger(__name__) @@ -88,8 +94,6 @@ SET_DYSON_SPEED_SCHEMA = vol.Schema( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Dyson fan components.""" - from libpurecool.dyson_pure_cool_link import DysonPureCoolLink - from libpurecool.dyson_pure_cool import DysonPureCool if discovery_info is None: return @@ -197,7 +201,6 @@ class DysonPureCoolLinkDevice(FanEntity): def on_message(self, message): """Call when new messages received from the fan.""" - from libpurecool.dyson_pure_state import DysonPureCoolState if isinstance(message, DysonPureCoolState): _LOGGER.debug("Message received for fan device %s: %s", self.name, message) @@ -215,8 +218,6 @@ class DysonPureCoolLinkDevice(FanEntity): def set_speed(self, speed: str) -> None: """Set the speed of the fan. Never called ??.""" - from libpurecool.const import FanSpeed, FanMode - _LOGGER.debug("Set fan speed to: %s", speed) if speed == FanSpeed.FAN_SPEED_AUTO.value: @@ -227,8 +228,6 @@ class DysonPureCoolLinkDevice(FanEntity): def turn_on(self, speed: str = None, **kwargs) -> None: """Turn on the fan.""" - from libpurecool.const import FanSpeed, FanMode - _LOGGER.debug("Turn on fan %s with speed %s", self.name, speed) if speed: if speed == FanSpeed.FAN_SPEED_AUTO.value: @@ -244,15 +243,11 @@ class DysonPureCoolLinkDevice(FanEntity): def turn_off(self, **kwargs) -> None: """Turn off the fan.""" - from libpurecool.const import FanMode - _LOGGER.debug("Turn off fan %s", self.name) self._device.set_configuration(fan_mode=FanMode.OFF) def oscillate(self, oscillating: bool) -> None: """Turn on/off oscillating.""" - from libpurecool.const import Oscillation - _LOGGER.debug("Turn oscillation %s for device %s", oscillating, self.name) if oscillating: @@ -275,8 +270,6 @@ class DysonPureCoolLinkDevice(FanEntity): @property def speed(self) -> str: """Return the current speed.""" - from libpurecool.const import FanSpeed - if self._device.state: if self._device.state.speed == FanSpeed.FAN_SPEED_AUTO.value: return self._device.state.speed @@ -295,8 +288,6 @@ class DysonPureCoolLinkDevice(FanEntity): def set_night_mode(self, night_mode: bool) -> None: """Turn fan in night mode.""" - from libpurecool.const import NightMode - _LOGGER.debug("Set %s night mode %s", self.name, night_mode) if night_mode: self._device.set_configuration(night_mode=NightMode.NIGHT_MODE_ON) @@ -310,8 +301,6 @@ class DysonPureCoolLinkDevice(FanEntity): def set_auto_mode(self, auto_mode: bool) -> None: """Turn fan in auto mode.""" - from libpurecool.const import FanMode - _LOGGER.debug("Set %s auto mode %s", self.name, auto_mode) if auto_mode: self._device.set_configuration(fan_mode=FanMode.AUTO) @@ -321,8 +310,6 @@ class DysonPureCoolLinkDevice(FanEntity): @property def speed_list(self) -> list: """Get the list of available speeds.""" - from libpurecool.const import FanSpeed - supported_speeds = [ FanSpeed.FAN_SPEED_AUTO.value, int(FanSpeed.FAN_SPEED_1.value), @@ -365,8 +352,6 @@ class DysonPureCoolDevice(FanEntity): def on_message(self, message): """Call when new messages received from the fan.""" - from libpurecool.dyson_pure_state_v2 import DysonPureCoolV2State - if isinstance(message, DysonPureCoolV2State): _LOGGER.debug("Message received for fan device %s: %s", self.name, message) self.schedule_update_ha_state() @@ -392,8 +377,6 @@ class DysonPureCoolDevice(FanEntity): def set_speed(self, speed: str) -> None: """Set the speed of the fan.""" - from libpurecool.const import FanSpeed - if speed == SPEED_LOW: self._device.set_fan_speed(FanSpeed.FAN_SPEED_4) elif speed == SPEED_MEDIUM: @@ -408,8 +391,6 @@ class DysonPureCoolDevice(FanEntity): def set_dyson_speed(self, speed: str = None) -> None: """Set the exact speed of the purecool fan.""" - from libpurecool.const import FanSpeed - _LOGGER.debug("Set exact speed for fan %s", self.name) fan_speed = FanSpeed("{0:04d}".format(int(speed))) @@ -487,8 +468,6 @@ class DysonPureCoolDevice(FanEntity): @property def speed(self): """Return the current speed.""" - from libpurecool.const import FanSpeed - speed_map = { FanSpeed.FAN_SPEED_1.value: SPEED_LOW, FanSpeed.FAN_SPEED_2.value: SPEED_LOW, @@ -508,8 +487,6 @@ class DysonPureCoolDevice(FanEntity): @property def dyson_speed(self): """Return the current speed.""" - from libpurecool.const import FanSpeed - if self._device.state: if self._device.state.speed == FanSpeed.FAN_SPEED_AUTO.value: return self._device.state.speed @@ -563,8 +540,6 @@ class DysonPureCoolDevice(FanEntity): @property def dyson_speed_list(self) -> list: """Get the list of available dyson speeds.""" - from libpurecool.const import FanSpeed - return [ int(FanSpeed.FAN_SPEED_1.value), int(FanSpeed.FAN_SPEED_2.value), diff --git a/homeassistant/components/dyson/manifest.json b/homeassistant/components/dyson/manifest.json index 92940c8c1e1..9b561d78f95 100644 --- a/homeassistant/components/dyson/manifest.json +++ b/homeassistant/components/dyson/manifest.json @@ -3,7 +3,7 @@ "name": "Dyson", "documentation": "https://www.home-assistant.io/integrations/dyson", "requirements": [ - "libpurecool==0.5.0" + "libpurecool==0.6.0" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/dyson/sensor.py b/homeassistant/components/dyson/sensor.py index 1eb2b79c073..2fdd3cd6c1f 100644 --- a/homeassistant/components/dyson/sensor.py +++ b/homeassistant/components/dyson/sensor.py @@ -1,8 +1,12 @@ """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.const import STATE_OFF, TEMP_CELSIUS from homeassistant.helpers.entity import Entity + from . import DYSON_DEVICES SENSOR_UNITS = { @@ -27,8 +31,6 @@ _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Dyson Sensors.""" - from libpurecool.dyson_pure_cool_link import DysonPureCoolLink - from libpurecool.dyson_pure_cool import DysonPureCool if discovery_info is None: return @@ -193,5 +195,5 @@ class DysonAirQualitySensor(DysonSensor): def state(self): """Return Air Quality value.""" if self._device.environmental_state: - return self._device.environmental_state.volatil_organic_compounds + return int(self._device.environmental_state.volatil_organic_compounds) return None diff --git a/homeassistant/components/dyson/vacuum.py b/homeassistant/components/dyson/vacuum.py index cef5f0c9961..6203b65c9db 100644 --- a/homeassistant/components/dyson/vacuum.py +++ b/homeassistant/components/dyson/vacuum.py @@ -1,6 +1,9 @@ """Support for the Dyson 360 eye vacuum cleaner robot.""" import logging +from libpurecool.const import Dyson360EyeMode, PowerMode +from libpurecool.dyson_360_eye import Dyson360Eye + from homeassistant.components.vacuum import ( SUPPORT_BATTERY, SUPPORT_FAN_SPEED, @@ -38,8 +41,6 @@ SUPPORT_DYSON = ( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Dyson 360 Eye robot vacuum platform.""" - from libpurecool.dyson_360_eye import Dyson360Eye - _LOGGER.debug("Creating new Dyson 360 Eye robot vacuum") if DYSON_360_EYE_DEVICES not in hass.data: hass.data[DYSON_360_EYE_DEVICES] = [] @@ -86,8 +87,6 @@ class Dyson360EyeDevice(VacuumDevice): @property def status(self): """Return the status of the vacuum cleaner.""" - from libpurecool.const import Dyson360EyeMode - dyson_labels = { Dyson360EyeMode.INACTIVE_CHARGING: "Stopped - Charging", Dyson360EyeMode.INACTIVE_CHARGED: "Stopped - Charged", @@ -110,8 +109,6 @@ class Dyson360EyeDevice(VacuumDevice): @property def fan_speed(self): """Return the fan speed of the vacuum cleaner.""" - from libpurecool.const import PowerMode - speed_labels = {PowerMode.MAX: "Max", PowerMode.QUIET: "Quiet"} return speed_labels[self._device.state.power_mode] @@ -128,8 +125,6 @@ class Dyson360EyeDevice(VacuumDevice): @property def is_on(self) -> bool: """Return True if entity is on.""" - from libpurecool.const import Dyson360EyeMode - return self._device.state.state in [ Dyson360EyeMode.FULL_CLEAN_INITIATED, Dyson360EyeMode.FULL_CLEAN_ABORTED, @@ -149,8 +144,6 @@ class Dyson360EyeDevice(VacuumDevice): @property def battery_icon(self): """Return the battery icon for the vacuum cleaner.""" - from libpurecool.const import Dyson360EyeMode - charging = self._device.state.state in [Dyson360EyeMode.INACTIVE_CHARGING] return icon_for_battery_level( battery_level=self.battery_level, charging=charging @@ -158,8 +151,6 @@ class Dyson360EyeDevice(VacuumDevice): def turn_on(self, **kwargs): """Turn the vacuum on.""" - from libpurecool.const import Dyson360EyeMode - _LOGGER.debug("Turn on device %s", self.name) if self._device.state.state in [Dyson360EyeMode.FULL_CLEAN_PAUSED]: self._device.resume() @@ -178,16 +169,12 @@ class Dyson360EyeDevice(VacuumDevice): def set_fan_speed(self, fan_speed, **kwargs): """Set fan speed.""" - from libpurecool.const import PowerMode - _LOGGER.debug("Set fan speed %s on device %s", fan_speed, self.name) power_modes = {"Quiet": PowerMode.QUIET, "Max": PowerMode.MAX} self._device.set_power_mode(power_modes[fan_speed]) def start_pause(self, **kwargs): """Start, pause or resume the cleaning task.""" - from libpurecool.const import Dyson360EyeMode - if self._device.state.state in [Dyson360EyeMode.FULL_CLEAN_PAUSED]: _LOGGER.debug("Resume device %s", self.name) self._device.resume() diff --git a/homeassistant/components/ebox/sensor.py b/homeassistant/components/ebox/sensor.py index 95c5513ecaf..55504e8edf7 100644 --- a/homeassistant/components/ebox/sensor.py +++ b/homeassistant/components/ebox/sensor.py @@ -6,23 +6,24 @@ Get data from 'My Usage Page' page: https://client.ebox.ca/myusage For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.ebox/ """ -import logging from datetime import timedelta +import logging +from pyebox import EboxClient +from pyebox.client import PyEboxError import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_USERNAME, - CONF_PASSWORD, - CONF_NAME, CONF_MONITORED_VARIABLES, + CONF_NAME, + CONF_PASSWORD, + CONF_USERNAME, ) +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.exceptions import PlatformNotReady - _LOGGER = logging.getLogger(__name__) @@ -75,8 +76,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= name = config.get(CONF_NAME) - from pyebox.client import PyEboxError - try: await ebox_data.async_update() except PyEboxError as exp: @@ -135,16 +134,12 @@ class EBoxData: def __init__(self, username, password, httpsession): """Initialize the data object.""" - from pyebox import EboxClient - self.client = EboxClient(username, password, REQUESTS_TIMEOUT, httpsession) self.data = {} @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self): """Get the latest data from Ebox.""" - from pyebox.client import PyEboxError - try: await self.client.fetch_data() except PyEboxError as exp: diff --git a/homeassistant/components/ecoal_boiler/__init__.py b/homeassistant/components/ecoal_boiler/__init__.py index 40769c9990a..608e4a59a3f 100644 --- a/homeassistant/components/ecoal_boiler/__init__.py +++ b/homeassistant/components/ecoal_boiler/__init__.py @@ -1,15 +1,16 @@ """Support to control ecoal/esterownik.pl coal/wood boiler controller.""" import logging +from ecoaliface.simple import ECoalController import voluptuous as vol from homeassistant.const import ( CONF_HOST, - CONF_PASSWORD, - CONF_USERNAME, CONF_MONITORED_CONDITIONS, + CONF_PASSWORD, CONF_SENSORS, CONF_SWITCHES, + CONF_USERNAME, ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform @@ -80,7 +81,6 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, hass_config): """Set up global ECoalController instance same for sensors and switches.""" - from ecoaliface.simple import ECoalController conf = hass_config[DOMAIN] host = conf[CONF_HOST] @@ -91,7 +91,7 @@ def setup(hass, hass_config): if ecoal_contr.version is None: # Wrong credentials nor network config _LOGGER.error( - "Unable to read controller status from %s@%s" " (wrong host/credentials)", + "Unable to read controller status from %s@%s (wrong host/credentials)", username, host, ) diff --git a/homeassistant/components/ecobee/.translations/da.json b/homeassistant/components/ecobee/.translations/da.json index 7a42a9470db..614811db45a 100644 --- a/homeassistant/components/ecobee/.translations/da.json +++ b/homeassistant/components/ecobee/.translations/da.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "one_instance_only": "Integrationen underst\u00f8tter kun \u00e9n ecobee forekomst" + "one_instance_only": "Denne integration underst\u00f8tter i \u00f8jeblikket kun en ecobee-instans." }, "error": { "pin_request_failed": "Fejl ved anmodning om pinkode fra ecobee. Kontroller at API-n\u00f8glen er korrekt.", @@ -9,7 +9,8 @@ }, "step": { "authorize": { - "title": "Autoriser app p\u00e5 ecobee.com" + "description": "Godkend denne app p\u00e5 https://www.ecobee.com/consumerportal/index.html med PIN-kode:\n\n{pin}\n\nTryk derefter p\u00e5 Indsend.", + "title": "Godkend app p\u00e5 ecobee.com" }, "user": { "data": { diff --git a/homeassistant/components/ecobee/__init__.py b/homeassistant/components/ecobee/__init__.py index eb65a7ed426..80c3be7954b 100644 --- a/homeassistant/components/ecobee/__init__.py +++ b/homeassistant/components/ecobee/__init__.py @@ -1,9 +1,9 @@ """Support for ecobee.""" import asyncio from datetime import timedelta -import voluptuous as vol -from pyecobee import Ecobee, ECOBEE_API_KEY, ECOBEE_REFRESH_TOKEN, ExpiredTokenError +from pyecobee import ECOBEE_API_KEY, ECOBEE_REFRESH_TOKEN, Ecobee, ExpiredTokenError +import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_API_KEY @@ -11,11 +11,11 @@ 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, - _LOGGER, ) MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=180) diff --git a/homeassistant/components/ecobee/binary_sensor.py b/homeassistant/components/ecobee/binary_sensor.py index 06289572aea..f7a24886b84 100644 --- a/homeassistant/components/ecobee/binary_sensor.py +++ b/homeassistant/components/ecobee/binary_sensor.py @@ -1,10 +1,10 @@ """Support for Ecobee binary sensors.""" from homeassistant.components.binary_sensor import ( - BinarySensorDevice, DEVICE_CLASS_OCCUPANCY, + BinarySensorDevice, ) -from .const import DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER, _LOGGER +from .const import _LOGGER, DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index c583f9696d2..5915e64334f 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -6,37 +6,37 @@ import voluptuous as vol from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( - HVAC_MODE_COOL, - HVAC_MODE_HEAT, - HVAC_MODE_AUTO, - HVAC_MODE_OFF, - ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, - SUPPORT_TARGET_TEMPERATURE, - SUPPORT_AUX_HEAT, - SUPPORT_TARGET_TEMPERATURE_RANGE, - SUPPORT_FAN_MODE, - PRESET_AWAY, + ATTR_TARGET_TEMP_LOW, + CURRENT_HVAC_COOL, + CURRENT_HVAC_DRY, + CURRENT_HVAC_FAN, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, FAN_AUTO, FAN_ON, - CURRENT_HVAC_IDLE, - CURRENT_HVAC_HEAT, - CURRENT_HVAC_COOL, - SUPPORT_PRESET_MODE, + HVAC_MODE_AUTO, + HVAC_MODE_COOL, + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + PRESET_AWAY, PRESET_NONE, - CURRENT_HVAC_FAN, - CURRENT_HVAC_DRY, + SUPPORT_AUX_HEAT, + SUPPORT_FAN_MODE, + SUPPORT_PRESET_MODE, + SUPPORT_TARGET_TEMPERATURE, + SUPPORT_TARGET_TEMPERATURE_RANGE, ) from homeassistant.const import ( ATTR_ENTITY_ID, - STATE_ON, ATTR_TEMPERATURE, + STATE_ON, TEMP_FAHRENHEIT, ) -from homeassistant.util.temperature import convert import homeassistant.helpers.config_validation as cv +from homeassistant.util.temperature import convert -from .const import DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER, _LOGGER +from .const import _LOGGER, DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER from .util import ecobee_date, ecobee_time ATTR_COOL_TEMP = "cool_temp" @@ -550,7 +550,7 @@ class Thermostat(ClimateDevice): self.hold_preference(), ) _LOGGER.debug( - "Setting ecobee hold_temp to: heat=%s, is=%s, " "cool=%s, is=%s", + "Setting ecobee hold_temp to: heat=%s, is=%s, cool=%s, is=%s", heat_temp, isinstance(heat_temp, (int, float)), cool_temp, diff --git a/homeassistant/components/ecobee/config_flow.py b/homeassistant/components/ecobee/config_flow.py index 56ce13f7701..bb406d81e3a 100644 --- a/homeassistant/components/ecobee/config_flow.py +++ b/homeassistant/components/ecobee/config_flow.py @@ -1,19 +1,18 @@ """Config flow to configure ecobee.""" -import voluptuous as vol - from pyecobee import ( - Ecobee, - ECOBEE_CONFIG_FILENAME, ECOBEE_API_KEY, + ECOBEE_CONFIG_FILENAME, ECOBEE_REFRESH_TOKEN, + Ecobee, ) +import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistantError from homeassistant.util.json import load_json -from .const import CONF_REFRESH_TOKEN, DATA_ECOBEE_CONFIG, DOMAIN, _LOGGER +from .const import _LOGGER, CONF_REFRESH_TOKEN, DATA_ECOBEE_CONFIG, DOMAIN class EcobeeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/ecobee/notify.py b/homeassistant/components/ecobee/notify.py index c7b3f47d29c..a8f53a027b3 100644 --- a/homeassistant/components/ecobee/notify.py +++ b/homeassistant/components/ecobee/notify.py @@ -1,8 +1,8 @@ """Support for Ecobee Send Message service.""" import voluptuous as vol +from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService import homeassistant.helpers.config_validation as cv -from homeassistant.components.notify import BaseNotificationService, PLATFORM_SCHEMA from .const import CONF_INDEX, DOMAIN diff --git a/homeassistant/components/ecobee/sensor.py b/homeassistant/components/ecobee/sensor.py index 76945080bfa..37201ec2121 100644 --- a/homeassistant/components/ecobee/sensor.py +++ b/homeassistant/components/ecobee/sensor.py @@ -8,7 +8,7 @@ from homeassistant.const import ( ) from homeassistant.helpers.entity import Entity -from .const import DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER, _LOGGER +from .const import _LOGGER, DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER SENSOR_TYPES = { "temperature": ["Temperature", TEMP_FAHRENHEIT], diff --git a/homeassistant/components/ecobee/util.py b/homeassistant/components/ecobee/util.py index 3acc3e5676d..2f5d194fec0 100644 --- a/homeassistant/components/ecobee/util.py +++ b/homeassistant/components/ecobee/util.py @@ -1,5 +1,6 @@ """Validation utility functions for ecobee services.""" from datetime import datetime + import voluptuous as vol diff --git a/homeassistant/components/ecobee/weather.py b/homeassistant/components/ecobee/weather.py index 7b057f09a0c..a571e854f73 100644 --- a/homeassistant/components/ecobee/weather.py +++ b/homeassistant/components/ecobee/weather.py @@ -15,11 +15,11 @@ from homeassistant.components.weather import ( from homeassistant.const import TEMP_FAHRENHEIT from .const import ( + _LOGGER, DOMAIN, ECOBEE_MODEL_TO_NAME, ECOBEE_WEATHER_SYMBOL_TO_HASS, MANUFACTURER, - _LOGGER, ) diff --git a/homeassistant/components/econet/const.py b/homeassistant/components/econet/const.py new file mode 100644 index 00000000000..88b1b851aa6 --- /dev/null +++ b/homeassistant/components/econet/const.py @@ -0,0 +1,5 @@ +"""Constants for Econet integration.""" + +DOMAIN = "econet" +SERVICE_ADD_VACATION = "add_vacation" +SERVICE_DELETE_VACATION = "delete_vacation" diff --git a/homeassistant/components/econet/services.yaml b/homeassistant/components/econet/services.yaml index e69de29bb2d..9f489165c22 100644 --- a/homeassistant/components/econet/services.yaml +++ b/homeassistant/components/econet/services.yaml @@ -0,0 +1,19 @@ +add_vacation: + description: Add a vacation to your water heater. + fields: + entity_id: + description: Name(s) of entities to change. + example: 'water_heater.econet' + start_date: + description: The timestamp of when the vacation should start. (Optional, defaults to now) + example: 1513186320 + end_date: + description: The timestamp of when the vacation should end. + example: 1513445520 + +delete_vacation: + description: Delete your existing vacation from your water heater. + fields: + entity_id: + description: Name(s) of entities to change. + example: 'water_heater.econet' \ No newline at end of file diff --git a/homeassistant/components/econet/water_heater.py b/homeassistant/components/econet/water_heater.py index 1c8deae5b99..26ee7cb8bd4 100644 --- a/homeassistant/components/econet/water_heater.py +++ b/homeassistant/components/econet/water_heater.py @@ -2,10 +2,10 @@ import datetime import logging +from pyeconet.api import PyEcoNet import voluptuous as vol from homeassistant.components.water_heater import ( - DOMAIN, PLATFORM_SCHEMA, STATE_ECO, STATE_ELECTRIC, @@ -27,6 +27,8 @@ from homeassistant.const import ( ) import homeassistant.helpers.config_validation as cv +from .const import DOMAIN, SERVICE_ADD_VACATION, SERVICE_DELETE_VACATION + _LOGGER = logging.getLogger(__name__) ATTR_VACATION_START = "next_vacation_start_date" @@ -40,9 +42,6 @@ ATTR_END_DATE = "end_date" SUPPORT_FLAGS_HEATER = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE -SERVICE_ADD_VACATION = "econet_add_vacation" -SERVICE_DELETE_VACATION = "econet_delete_vacation" - ADD_VACATION_SCHEMA = vol.Schema( { vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, @@ -74,7 +73,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the EcoNet water heaters.""" - from pyeconet.api import PyEcoNet hass.data[ECONET_DATA] = {} hass.data[ECONET_DATA]["water_heaters"] = [] diff --git a/homeassistant/components/ecovacs/__init__.py b/homeassistant/components/ecovacs/__init__.py index 76566912d12..964dd7a3f2a 100644 --- a/homeassistant/components/ecovacs/__init__.py +++ b/homeassistant/components/ecovacs/__init__.py @@ -3,6 +3,7 @@ import logging import random import string +from sucks import EcoVacsAPI, VacBot import voluptuous as vol from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP @@ -44,8 +45,6 @@ def setup(hass, config): hass.data[ECOVACS_DEVICES] = [] - from sucks import EcoVacsAPI, VacBot - ecovacs_api = EcoVacsAPI( ECOVACS_API_DEVICEID, config[DOMAIN].get(CONF_USERNAME), diff --git a/homeassistant/components/ecovacs/vacuum.py b/homeassistant/components/ecovacs/vacuum.py index fdaf6291be5..16a9d67bffc 100644 --- a/homeassistant/components/ecovacs/vacuum.py +++ b/homeassistant/components/ecovacs/vacuum.py @@ -1,6 +1,8 @@ """Support for Ecovacs Ecovacs Vaccums.""" import logging +import sucks + from homeassistant.components.vacuum import ( SUPPORT_BATTERY, SUPPORT_CLEAN_SPOT, @@ -123,9 +125,8 @@ class EcovacsVacuum(VacuumDevice): def return_to_base(self, **kwargs): """Set the vacuum cleaner to return to the dock.""" - from sucks import Charge - self.device.run(Charge()) + self.device.run(sucks.Charge()) @property def battery_icon(self): @@ -150,15 +151,13 @@ class EcovacsVacuum(VacuumDevice): @property def fan_speed_list(self): """Get the list of available fan speed steps of the vacuum cleaner.""" - from sucks import FAN_SPEED_NORMAL, FAN_SPEED_HIGH - return [FAN_SPEED_NORMAL, FAN_SPEED_HIGH] + return [sucks.FAN_SPEED_NORMAL, sucks.FAN_SPEED_HIGH] def turn_on(self, **kwargs): """Turn the vacuum on and start cleaning.""" - from sucks import Clean - self.device.run(Clean()) + self.device.run(sucks.Clean()) def turn_off(self, **kwargs): """Turn the vacuum off stopping the cleaning and returning home.""" @@ -166,34 +165,28 @@ class EcovacsVacuum(VacuumDevice): def stop(self, **kwargs): """Stop the vacuum cleaner.""" - from sucks import Stop - self.device.run(Stop()) + self.device.run(sucks.Stop()) def clean_spot(self, **kwargs): """Perform a spot clean-up.""" - from sucks import Spot - self.device.run(Spot()) + self.device.run(sucks.Spot()) def locate(self, **kwargs): """Locate the vacuum cleaner.""" - from sucks import PlaySound - self.device.run(PlaySound()) + self.device.run(sucks.PlaySound()) def set_fan_speed(self, fan_speed, **kwargs): """Set fan speed.""" if self.is_on: - from sucks import Clean - self.device.run(Clean(mode=self.device.clean_status, speed=fan_speed)) + self.device.run(sucks.Clean(mode=self.device.clean_status, speed=fan_speed)) def send_command(self, command, params=None, **kwargs): """Send a command to a vacuum cleaner.""" - from sucks import VacBotCommand - - self.device.run(VacBotCommand(command, params)) + self.device.run(sucks.VacBotCommand(command, params)) @property def device_state_attributes(self): diff --git a/homeassistant/components/eddystone_temperature/sensor.py b/homeassistant/components/eddystone_temperature/sensor.py index 67724e9fcf3..22d3533d32f 100644 --- a/homeassistant/components/eddystone_temperature/sensor.py +++ b/homeassistant/components/eddystone_temperature/sensor.py @@ -9,6 +9,8 @@ https://home-assistant.io/components/sensor.eddystone_temperature/ """ import logging +# pylint: disable=import-error +from beacontools import BeaconScanner, EddystoneFilter, EddystoneTLMFrame import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -150,12 +152,6 @@ class Monitor: packet.temperature, ) - from beacontools import ( # pylint: disable=import-error - BeaconScanner, - EddystoneFilter, - EddystoneTLMFrame, - ) - device_filters = [EddystoneFilter(d.namespace, d.instance) for d in devices] self.scanner = BeaconScanner( diff --git a/homeassistant/components/edimax/switch.py b/homeassistant/components/edimax/switch.py index f1d8f8046ef..3d558f6c770 100644 --- a/homeassistant/components/edimax/switch.py +++ b/homeassistant/components/edimax/switch.py @@ -1,9 +1,10 @@ """Support for Edimax switches.""" import logging +from pyedimax.smartplug import SmartPlug import voluptuous as vol -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME import homeassistant.helpers.config_validation as cv @@ -25,8 +26,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Find and return Edimax Smart Plugs.""" - from pyedimax.smartplug import SmartPlug - host = config.get(CONF_HOST) auth = (config.get(CONF_USERNAME), config.get(CONF_PASSWORD)) name = config.get(CONF_NAME) diff --git a/homeassistant/components/ee_brightbox/device_tracker.py b/homeassistant/components/ee_brightbox/device_tracker.py index 81dbf9eab1f..845d557e029 100644 --- a/homeassistant/components/ee_brightbox/device_tracker.py +++ b/homeassistant/components/ee_brightbox/device_tracker.py @@ -1,6 +1,7 @@ """Support for EE Brightbox router.""" import logging +from eebrightbox import EEBrightBox, EEBrightBoxException import voluptuous as vol from homeassistant.components.device_tracker import ( @@ -46,8 +47,6 @@ class EEBrightBoxScanner(DeviceScanner): def check_config(self): """Check if provided configuration and credentials are correct.""" - from eebrightbox import EEBrightBox, EEBrightBoxException - try: with EEBrightBox(self.config) as ee_brightbox: return bool(ee_brightbox.get_devices()) @@ -57,8 +56,6 @@ class EEBrightBoxScanner(DeviceScanner): def scan_devices(self): """Scan for devices.""" - from eebrightbox import EEBrightBox - with EEBrightBox(self.config) as ee_brightbox: self.devices = {d["mac"]: d for d in ee_brightbox.get_devices()} diff --git a/homeassistant/components/efergy/sensor.py b/homeassistant/components/efergy/sensor.py index 43c3b67457a..3be962fea2f 100644 --- a/homeassistant/components/efergy/sensor.py +++ b/homeassistant/components/efergy/sensor.py @@ -5,7 +5,7 @@ import requests import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_CURRENCY, POWER_WATT, ENERGY_KILO_WATT_HOUR +from homeassistant.const import CONF_CURRENCY, ENERGY_KILO_WATT_HOUR, POWER_WATT import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity diff --git a/homeassistant/components/egardia/__init__.py b/homeassistant/components/egardia/__init__.py index 9e11f522dd5..efe47736479 100644 --- a/homeassistant/components/egardia/__init__.py +++ b/homeassistant/components/egardia/__init__.py @@ -1,6 +1,7 @@ """Interfaces with Egardia/Woonveilig alarm control panel.""" import logging +from pythonegardia import egardiadevice, egardiaserver import requests import voluptuous as vol @@ -78,8 +79,6 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): """Set up the Egardia platform.""" - from pythonegardia import egardiadevice - from pythonegardia import egardiaserver conf = config[DOMAIN] username = conf.get(CONF_USERNAME) diff --git a/homeassistant/components/egardia/alarm_control_panel.py b/homeassistant/components/egardia/alarm_control_panel.py index 22a458ae9aa..7e5f88cff3e 100644 --- a/homeassistant/components/egardia/alarm_control_panel.py +++ b/homeassistant/components/egardia/alarm_control_panel.py @@ -4,6 +4,10 @@ import logging import requests import homeassistant.components.alarm_control_panel as alarm +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, @@ -79,6 +83,11 @@ class EgardiaAlarm(alarm.AlarmControlPanel): """Return the state of the device.""" return self._status + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY + @property def should_poll(self): """Poll if no report server is enabled.""" @@ -130,7 +139,7 @@ class EgardiaAlarm(alarm.AlarmControlPanel): self._egardiasystem.alarm_disarm() except requests.exceptions.RequestException as err: _LOGGER.error( - "Egardia device exception occurred when " "sending disarm command: %s", + "Egardia device exception occurred when sending disarm command: %s", err, ) diff --git a/homeassistant/components/eight_sleep/__init__.py b/homeassistant/components/eight_sleep/__init__.py index 923c3f7d309..a8a5a6e1fcc 100644 --- a/homeassistant/components/eight_sleep/__init__.py +++ b/homeassistant/components/eight_sleep/__init__.py @@ -1,23 +1,24 @@ """Support for Eight smart mattress covers and mattresses.""" -import logging from datetime import timedelta +import logging +from pyeight.eight import EightSleep import voluptuous as vol -from homeassistant.core import callback from homeassistant.const import ( - CONF_USERNAME, + ATTR_ENTITY_ID, + CONF_BINARY_SENSORS, CONF_PASSWORD, CONF_SENSORS, - CONF_BINARY_SENSORS, - ATTR_ENTITY_ID, + CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, ) +from homeassistant.core import callback from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( - async_dispatcher_send, async_dispatcher_connect, + async_dispatcher_send, ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_point_in_utc_time @@ -90,7 +91,6 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass, config): """Set up the Eight Sleep component.""" - from pyeight.eight import EightSleep conf = config.get(DOMAIN) user = conf.get(CONF_USERNAME) diff --git a/homeassistant/components/elgato/.translations/ca.json b/homeassistant/components/elgato/.translations/ca.json new file mode 100644 index 00000000000..b717a5abade --- /dev/null +++ b/homeassistant/components/elgato/.translations/ca.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Aquest dispositiu Elgato Key Light ja est\u00e0 configurat.", + "connection_error": "No s'ha pogut connectar amb el dispositiu Elgato Key Light." + }, + "error": { + "connection_error": "No s'ha pogut connectar amb el dispositiu Elgato Key Light." + }, + "flow_title": "Elgato Key Light: {serial_number}", + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3 o adre\u00e7a IP", + "port": "N\u00famero de port" + }, + "description": "Configura l'Elgato Key Light per integrar-lo amb Home Assistant.", + "title": "Enlla\u00e7a Elgato Key Light" + }, + "zeroconf_confirm": { + "description": "Vols afegir l'Elgato Key Light amb n\u00famero de s\u00e8rie `{serial_number}` a Home Assistant?", + "title": "S'ha descobert un dispositiu Elgato Key Light" + } + }, + "title": "Elgato Key Light" + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/.translations/da.json b/homeassistant/components/elgato/.translations/da.json new file mode 100644 index 00000000000..a10e4d9e89f --- /dev/null +++ b/homeassistant/components/elgato/.translations/da.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Denne Elgato Key Light-enhed er allerede konfigureret.", + "connection_error": "Kunne ikke oprette forbindelse til Elgato Key Light-enheden." + }, + "error": { + "connection_error": "Kunne ikke oprette forbindelse til Elgato Key Light-enheden." + }, + "flow_title": "Elgato Key Light: {serial_number}", + "step": { + "user": { + "data": { + "host": "V\u00e6rt eller IP-adresse", + "port": "Portnummer" + }, + "description": "Indstil din Elgato Key Light til at integrere med Home Assistant.", + "title": "Forbind din Elgato Key Light" + }, + "zeroconf_confirm": { + "description": "Vil du tilf\u00f8je Elgato Key Light med serienummer `{serial_number}` til Home Assistant?", + "title": "Fandt Elgato Key Light-enhed" + } + }, + "title": "Elgato Key Light" + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/.translations/en.json b/homeassistant/components/elgato/.translations/en.json new file mode 100644 index 00000000000..d52003d10e1 --- /dev/null +++ b/homeassistant/components/elgato/.translations/en.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "This Elgato Key Light device is already configured.", + "connection_error": "Failed to connect to Elgato Key Light device." + }, + "error": { + "connection_error": "Failed to connect to Elgato Key Light device." + }, + "flow_title": "Elgato Key Light: {serial_number}", + "step": { + "user": { + "data": { + "host": "Host or IP address", + "port": "Port number" + }, + "description": "Set up your Elgato Key Light to integrate with Home Assistant.", + "title": "Link your Elgato Key Light" + }, + "zeroconf_confirm": { + "description": "Do you want to add the Elgato Key Light with serial number `{serial_number}` to Home Assistant?", + "title": "Discovered Elgato Key Light device" + } + }, + "title": "Elgato Key Light" + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/.translations/es.json b/homeassistant/components/elgato/.translations/es.json new file mode 100644 index 00000000000..2e689b5e064 --- /dev/null +++ b/homeassistant/components/elgato/.translations/es.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Este dispositivo Elgato Key Light ya est\u00e1 configurado.", + "connection_error": "No se pudo conectar al dispositivo Elgato Key Light." + }, + "error": { + "connection_error": "No se pudo conectar al dispositivo Elgato Key Light." + }, + "flow_title": "Elgato Key Light: {serial_number}", + "step": { + "user": { + "data": { + "host": "Host o direcci\u00f3n IP", + "port": "N\u00famero de puerto" + }, + "description": "Configura tu Elgato Key Light para integrarlo con Home Assistant.", + "title": "Conecte su Elgato Key Light" + }, + "zeroconf_confirm": { + "description": "\u00bfDesea agregar Elgato Key Light con el n\u00famero de serie `{serial_number}` a Home Assistant?", + "title": "Descubierto dispositivo Elgato Key Light" + } + }, + "title": "Elgato Key Light" + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/.translations/fr.json b/homeassistant/components/elgato/.translations/fr.json new file mode 100644 index 00000000000..e8465a56728 --- /dev/null +++ b/homeassistant/components/elgato/.translations/fr.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Cet appareil Elgato Key Light est d\u00e9j\u00e0 configur\u00e9.", + "connection_error": "Impossible de se connecter au p\u00e9riph\u00e9rique Elgato Key Light." + }, + "error": { + "connection_error": "Impossible de se connecter au p\u00e9riph\u00e9rique Elgato Key Light." + }, + "flow_title": "Elgato Key Light: {serial_number}", + "step": { + "user": { + "data": { + "host": "H\u00f4te ou adresse IP", + "port": "Port" + }, + "description": "Configurez votre Elgato Key Light pour l'int\u00e9grer \u00e0 Home Assistant.", + "title": "Associez votre Elgato Key Light" + }, + "zeroconf_confirm": { + "description": "Voulez-vous ajouter l'Elgato Key Light avec le num\u00e9ro de s\u00e9rie `{serial_number}` \u00e0 Home Assistant?", + "title": "Appareil Elgato Key Light d\u00e9couvert" + } + }, + "title": "Elgato Key Light" + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/.translations/it.json b/homeassistant/components/elgato/.translations/it.json new file mode 100644 index 00000000000..81e363aa01b --- /dev/null +++ b/homeassistant/components/elgato/.translations/it.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Questo dispositivo Elgato Key Light \u00e8 gi\u00e0 configurato.", + "connection_error": "Impossibile connettersi al dispositivo Elgato Key Light." + }, + "error": { + "connection_error": "Impossibile connettersi al dispositivo Elgato Key Light." + }, + "flow_title": "Elgato Key Light: {serial_number}", + "step": { + "user": { + "data": { + "host": "Host o indirizzo IP", + "port": "Numero porta" + }, + "description": "Configura Elgato Key Light per l'integrazione con Home Assistant.", + "title": "Collega il tuo Elgato Key Light" + }, + "zeroconf_confirm": { + "description": "Vuoi aggiungere il dispositivo Elgato Key Light con il numero di serie {serial_number} a Home Assistant?", + "title": "Dispositivo Elgato Key Light rilevato" + } + }, + "title": "Elgato Key Light" + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/.translations/ko.json b/homeassistant/components/elgato/.translations/ko.json new file mode 100644 index 00000000000..9d7ab4ef2b0 --- /dev/null +++ b/homeassistant/components/elgato/.translations/ko.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Elgato Key Light \uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "connection_error": "Elgato Key Light \uae30\uae30\uc5d0 \uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4." + }, + "error": { + "connection_error": "Elgato Key Light \uae30\uae30\uc5d0 \uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4." + }, + "flow_title": "Elgato Key Light: {serial_number}", + "step": { + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8 \ub610\ub294 IP \uc8fc\uc18c", + "port": "\ud3ec\ud2b8 \ubc88\ud638" + }, + "description": "Home Assistant \uc5d0 Elgato Key Light \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4.", + "title": "Elgato Key Light \uc5f0\uacb0" + }, + "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?", + "title": "\ubc1c\uacac\ub41c Elgato Key Light \uae30\uae30" + } + }, + "title": "Elgato Key Light" + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/.translations/lb.json b/homeassistant/components/elgato/.translations/lb.json new file mode 100644 index 00000000000..d53eea87c4c --- /dev/null +++ b/homeassistant/components/elgato/.translations/lb.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "D\u00ebsen Elgato Key Light Apparat ass scho konfigur\u00e9iert.", + "connection_error": "Feeler beim verbannen mam Elgato key Light Apparat." + }, + "error": { + "connection_error": "Feeler beim verbannen mam Elgato key Light Apparat." + }, + "flow_title": "Elgato Key Light: {serial_number}", + "step": { + "user": { + "data": { + "host": "Numm oder IP Adresse", + "port": "Port Nummer" + }, + "description": "\u00c4ren Elgator Key Light als Integratioun mam Home Assistant ariichten.", + "title": "\u00c4ren Elgato Key Light verbannen" + }, + "zeroconf_confirm": { + "description": "W\u00ebllt dir den Elgato Key Light mat der Seriennummer `{serial_number}` am 'Home Assistant dob\u00e4isetzen?", + "title": "Entdeckten Elgato Key Light Apparat" + } + }, + "title": "Elgato Key Light" + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/.translations/nl.json b/homeassistant/components/elgato/.translations/nl.json new file mode 100644 index 00000000000..ca05983eeb5 --- /dev/null +++ b/homeassistant/components/elgato/.translations/nl.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Dit Elgato Key Light apparaat is al geconfigureerd.", + "connection_error": "Kan geen verbinding maken met het Elgato Key Light apparaat." + }, + "error": { + "connection_error": "Kan geen verbinding maken met het Elgato Key Light apparaat." + }, + "flow_title": "Elgato Key Light: {serial_number}", + "step": { + "user": { + "data": { + "host": "Hostnaam of IP-adres", + "port": "Poortnummer" + }, + "description": "Stel uw Elgato Key Light in om te integreren met Home Assistant.", + "title": "Koppel uw Elgato Key Light" + }, + "zeroconf_confirm": { + "description": "Wilt u de Elgato Key Light met serienummer ` {serial_number} ` toevoegen aan Home Assistant?", + "title": "Elgato Key Light apparaat ontdekt" + } + }, + "title": "Elgato Key Light" + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/.translations/no.json b/homeassistant/components/elgato/.translations/no.json new file mode 100644 index 00000000000..8642ae75025 --- /dev/null +++ b/homeassistant/components/elgato/.translations/no.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Denne Elgato Key Light-enheten er allerede konfigurert.", + "connection_error": "Kunne ikke koble til Elgato Key Light-enheten." + }, + "error": { + "connection_error": "Kunne ikke koble til Elgato Key Light-enheten." + }, + "flow_title": "Elgato Key Light: {serial_number}", + "step": { + "user": { + "data": { + "host": "Vert eller IP-adresse", + "port": "Portnummer" + }, + "description": "Sett opp Elgato Key Light for \u00e5 integrere med Home Assistant.", + "title": "Linken ditt Elgato Key Light" + }, + "zeroconf_confirm": { + "description": "Vil du legge Elgato Key Light med serienummer ` {serial_number} til Home Assistant?", + "title": "Oppdaget Elgato Key Light-enheten" + } + }, + "title": "Elgato Key Light" + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/.translations/pl.json b/homeassistant/components/elgato/.translations/pl.json new file mode 100644 index 00000000000..97e10b451f0 --- /dev/null +++ b/homeassistant/components/elgato/.translations/pl.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "To urz\u0105dzenie Elgato Key Light jest ju\u017c skonfigurowane.", + "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia z urz\u0105dzeniem Elgato Key Light." + }, + "error": { + "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia z urz\u0105dzeniem Elgato Key Light." + }, + "flow_title": "Elgato Key Light: {serial_number}", + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "port": "Port" + }, + "description": "Konfiguracja Elgato Key Light w celu integracji z Home Assistant'em.", + "title": "Po\u0142\u0105cz swoje Elgato Key Light" + }, + "zeroconf_confirm": { + "description": "Czy chcesz doda\u0107 urz\u0105dzenie Elgato Key Light o numerze seryjnym `{serial_number}` do Home Assistant'a?", + "title": "Wykryto urz\u0105dzenie Elgato Key Light" + } + }, + "title": "Elgato Key Light" + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/.translations/ru.json b/homeassistant/components/elgato/.translations/ru.json new file mode 100644 index 00000000000..3eeeed631c6 --- /dev/null +++ b/homeassistant/components/elgato/.translations/ru.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Elgato Key Light \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043e.", + "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443 Elgato Key Light." + }, + "error": { + "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443 Elgato Key Light." + }, + "flow_title": "Elgato Key Light: {serial_number}", + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442 \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441", + "port": "\u041d\u043e\u043c\u0435\u0440 \u043f\u043e\u0440\u0442\u0430" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Elgato Key Light \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Home Assistant.", + "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0432\u0430\u0448 Elgato Key Light" + }, + "zeroconf_confirm": { + "description": "\u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c Elgato Key Light \u0441 \u0441\u0435\u0440\u0438\u0439\u043d\u044b\u043c \u043d\u043e\u043c\u0435\u0440\u043e\u043c ` {serial_number} ` \u0432 Home Assistant?", + "title": "\u041d\u0430\u0439\u0434\u0435\u043d\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Elgado Key Light" + } + }, + "title": "\u041e\u0441\u0432\u0435\u0442\u0438\u0442\u0435\u043b\u044c Elgado Key Light" + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/.translations/sl.json b/homeassistant/components/elgato/.translations/sl.json new file mode 100644 index 00000000000..f05b0bcbd8f --- /dev/null +++ b/homeassistant/components/elgato/.translations/sl.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Ta naprava Elgato Key Light je \u017ee nastavljena.", + "connection_error": "Povezava z napravo Elgato Key Light ni uspela." + }, + "error": { + "connection_error": "Povezava z napravo Elgato Key Light ni uspela." + }, + "flow_title": "Elgato Key Light: {serial_number}", + "step": { + "user": { + "data": { + "host": "Gostitelj ali IP naslov", + "port": "\u0160tevilka vrat" + }, + "description": "Nastavite svojo Elgato Key Light tako, da se bo vklju\u010dila v Home Assistant.", + "title": "Pove\u017eite svojo Elgato Key Light" + }, + "zeroconf_confirm": { + "description": "Ali \u017eelite dodati Elgato Key Light s serijsko \u0161tevilko ' {serial_number} ' v Home Assistant-a?", + "title": "Odkrita naprava Elgato Key Light" + } + }, + "title": "Elgato Key Light" + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/.translations/zh-Hant.json b/homeassistant/components/elgato/.translations/zh-Hant.json new file mode 100644 index 00000000000..b187abc5ccd --- /dev/null +++ b/homeassistant/components/elgato/.translations/zh-Hant.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Elgato Key \u7167\u660e\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002", + "connection_error": "Elgato Key \u7167\u660e\u8a2d\u5099\u9023\u7dda\u5931\u6557\u3002" + }, + "error": { + "connection_error": "Elgato Key \u7167\u660e\u8a2d\u5099\u9023\u7dda\u5931\u6557\u3002" + }, + "flow_title": "Elgato Key \u7167\u660e\uff1a{serial_number}", + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u6216 IP \u4f4d\u5740", + "port": "\u901a\u8a0a\u57e0" + }, + "description": "\u8a2d\u5b9a Elgato Key \u7167\u660e\u4ee5\u6574\u5408\u81f3 Home Assistant\u3002", + "title": "\u9023\u7d50 Elgato Key \u7167\u660e\u3002" + }, + "zeroconf_confirm": { + "description": "\u662f\u5426\u8981\u5c07 Elgato Key \u7167\u660e\u5e8f\u865f `{serial_number}` \u65b0\u589e\u81f3 Home Assistant\uff1f", + "title": "\u767c\u73fe\u5230 Elgato Key \u7167\u660e\u8a2d\u5099" + } + }, + "title": "Elgato Key \u7167\u660e" + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/__init__.py b/homeassistant/components/elgato/__init__.py new file mode 100644 index 00000000000..993748033b5 --- /dev/null +++ b/homeassistant/components/elgato/__init__.py @@ -0,0 +1,55 @@ +"""Support for Elgato Key Lights.""" +import logging + +from elgato import Elgato, ElgatoConnectionError + +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.typing import ConfigType + +from .const import DATA_ELGATO_CLIENT, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +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) + elgato = Elgato(entry.data[CONF_HOST], port=entry.data[CONF_PORT], session=session,) + + # Ensure we can connect to it + try: + await elgato.info() + except ElgatoConnectionError as exception: + raise ConfigEntryNotReady from exception + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = {DATA_ELGATO_CLIENT: elgato} + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, LIGHT_DOMAIN) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload Elgato Key Light config entry.""" + # Unload entities for this entry/device. + await hass.config_entries.async_forward_entry_unload(entry, LIGHT_DOMAIN) + + # Cleanup + del hass.data[DOMAIN][entry.entry_id] + if not hass.data[DOMAIN]: + del hass.data[DOMAIN] + + return True diff --git a/homeassistant/components/elgato/config_flow.py b/homeassistant/components/elgato/config_flow.py new file mode 100644 index 00000000000..1d14fca18d2 --- /dev/null +++ b/homeassistant/components/elgato/config_flow.py @@ -0,0 +1,146 @@ +"""Config flow to configure the Elgato Key Light integration.""" +import logging +from typing import Any, Dict, Optional + +from elgato import Elgato, ElgatoError, Info +import voluptuous as vol + +from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL, ConfigFlow +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.helpers import ConfigType +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import CONF_SERIAL_NUMBER, DOMAIN # pylint: disable=unused-import + +_LOGGER = logging.getLogger(__name__) + + +class ElgatoFlowHandler(ConfigFlow, domain=DOMAIN): + """Handle a Elgato Key Light config flow.""" + + VERSION = 1 + CONNECTION_CLASS = CONN_CLASS_LOCAL_POLL + + async def async_step_user( + self, user_input: Optional[ConfigType] = None + ) -> Dict[str, Any]: + """Handle a flow initiated by the user.""" + if user_input is None: + return self._show_setup_form() + + try: + info = await self._get_elgato_info( + user_input[CONF_HOST], user_input[CONF_PORT] + ) + except ElgatoError: + return self._show_setup_form({"base": "connection_error"}) + + # Check if already configured + if await self._device_already_configured(info): + # This serial number is already configured + return self.async_abort(reason="already_configured") + + 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, + }, + ) + + async def async_step_zeroconf( + self, user_input: Optional[ConfigType] = None + ) -> Dict[str, Any]: + """Handle zeroconf discovery.""" + if user_input is None: + return self.async_abort(reason="connection_error") + + # Hostname is format: my-ke.local. + host = user_input["hostname"].rstrip(".") + try: + info = await self._get_elgato_info(host, user_input[CONF_PORT]) + except ElgatoError: + return self.async_abort(reason="connection_error") + + # Check if already configured + if await self._device_already_configured(info): + # This serial number is already configured + return self.async_abort(reason="already_configured") + + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + self.context.update( + { + CONF_HOST: host, + CONF_PORT: user_input[CONF_PORT], + CONF_SERIAL_NUMBER: info.serial_number, + "title_placeholders": {"serial_number": info.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]: + """Handle a flow initiated by zeroconf.""" + if user_input is None: + return self._show_confirm_dialog() + + try: + info = await self._get_elgato_info( + self.context.get(CONF_HOST), self.context.get(CONF_PORT) + ) + except ElgatoError: + return self.async_abort(reason="connection_error") + + # Check if already configured + if await self._device_already_configured(info): + # This serial number is already configured + return self.async_abort(reason="already_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]: + """Show the setup form to the user.""" + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Optional(CONF_PORT, default=9123): int, + } + ), + 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}, + ) + + async def _get_elgato_info(self, host: str, port: int) -> Info: + """Get device information from an Elgato Key Light device.""" + session = async_get_clientsession(self.hass) + elgato = Elgato(host, port=port, session=session,) + return await elgato.info() + + async def _device_already_configured(self, info: Info) -> bool: + """Return if a Elgato Key Light is already configured.""" + for entry in self._async_current_entries(): + if entry.data[CONF_SERIAL_NUMBER] == info.serial_number: + return True + return False diff --git a/homeassistant/components/elgato/const.py b/homeassistant/components/elgato/const.py new file mode 100644 index 00000000000..4983608f899 --- /dev/null +++ b/homeassistant/components/elgato/const.py @@ -0,0 +1,17 @@ +"""Constants for the Elgato Key Light integration.""" + +# Integration domain +DOMAIN = "elgato" + +# Hass data keys +DATA_ELGATO_CLIENT = "elgato_client" + +# Attributes +ATTR_IDENTIFIERS = "identifiers" +ATTR_MANUFACTURER = "manufacturer" +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 new file mode 100644 index 00000000000..99bca1ba20e --- /dev/null +++ b/homeassistant/components/elgato/light.py @@ -0,0 +1,158 @@ +"""Support for LED lights.""" +from datetime import timedelta +import logging +from typing import Any, Callable, Dict, List, Optional + +from elgato import Elgato, ElgatoError, Info, State + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR_TEMP, + Light, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_NAME +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import HomeAssistantType + +from .const import ( + ATTR_IDENTIFIERS, + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_ON, + ATTR_SOFTWARE_VERSION, + ATTR_TEMPERATURE, + DATA_ELGATO_CLIENT, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + +PARALLEL_UPDATES = 1 +SCAN_INTERVAL = timedelta(seconds=10) + + +async def async_setup_entry( + hass: HomeAssistantType, + entry: ConfigEntry, + 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) + + +class ElgatoLight(Light): + """Defines a Elgato Key Light.""" + + 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.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 + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._available + + @property + def unique_id(self) -> str: + """Return the unique ID for this sensor.""" + return self._info.serial_number + + @property + def brightness(self) -> Optional[int]: + """Return the brightness of this light between 1..255.""" + return self._brightness + + @property + def color_temp(self): + """Return the CT color value in mireds.""" + return self._temperature + + @property + def min_mireds(self): + """Return the coldest color_temp that this light supports.""" + return 143 + + @property + def max_mireds(self): + """Return the warmest color_temp that this light supports.""" + return 344 + + @property + def supported_features(self) -> int: + """Flag supported features.""" + return SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP + + @property + def is_on(self) -> bool: + """Return the state of the light.""" + return bool(self._state) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off the light.""" + await self.async_turn_on(on=False) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on the light.""" + data = {} + + data[ATTR_ON] = True + if ATTR_ON in kwargs: + data[ATTR_ON] = kwargs[ATTR_ON] + + if ATTR_COLOR_TEMP in kwargs: + data[ATTR_TEMPERATURE] = kwargs[ATTR_COLOR_TEMP] + + if ATTR_BRIGHTNESS in kwargs: + data[ATTR_BRIGHTNESS] = round((kwargs[ATTR_BRIGHTNESS] / 255) * 100) + + try: + await self.elgato.light(**data) + except ElgatoError: + _LOGGER.error("An error occurred while updating the Elgato Key Light") + self._available = False + + async def async_update(self) -> None: + """Update Elgato entity.""" + 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 + + @property + def device_info(self) -> Dict[str, Any]: + """Return device information about this Elgato Key Light.""" + return { + ATTR_IDENTIFIERS: {(DOMAIN, self._info.serial_number)}, + ATTR_NAME: self._info.product_name, + ATTR_MANUFACTURER: "Elgato", + ATTR_MODEL: self._info.product_name, + ATTR_SOFTWARE_VERSION: f"{self._info.firmware_version} ({self._info.firmware_build_number})", + } diff --git a/homeassistant/components/elgato/manifest.json b/homeassistant/components/elgato/manifest.json new file mode 100644 index 00000000000..bed28364fa1 --- /dev/null +++ b/homeassistant/components/elgato/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "elgato", + "name": "Elgato Key Light", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/elgato", + "requirements": ["elgato==0.1.0"], + "dependencies": [], + "zeroconf": ["_elg._tcp.local."], + "codeowners": ["@frenck"] +} diff --git a/homeassistant/components/elgato/strings.json b/homeassistant/components/elgato/strings.json new file mode 100644 index 00000000000..03c46f02efc --- /dev/null +++ b/homeassistant/components/elgato/strings.json @@ -0,0 +1,27 @@ +{ + "config": { + "title": "Elgato Key Light", + "flow_title": "Elgato Key Light: {serial_number}", + "step": { + "user": { + "title": "Link your Elgato Key Light", + "description": "Set up your Elgato Key Light to integrate with Home Assistant.", + "data": { + "host": "Host or IP address", + "port": "Port number" + } + }, + "zeroconf_confirm": { + "description": "Do you want to add the Elgato Key Light with serial number `{serial_number}` to Home Assistant?", + "title": "Discovered Elgato Key Light device" + } + }, + "error": { + "connection_error": "Failed to connect to Elgato Key Light device." + }, + "abort": { + "already_configured": "This Elgato Key Light device is already configured.", + "connection_error": "Failed to connect to Elgato Key Light device." + } + } +} diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index 601309590c8..67b84c4f3bf 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -35,6 +35,11 @@ CONF_PREFIX = "prefix" _LOGGER = logging.getLogger(__name__) +SERVICE_ALARM_DISPLAY_MESSAGE = "alarm_display_message" +SERVICE_ALARM_ARM_VACATION = "alarm_arm_vacation" +SERVICE_ALARM_ARM_HOME_INSTANT = "alarm_arm_home_instant" +SERVICE_ALARM_ARM_NIGHT_INSTANT = "alarm_arm_night_instant" + SUPPORTED_DOMAINS = [ "alarm_control_panel", "climate", diff --git a/homeassistant/components/elkm1/alarm_control_panel.py b/homeassistant/components/elkm1/alarm_control_panel.py index 38519ab5b3f..de1cb62234c 100644 --- a/homeassistant/components/elkm1/alarm_control_panel.py +++ b/homeassistant/components/elkm1/alarm_control_panel.py @@ -2,7 +2,15 @@ from elkm1_lib.const import AlarmState, ArmedStatus, ArmLevel, ArmUpState import voluptuous as vol -import homeassistant.components.alarm_control_panel as alarm +from homeassistant.components.alarm_control_panel import ( + FORMAT_NUMBER, + AlarmControlPanel, +) +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, +) from homeassistant.const import ( ATTR_CODE, ATTR_ENTITY_ID, @@ -20,7 +28,15 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_send, ) -from . import DOMAIN as ELK_DOMAIN, ElkEntity, create_elk_entities +from . import ( + DOMAIN, + SERVICE_ALARM_ARM_HOME_INSTANT, + SERVICE_ALARM_ARM_NIGHT_INSTANT, + SERVICE_ALARM_ARM_VACATION, + SERVICE_ALARM_DISPLAY_MESSAGE, + ElkEntity, + create_elk_entities, +) SIGNAL_ARM_ENTITY = "elkm1_arm" SIGNAL_DISPLAY_MESSAGE = "elkm1_display_message" @@ -51,7 +67,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= if discovery_info is None: return - elk_datas = hass.data[ELK_DOMAIN] + elk_datas = hass.data[DOMAIN] entities = [] for elk_data in elk_datas.values(): elk = elk_data["elk"] @@ -70,7 +86,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= for service in _arm_services(): hass.services.async_register( - alarm.DOMAIN, service, _arm_service, ELK_ALARM_SERVICE_SCHEMA + DOMAIN, service, _arm_service, ELK_ALARM_SERVICE_SCHEMA ) def _display_message_service(service): @@ -86,8 +102,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= _dispatch(SIGNAL_DISPLAY_MESSAGE, entity_ids, *args) hass.services.async_register( - alarm.DOMAIN, - "elkm1_alarm_display_message", + DOMAIN, + SERVICE_ALARM_DISPLAY_MESSAGE, _display_message_service, DISPLAY_MESSAGE_SERVICE_SCHEMA, ) @@ -95,13 +111,13 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= def _arm_services(): return { - "elkm1_alarm_arm_vacation": ArmLevel.ARMED_VACATION.value, - "elkm1_alarm_arm_home_instant": ArmLevel.ARMED_STAY_INSTANT.value, - "elkm1_alarm_arm_night_instant": ArmLevel.ARMED_NIGHT_INSTANT.value, + SERVICE_ALARM_ARM_VACATION: ArmLevel.ARMED_VACATION.value, + SERVICE_ALARM_ARM_HOME_INSTANT: ArmLevel.ARMED_STAY_INSTANT.value, + SERVICE_ALARM_ARM_NIGHT_INSTANT: ArmLevel.ARMED_NIGHT_INSTANT.value, } -class ElkArea(ElkEntity, alarm.AlarmControlPanel): +class ElkArea(ElkEntity, AlarmControlPanel): """Representation of an Area / Partition within the ElkM1 alarm panel.""" def __init__(self, element, elk, elk_data): @@ -128,7 +144,7 @@ class ElkArea(ElkEntity, alarm.AlarmControlPanel): if keypad.area != self._element.index: return if changeset.get("last_user") is not None: - self._changed_by_entity_id = self.hass.data[ELK_DOMAIN][self._prefix][ + self._changed_by_entity_id = self.hass.data[DOMAIN][self._prefix][ "keypads" ].get(keypad.index, "") self.async_schedule_update_ha_state(True) @@ -136,13 +152,18 @@ class ElkArea(ElkEntity, alarm.AlarmControlPanel): @property def code_format(self): """Return the alarm code format.""" - return alarm.FORMAT_NUMBER + return FORMAT_NUMBER @property def state(self): """Return the state of the element.""" return self._state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT + @property def device_state_attributes(self): """Attributes of the area.""" diff --git a/homeassistant/components/elkm1/services.yaml b/homeassistant/components/elkm1/services.yaml index 40571656963..fbcbf7edc6d 100644 --- a/homeassistant/components/elkm1/services.yaml +++ b/homeassistant/components/elkm1/services.yaml @@ -1,12 +1,65 @@ -speak_word: - description: Speak a word. See list of words in ElkM1 ASCII Protocol documentation. +alarm_arm_home_instant: + description: Arm the ElkM1 in home instant mode. fields: - number: - description: Word number to speak. - example: 142 + entity_id: + description: Name of alarm control panel to arm. + example: 'alarm_control_panel.main' + code: + description: An code to arm the alarm control panel. + example: 1234 + +alarm_arm_night_instant: + description: Arm the ElkM1 in night instant mode. + fields: + entity_id: + description: Name of alarm control panel to arm. + example: 'alarm_control_panel.main' + code: + description: An code to arm the alarm control panel. + example: 1234 + +alarm_arm_vacation: + description: Arm the ElkM1 in vacation mode. + fields: + entity_id: + description: Name of alarm control panel to arm. + example: 'alarm_control_panel.main' + code: + description: An code to arm the alarm control panel. + example: 1234 + +alarm_display_message: + description: Display a message on all of the ElkM1 keypads for an area. + fields: + entity_id: + description: Name of alarm control panel to display messages on. + example: 'alarm_control_panel.main' + clear: + description: 0=clear message, 1=clear message with * key, 2=Display until timeout; default 2 + example: 1 + beep: + description: 0=no beep, 1=beep; default 0 + example: 1 + timeout: + description: Time to display message, 0=forever, max 65535, default 0 + example: 4242 + line1: + description: Up to 16 characters of text (truncated if too long). Default blank. + example: The answer to life, + line2: + description: Up to 16 characters of text (truncated if too long). Default blank. + example: the universe, and everything. + speak_phrase: description: Speak a phrase. See list of phrases in ElkM1 ASCII Protocol documentation. fields: number: description: Phrase number to speak. example: 42 + +speak_word: + description: Speak a word. See list of words in ElkM1 ASCII Protocol documentation. + fields: + number: + description: Word number to speak. + example: 142 diff --git a/homeassistant/components/elv/__init__.py b/homeassistant/components/elv/__init__.py index b6097737414..b776c7f5453 100644 --- a/homeassistant/components/elv/__init__.py +++ b/homeassistant/components/elv/__init__.py @@ -4,8 +4,8 @@ import logging import voluptuous as vol -from homeassistant.helpers import discovery from homeassistant.const import CONF_DEVICE +from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/elv/manifest.json b/homeassistant/components/elv/manifest.json index b4871a805d2..8390fc597f0 100644 --- a/homeassistant/components/elv/manifest.json +++ b/homeassistant/components/elv/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/pca", "dependencies": [], "codeowners": ["@majuss"], - "requirements": ["pypca==0.0.5"] + "requirements": ["pypca==0.0.7"] } diff --git a/homeassistant/components/elv/switch.py b/homeassistant/components/elv/switch.py index 362424c7fac..a77d21cf173 100644 --- a/homeassistant/components/elv/switch.py +++ b/homeassistant/components/elv/switch.py @@ -4,7 +4,7 @@ import logging import pypca from serial import SerialException -from homeassistant.components.switch import SwitchDevice, ATTR_CURRENT_POWER_W +from homeassistant.components.switch import ATTR_CURRENT_POWER_W, SwitchDevice from homeassistant.const import EVENT_HOMEASSISTANT_STOP _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/emby/media_player.py b/homeassistant/components/emby/media_player.py index d8a98a96585..57f781deceb 100644 --- a/homeassistant/components/emby/media_player.py +++ b/homeassistant/components/emby/media_player.py @@ -1,9 +1,10 @@ """Support to interface with the Emby API.""" import logging +from pyemby import EmbyServer import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, MEDIA_TYPE_MOVIE, @@ -70,7 +71,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Emby platform.""" - from pyemby import EmbyServer host = config.get(CONF_HOST) key = config.get(CONF_API_KEY) diff --git a/homeassistant/components/emoncms/sensor.py b/homeassistant/components/emoncms/sensor.py index 5f9d31697b8..34063e4c253 100644 --- a/homeassistant/components/emoncms/sensor.py +++ b/homeassistant/components/emoncms/sensor.py @@ -2,23 +2,23 @@ from datetime import timedelta import logging -import voluptuous as vol import requests +import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_API_KEY, - CONF_URL, - CONF_VALUE_TEMPLATE, - CONF_UNIT_OF_MEASUREMENT, CONF_ID, CONF_SCAN_INTERVAL, - STATE_UNKNOWN, + CONF_UNIT_OF_MEASUREMENT, + CONF_URL, + CONF_VALUE_TEMPLATE, POWER_WATT, + STATE_UNKNOWN, ) -from homeassistant.helpers.entity import Entity 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__) diff --git a/homeassistant/components/emoncms_history/__init__.py b/homeassistant/components/emoncms_history/__init__.py index 3b30a29960b..fd38da1cac1 100644 --- a/homeassistant/components/emoncms_history/__init__.py +++ b/homeassistant/components/emoncms_history/__init__.py @@ -1,20 +1,20 @@ """Support for sending data to Emoncms.""" -import logging from datetime import timedelta +import logging import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( CONF_API_KEY, - CONF_WHITELIST, - CONF_URL, - STATE_UNKNOWN, - STATE_UNAVAILABLE, CONF_SCAN_INTERVAL, + CONF_URL, + CONF_WHITELIST, + STATE_UNAVAILABLE, + STATE_UNKNOWN, ) from homeassistant.helpers import state as state_helper +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import track_point_in_time from homeassistant.util import dt as dt_util diff --git a/homeassistant/components/emulated_hue/__init__.py b/homeassistant/components/emulated_hue/__init__.py index 791085b46f3..0a358c6e894 100644 --- a/homeassistant/components/emulated_hue/__init__.py +++ b/homeassistant/components/emulated_hue/__init__.py @@ -5,20 +5,22 @@ from aiohttp import web import voluptuous as vol from homeassistant import util +from homeassistant.components.http import real_ip from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.deprecation import get_deprecated import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.deprecation import get_deprecated from homeassistant.util.json import load_json, save_json -from homeassistant.components.http import real_ip from .hue_api import ( - HueUsernameView, - HueAllLightsStateView, - HueOneLightStateView, - HueOneLightChangeView, - HueGroupView, HueAllGroupsStateView, + HueAllLightsStateView, + HueFullStateView, + HueGroupView, + HueOneLightChangeView, + HueOneLightStateView, + HueUnauthorizedUser, + HueUsernameView, ) from .upnp import DescriptionXmlView, UPNPResponderThread @@ -113,11 +115,13 @@ async def async_setup(hass, yaml_config): DescriptionXmlView(config).register(app, app.router) HueUsernameView().register(app, app.router) + HueUnauthorizedUser().register(app, app.router) HueAllLightsStateView(config).register(app, app.router) HueOneLightStateView(config).register(app, app.router) HueOneLightChangeView(config).register(app, app.router) HueAllGroupsStateView(config).register(app, app.router) HueGroupView(config).register(app, app.router) + HueFullStateView(config).register(app, app.router) upnp_listener = UPNPResponderThread( config.host_ip_addr, @@ -308,7 +312,7 @@ class Config: def _load_json(filename): - """Wrapper, because we actually want to handle invalid json.""" + """Load JSON, handling invalid syntax.""" try: return load_json(filename) except HomeAssistantError: diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index 5d08af6c5ee..41c6329446c 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -1,8 +1,7 @@ """Support for a Hue API to control Home Assistant.""" +import hashlib import logging -from aiohttp import web - from homeassistant import core from homeassistant.components import ( climate, @@ -35,9 +34,11 @@ from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.const import KEY_REAL_IP from homeassistant.components.light import ( ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, + SUPPORT_COLOR_TEMP, ) from homeassistant.components.media_player.const import ( ATTR_MEDIA_VOLUME_LEVEL, @@ -49,6 +50,7 @@ from homeassistant.const import ( ATTR_TEMPERATURE, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, + HTTP_UNAUTHORIZED, SERVICE_CLOSE_COVER, SERVICE_OPEN_COVER, SERVICE_TURN_OFF, @@ -56,23 +58,54 @@ from homeassistant.const import ( SERVICE_VOLUME_SET, STATE_OFF, STATE_ON, + STATE_UNAVAILABLE, ) from homeassistant.util.network import is_local _LOGGER = logging.getLogger(__name__) +STATE_BRIGHTNESS = "bri" +STATE_COLORMODE = "colormode" +STATE_HUE = "hue" +STATE_SATURATION = "sat" +STATE_COLOR_TEMP = "ct" + +# Hue API states, defined separately in case they change HUE_API_STATE_ON = "on" HUE_API_STATE_BRI = "bri" +HUE_API_STATE_COLORMODE = "colormode" HUE_API_STATE_HUE = "hue" HUE_API_STATE_SAT = "sat" +HUE_API_STATE_CT = "ct" +HUE_API_STATE_EFFECT = "effect" -HUE_API_STATE_HUE_MAX = 65535.0 -HUE_API_STATE_SAT_MAX = 254.0 -HUE_API_STATE_BRI_MAX = 255.0 +# Hue API min/max values - https://developers.meethue.com/develop/hue-api/lights-api/ +HUE_API_STATE_BRI_MIN = 1 # Brightness +HUE_API_STATE_BRI_MAX = 254 +HUE_API_STATE_HUE_MIN = 0 # Hue +HUE_API_STATE_HUE_MAX = 65535 +HUE_API_STATE_SAT_MIN = 0 # Saturation +HUE_API_STATE_SAT_MAX = 254 +HUE_API_STATE_CT_MIN = 153 # Color temp +HUE_API_STATE_CT_MAX = 500 -STATE_BRIGHTNESS = HUE_API_STATE_BRI -STATE_HUE = HUE_API_STATE_HUE -STATE_SATURATION = HUE_API_STATE_SAT +HUE_API_USERNAME = "12345678901234567890" +UNAUTHORIZED_USER = [ + {"error": {"address": "/", "description": "unauthorized user", "type": "1"}} +] + + +class HueUnauthorizedUser(HomeAssistantView): + """Handle requests to find the emulated hue bridge.""" + + url = "/api" + name = "emulated_hue:api:unauthorized_user" + extra_urls = ["/api/"] + requires_auth = False + + async def get(self, request): + """Handle a GET request.""" + return self.json(UNAUTHORIZED_USER) class HueUsernameView(HomeAssistantView): @@ -85,6 +118,9 @@ class HueUsernameView(HomeAssistantView): async def post(self, request): """Handle a POST request.""" + if not is_local(request[KEY_REAL_IP]): + return self.json_message("Only local IPs allowed", HTTP_UNAUTHORIZED) + try: data = await request.json() except ValueError: @@ -93,14 +129,11 @@ class HueUsernameView(HomeAssistantView): if "devicetype" not in data: return self.json_message("devicetype not specified", HTTP_BAD_REQUEST) - if not is_local(request[KEY_REAL_IP]): - return self.json_message("only local IPs allowed", HTTP_BAD_REQUEST) - - return self.json([{"success": {"username": "12345678901234567890"}}]) + return self.json([{"success": {"username": HUE_API_USERNAME}}]) class HueAllGroupsStateView(HomeAssistantView): - """Group handler.""" + """Handle requests for getting info about entity groups.""" url = "/api/{username}/groups" name = "emulated_hue:all_groups:state" @@ -114,7 +147,7 @@ class HueAllGroupsStateView(HomeAssistantView): def get(self, request, username): """Process a request to make the Brilliant Lightpad work.""" if not is_local(request[KEY_REAL_IP]): - return self.json_message("only local IPs allowed", HTTP_BAD_REQUEST) + return self.json_message("Only local IPs allowed", HTTP_UNAUTHORIZED) return self.json({}) @@ -134,7 +167,7 @@ class HueGroupView(HomeAssistantView): def put(self, request, username): """Process a request to make the Logitech Pop working.""" if not is_local(request[KEY_REAL_IP]): - return self.json_message("only local IPs allowed", HTTP_BAD_REQUEST) + return self.json_message("Only local IPs allowed", HTTP_UNAUTHORIZED) return self.json( [ @@ -150,7 +183,7 @@ class HueGroupView(HomeAssistantView): class HueAllLightsStateView(HomeAssistantView): - """Handle requests for getting and setting info about entities.""" + """Handle requests for getting info about all entities.""" url = "/api/{username}/lights" name = "emulated_hue:lights:state" @@ -164,23 +197,45 @@ class HueAllLightsStateView(HomeAssistantView): def get(self, request, username): """Process a request to get the list of available lights.""" if not is_local(request[KEY_REAL_IP]): - return self.json_message("only local IPs allowed", HTTP_BAD_REQUEST) + return self.json_message("Only local IPs allowed", HTTP_UNAUTHORIZED) - hass = request.app["hass"] - json_response = {} + return self.json(create_list_of_entities(self.config, request)) - for entity in hass.states.async_all(): - if self.config.is_entity_exposed(entity): - state = get_entity_state(self.config, entity) - number = self.config.entity_id_to_number(entity.entity_id) - json_response[number] = entity_to_json(self.config, entity, state) +class HueFullStateView(HomeAssistantView): + """Return full state view of emulated hue.""" + + url = "/api/{username}" + name = "emulated_hue:username:state" + requires_auth = False + + def __init__(self, config): + """Initialize the instance of the view.""" + self.config = config + + @core.callback + def get(self, request, username): + """Process a request to get the list of available lights.""" + if not is_local(request[KEY_REAL_IP]): + return self.json_message("only local IPs allowed", HTTP_UNAUTHORIZED) + if username != HUE_API_USERNAME: + return self.json(UNAUTHORIZED_USER) + + json_response = { + "lights": create_list_of_entities(self.config, request), + "config": { + "mac": "00:00:00:00:00:00", + "swversion": "01003542", + "whitelist": {HUE_API_USERNAME: {"name": "HASS BRIDGE"}}, + "ipaddress": f"{self.config.advertise_ip}:{self.config.advertise_port}", + }, + } return self.json(json_response) class HueOneLightStateView(HomeAssistantView): - """Handle requests for getting and setting info about entities.""" + """Handle requests for getting info about a single entity.""" url = "/api/{username}/lights/{entity_id}" name = "emulated_hue:light:state" @@ -194,7 +249,7 @@ class HueOneLightStateView(HomeAssistantView): def get(self, request, username, entity_id): """Process a request to get the state of an individual light.""" if not is_local(request[KEY_REAL_IP]): - return self.json_message("only local IPs allowed", HTTP_BAD_REQUEST) + return self.json_message("Only local IPs allowed", HTTP_UNAUTHORIZED) hass = request.app["hass"] hass_entity_id = self.config.number_to_entity_id(entity_id) @@ -204,27 +259,25 @@ class HueOneLightStateView(HomeAssistantView): "Unknown entity number: %s not found in emulated_hue_ids.json", entity_id, ) - return web.Response(text="Entity not found", status=404) + return self.json_message("Entity not found", HTTP_NOT_FOUND) entity = hass.states.get(hass_entity_id) if entity is None: _LOGGER.error("Entity not found: %s", hass_entity_id) - return web.Response(text="Entity not found", status=404) + return self.json_message("Entity not found", HTTP_NOT_FOUND) if not self.config.is_entity_exposed(entity): _LOGGER.error("Entity not exposed: %s", entity_id) - return web.Response(text="Entity not exposed", status=404) + return self.json_message("Entity not exposed", HTTP_UNAUTHORIZED) - state = get_entity_state(self.config, entity) - - json_response = entity_to_json(self.config, entity, state) + json_response = entity_to_json(self.config, entity) return self.json(json_response) class HueOneLightChangeView(HomeAssistantView): - """Handle requests for getting and setting info about entities.""" + """Handle requests for setting info about entities.""" url = "/api/{username}/lights/{entity_number}/state" name = "emulated_hue:light:state" @@ -237,7 +290,7 @@ class HueOneLightChangeView(HomeAssistantView): async def put(self, request, username, entity_number): """Process a request to set the state of an individual light.""" if not is_local(request[KEY_REAL_IP]): - return self.json_message("only local IPs allowed", HTTP_BAD_REQUEST) + return self.json_message("Only local IPs allowed", HTTP_UNAUTHORIZED) config = self.config hass = request.app["hass"] @@ -255,7 +308,7 @@ class HueOneLightChangeView(HomeAssistantView): if not config.is_entity_exposed(entity): _LOGGER.error("Entity not exposed: %s", entity_id) - return web.Response(text="Entity not exposed", status=404) + return self.json_message("Entity not exposed", HTTP_UNAUTHORIZED) try: request_json = await request.json() @@ -263,12 +316,60 @@ class HueOneLightChangeView(HomeAssistantView): _LOGGER.error("Received invalid json") return self.json_message("Invalid JSON", HTTP_BAD_REQUEST) - # Parse the request into requested "on" status and brightness - parsed = parse_hue_api_put_light_body(request_json, entity) + # Get the entity's supported features + entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - if parsed is None: - _LOGGER.error("Unable to parse data: %s", request_json) - return web.Response(text="Bad request", status=400) + # Parse the request + parsed = { + STATE_ON: False, + STATE_BRIGHTNESS: None, + STATE_HUE: None, + STATE_SATURATION: None, + STATE_COLOR_TEMP: None, + } + + if HUE_API_STATE_ON in request_json: + if not isinstance(request_json[HUE_API_STATE_ON], bool): + _LOGGER.error("Unable to parse data: %s", request_json) + return self.json_message("Bad request", HTTP_BAD_REQUEST) + parsed[STATE_ON] = request_json[HUE_API_STATE_ON] + else: + parsed[STATE_ON] = entity.state != STATE_OFF + + for (key, attr) in ( + (HUE_API_STATE_BRI, STATE_BRIGHTNESS), + (HUE_API_STATE_HUE, STATE_HUE), + (HUE_API_STATE_SAT, STATE_SATURATION), + (HUE_API_STATE_CT, STATE_COLOR_TEMP), + ): + if key in request_json: + try: + parsed[attr] = int(request_json[key]) + 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: + parsed[STATE_ON] = parsed[STATE_BRIGHTNESS] > 0 + if not entity_features & SUPPORT_BRIGHTNESS: + parsed[STATE_BRIGHTNESS] = None + + elif entity.domain == scene.DOMAIN: + parsed[STATE_BRIGHTNESS] = None + parsed[STATE_ON] = True + + elif entity.domain in [ + script.DOMAIN, + media_player.DOMAIN, + fan.DOMAIN, + cover.DOMAIN, + climate.DOMAIN, + ]: + # Convert 0-255 to 0-100 + level = (parsed[STATE_BRIGHTNESS] / HUE_API_STATE_BRI_MAX) * 100 + parsed[STATE_BRIGHTNESS] = round(level) + parsed[STATE_ON] = True # Choose general HA domain domain = core.DOMAIN @@ -282,29 +383,37 @@ class HueOneLightChangeView(HomeAssistantView): # Construct what we need to send to the service data = {ATTR_ENTITY_ID: entity_id} - # Make sure the entity actually supports brightness - entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - + # If the requested entity is a light, set the brightness, hue, + # 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] = parsed[STATE_BRIGHTNESS] + if entity_features & SUPPORT_COLOR: - if parsed[STATE_HUE] is not None: - if parsed[STATE_SATURATION]: + if any((parsed[STATE_HUE], parsed[STATE_SATURATION])): + if parsed[STATE_HUE] is not None: + hue = parsed[STATE_HUE] + else: + hue = 0 + + if parsed[STATE_SATURATION] is not None: sat = parsed[STATE_SATURATION] else: sat = 0 - hue = parsed[STATE_HUE] # Convert hs values to hass hs values - sat = int((sat / HUE_API_STATE_SAT_MAX) * 100) hue = int((hue / HUE_API_STATE_HUE_MAX) * 360) + sat = int((sat / HUE_API_STATE_SAT_MAX) * 100) data[ATTR_HS_COLOR] = (hue, sat) - # If the requested entity is a script add some variables + if entity_features & SUPPORT_COLOR_TEMP: + if parsed[STATE_COLOR_TEMP] is not None: + data[ATTR_COLOR_TEMP] = parsed[STATE_COLOR_TEMP] + + # If the requested entity is a script, add some variables elif entity.domain == script.DOMAIN: data["variables"] = { "requested_state": STATE_ON if parsed[STATE_ON] else STATE_OFF @@ -365,8 +474,8 @@ class HueOneLightChangeView(HomeAssistantView): 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: - # Map the off command to on service = SERVICE_TURN_ON # Caching is required because things like scripts and scenes won't @@ -392,141 +501,65 @@ class HueOneLightChangeView(HomeAssistantView): hass.services.async_call(domain, service, data, blocking=True) ) + # Create success responses for all received keys json_response = [ create_hue_success_response(entity_id, HUE_API_STATE_ON, parsed[STATE_ON]) ] - if parsed[STATE_BRIGHTNESS] is not None: - json_response.append( - create_hue_success_response( - entity_id, HUE_API_STATE_BRI, parsed[STATE_BRIGHTNESS] + for (key, val) in ( + (STATE_BRIGHTNESS, HUE_API_STATE_BRI), + (STATE_HUE, HUE_API_STATE_HUE), + (STATE_SATURATION, HUE_API_STATE_SAT), + (STATE_COLOR_TEMP, HUE_API_STATE_CT), + ): + if parsed[key] is not None: + json_response.append( + create_hue_success_response(entity_id, val, parsed[key]) ) - ) - if parsed[STATE_HUE] is not None: - json_response.append( - create_hue_success_response( - entity_id, HUE_API_STATE_HUE, parsed[STATE_HUE] - ) - ) - if parsed[STATE_SATURATION] is not None: - json_response.append( - create_hue_success_response( - entity_id, HUE_API_STATE_SAT, parsed[STATE_SATURATION] - ) - ) return self.json(json_response) -def parse_hue_api_put_light_body(request_json, entity): - """Parse the body of a request to change the state of a light.""" - data = { - STATE_BRIGHTNESS: None, - STATE_HUE: None, - STATE_ON: False, - STATE_SATURATION: None, - } - - # Make sure the entity actually supports brightness - entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - - if HUE_API_STATE_ON in request_json: - if not isinstance(request_json[HUE_API_STATE_ON], bool): - return None - - if request_json[HUE_API_STATE_ON]: - # Echo requested device be turned on - data[STATE_BRIGHTNESS] = None - data[STATE_ON] = True - else: - # Echo requested device be turned off - data[STATE_BRIGHTNESS] = None - data[STATE_ON] = False - - if HUE_API_STATE_HUE in request_json: - try: - # Clamp brightness from 0 to 65535 - data[STATE_HUE] = max( - 0, min(int(request_json[HUE_API_STATE_HUE]), HUE_API_STATE_HUE_MAX) - ) - except ValueError: - return None - - if HUE_API_STATE_SAT in request_json: - try: - # Clamp saturation from 0 to 254 - data[STATE_SATURATION] = max( - 0, min(int(request_json[HUE_API_STATE_SAT]), HUE_API_STATE_SAT_MAX) - ) - except ValueError: - return None - - if HUE_API_STATE_BRI in request_json: - try: - # Clamp brightness from 0 to 255 - data[STATE_BRIGHTNESS] = max( - 0, min(int(request_json[HUE_API_STATE_BRI]), HUE_API_STATE_BRI_MAX) - ) - except ValueError: - return None - - if entity.domain == light.DOMAIN: - data[STATE_ON] = data[STATE_BRIGHTNESS] > 0 - if not entity_features & SUPPORT_BRIGHTNESS: - data[STATE_BRIGHTNESS] = None - - elif entity.domain == scene.DOMAIN: - data[STATE_BRIGHTNESS] = None - data[STATE_ON] = True - - elif entity.domain in [ - script.DOMAIN, - media_player.DOMAIN, - fan.DOMAIN, - cover.DOMAIN, - climate.DOMAIN, - ]: - # Convert 0-255 to 0-100 - level = (data[STATE_BRIGHTNESS] / HUE_API_STATE_BRI_MAX) * 100 - data[STATE_BRIGHTNESS] = round(level) - data[STATE_ON] = True - - return data - - def get_entity_state(config, entity): """Retrieve and convert state and brightness values for an entity.""" cached_state = config.cached_states.get(entity.entity_id, None) data = { + STATE_ON: False, STATE_BRIGHTNESS: None, STATE_HUE: None, - STATE_ON: False, STATE_SATURATION: None, + STATE_COLOR_TEMP: None, } if cached_state is None: data[STATE_ON] = entity.state != STATE_OFF + if data[STATE_ON]: data[STATE_BRIGHTNESS] = entity.attributes.get(ATTR_BRIGHTNESS, 0) hue_sat = entity.attributes.get(ATTR_HS_COLOR, None) if hue_sat is not None: hue = hue_sat[0] sat = hue_sat[1] - # convert hass hs values back to hue hs values + # Convert hass hs values back to hue hs values data[STATE_HUE] = int((hue / 360.0) * HUE_API_STATE_HUE_MAX) data[STATE_SATURATION] = int((sat / 100.0) * HUE_API_STATE_SAT_MAX) + else: + data[STATE_HUE] = HUE_API_STATE_HUE_MIN + data[STATE_SATURATION] = HUE_API_STATE_SAT_MIN + data[STATE_COLOR_TEMP] = entity.attributes.get(ATTR_COLOR_TEMP, 0) + else: data[STATE_BRIGHTNESS] = 0 data[STATE_HUE] = 0 data[STATE_SATURATION] = 0 + data[STATE_COLOR_TEMP] = 0 - # Make sure the entity actually supports brightness + # Get the entity's supported features entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if entity.domain == light.DOMAIN: if entity_features & SUPPORT_BRIGHTNESS: pass - elif entity.domain == climate.DOMAIN: temperature = entity.attributes.get(ATTR_TEMPERATURE, 0) # Convert 0-100 to 0-255 @@ -536,7 +569,7 @@ def get_entity_state(config, entity): ATTR_MEDIA_VOLUME_LEVEL, 1.0 if data[STATE_ON] else 0.0 ) # Convert 0.0-1.0 to 0-255 - data[STATE_BRIGHTNESS] = round(min(1.0, level) * HUE_API_STATE_BRI_MAX) + data[STATE_BRIGHTNESS] = round(min(1.0, level) * 255) elif entity.domain == fan.DOMAIN: speed = entity.attributes.get(ATTR_SPEED, 0) # Convert 0.0-1.0 to 0-255 @@ -549,12 +582,13 @@ def get_entity_state(config, entity): data[STATE_BRIGHTNESS] = 255 elif entity.domain == cover.DOMAIN: level = entity.attributes.get(ATTR_CURRENT_POSITION, 0) - data[STATE_BRIGHTNESS] = round(level / 100 * HUE_API_STATE_BRI_MAX) + data[STATE_BRIGHTNESS] = round(level / 100 * 255) else: data = cached_state # Make sure brightness is valid if data[STATE_BRIGHTNESS] is None: data[STATE_BRIGHTNESS] = 255 if data[STATE_ON] else 0 + # Make sure hue/saturation are valid if (data[STATE_HUE] is None) or (data[STATE_SATURATION] is None): data[STATE_HUE] = 0 @@ -565,38 +599,132 @@ def get_entity_state(config, entity): data[STATE_HUE] = 0 data[STATE_SATURATION] = 0 + # Clamp brightness, hue, saturation, and color temp to valid values + for (key, v_min, v_max) in ( + (STATE_BRIGHTNESS, HUE_API_STATE_BRI_MIN, HUE_API_STATE_BRI_MAX), + (STATE_HUE, HUE_API_STATE_HUE_MIN, HUE_API_STATE_HUE_MAX), + (STATE_SATURATION, HUE_API_STATE_SAT_MIN, HUE_API_STATE_SAT_MAX), + (STATE_COLOR_TEMP, HUE_API_STATE_CT_MIN, HUE_API_STATE_CT_MAX), + ): + if data[key] is not None: + data[key] = max(v_min, min(data[key], v_max)) + return data -def entity_to_json(config, entity, state): +def entity_to_json(config, entity): """Convert an entity to its Hue bridge JSON representation.""" entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - if (entity_features & SUPPORT_BRIGHTNESS) or entity.domain != light.DOMAIN: - return { - "state": { - HUE_API_STATE_ON: state[STATE_ON], + unique_id = hashlib.md5(entity.entity_id.encode()).hexdigest() + unique_id = "00:{}:{}:{}:{}:{}:{}:{}-{}".format( + unique_id[0:2], + unique_id[2:4], + unique_id[4:6], + unique_id[6:8], + unique_id[8:10], + unique_id[10:12], + unique_id[12:14], + unique_id[14:16], + ) + + state = get_entity_state(config, entity) + + retval = { + "state": { + HUE_API_STATE_ON: state[STATE_ON], + "reachable": entity.state != STATE_UNAVAILABLE, + "mode": "homeautomation", + }, + "name": config.get_entity_name(entity), + "uniqueid": unique_id, + "manufacturername": "Home Assistant", + "swversion": "123", + } + + if ( + (entity_features & SUPPORT_BRIGHTNESS) + and (entity_features & SUPPORT_COLOR) + and (entity_features & SUPPORT_COLOR_TEMP) + ): + # Extended Color light (ZigBee Device ID: 0x0210) + # Same as Color light, but which supports additional setting of color temperature + retval["type"] = "Extended color light" + retval["modelid"] = "HASS231" + retval["state"].update( + { HUE_API_STATE_BRI: state[STATE_BRIGHTNESS], HUE_API_STATE_HUE: state[STATE_HUE], HUE_API_STATE_SAT: state[STATE_SATURATION], - "reachable": True, - }, - "type": "Dimmable light", - "name": config.get_entity_name(entity), - "modelid": "HASS123", - "uniqueid": entity.entity_id, - "swversion": "123", - } - return { - "state": {HUE_API_STATE_ON: state[STATE_ON], "reachable": True}, - "type": "On/off light", - "name": config.get_entity_name(entity), - "modelid": "HASS321", - "uniqueid": entity.entity_id, - "swversion": "123", - } + HUE_API_STATE_CT: state[STATE_COLOR_TEMP], + HUE_API_STATE_EFFECT: "none", + } + ) + if state[STATE_HUE] > 0 or state[STATE_SATURATION] > 0: + retval["state"][HUE_API_STATE_COLORMODE] = "hs" + else: + retval["state"][HUE_API_STATE_COLORMODE] = "ct" + elif (entity_features & SUPPORT_BRIGHTNESS) and (entity_features & SUPPORT_COLOR): + # Color light (ZigBee Device ID: 0x0200) + # Supports on/off, dimming and color control (hue/saturation, enhanced hue, color loop and XY) + retval["type"] = "Color light" + retval["modelid"] = "HASS213" + retval["state"].update( + { + HUE_API_STATE_BRI: state[STATE_BRIGHTNESS], + HUE_API_STATE_COLORMODE: "hs", + HUE_API_STATE_HUE: state[STATE_HUE], + HUE_API_STATE_SAT: state[STATE_SATURATION], + HUE_API_STATE_EFFECT: "none", + } + ) + elif (entity_features & SUPPORT_BRIGHTNESS) and ( + entity_features & SUPPORT_COLOR_TEMP + ): + # Color temperature light (ZigBee Device ID: 0x0220) + # Supports groups, scenes, on/off, dimming, and setting of a color temperature + retval["type"] = "Color temperature light" + retval["modelid"] = "HASS312" + retval["state"].update( + {HUE_API_STATE_COLORMODE: "ct", HUE_API_STATE_CT: state[STATE_COLOR_TEMP]} + ) + elif ( + entity_features + & ( + SUPPORT_BRIGHTNESS + | SUPPORT_SET_POSITION + | SUPPORT_SET_SPEED + | SUPPORT_VOLUME_SET + | SUPPORT_TARGET_TEMPERATURE + ) + ) or entity.domain == script.DOMAIN: + # Dimmable light (ZigBee Device ID: 0x0100) + # Supports groups, scenes, on/off and dimming + retval["type"] = "Dimmable light" + retval["modelid"] = "HASS123" + retval["state"].update({HUE_API_STATE_BRI: state[STATE_BRIGHTNESS]}) + else: + # On/off light (ZigBee Device ID: 0x0000) + # Supports groups, scenes and on/off control + retval["type"] = "On/off light" + retval["modelid"] = "HASS321" + + return retval def create_hue_success_response(entity_id, attr, value): """Create a success response for an attribute set on a light.""" success_key = f"/lights/{entity_id}/state/{attr}" return {"success": {success_key: value}} + + +def create_list_of_entities(config, request): + """Create a list of all entites.""" + hass = request.app["hass"] + json_response = {} + + for entity in hass.states.async_all(): + if config.is_entity_exposed(entity): + number = config.entity_id_to_number(entity.entity_id) + json_response[number] = entity_to_json(config, entity) + + return json_response diff --git a/homeassistant/components/emulated_hue/manifest.json b/homeassistant/components/emulated_hue/manifest.json index 9b3b00d20b2..ddd39443886 100644 --- a/homeassistant/components/emulated_hue/manifest.json +++ b/homeassistant/components/emulated_hue/manifest.json @@ -6,5 +6,7 @@ "aiohttp_cors==0.7.0" ], "dependencies": [], - "codeowners": [] + "codeowners": [ + "@NobleKangaroo" + ] } diff --git a/homeassistant/components/emulated_hue/upnp.py b/homeassistant/components/emulated_hue/upnp.py index 412dfdd673e..0583ae9a1b6 100644 --- a/homeassistant/components/emulated_hue/upnp.py +++ b/homeassistant/components/emulated_hue/upnp.py @@ -1,8 +1,8 @@ """Support UPNP discovery method that mimics Hue hubs.""" -import threading -import socket import logging import select +import socket +import threading from aiohttp import web diff --git a/homeassistant/components/emulated_roku/.translations/da.json b/homeassistant/components/emulated_roku/.translations/da.json index 0479dee437d..0da64fac623 100644 --- a/homeassistant/components/emulated_roku/.translations/da.json +++ b/homeassistant/components/emulated_roku/.translations/da.json @@ -6,14 +6,14 @@ "step": { "user": { "data": { - "advertise_ip": "Adviserings IP", - "advertise_port": "Adviserings port", - "host_ip": "V\u00e6rt IP", - "listen_port": "Lytte port", + "advertise_ip": "Adviserings-IP", + "advertise_port": "Adviseringsport", + "host_ip": "V\u00e6rts-IP", + "listen_port": "Lytte-port", "name": "Navn", "upnp_bind_multicast": "Bind multicast (sand/falsk)" }, - "title": "Angiv server konfiguration" + "title": "Angiv server-konfiguration" } }, "title": "EmulatedRoku" diff --git a/homeassistant/components/emulated_roku/binding.py b/homeassistant/components/emulated_roku/binding.py index 4c98af69848..a44effff55a 100644 --- a/homeassistant/components/emulated_roku/binding.py +++ b/homeassistant/components/emulated_roku/binding.py @@ -1,6 +1,8 @@ """Bridge between emulated_roku and Home Assistant.""" import logging +from emulated_roku import EmulatedRokuCommandHandler, EmulatedRokuServer + from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP from homeassistant.core import CoreState, EventOrigin @@ -51,7 +53,6 @@ class EmulatedRoku: async def setup(self): """Start the emulated_roku server.""" - from emulated_roku import EmulatedRokuServer, EmulatedRokuCommandHandler class EventCommandHandler(EmulatedRokuCommandHandler): """emulated_roku command handler to turn commands into events.""" diff --git a/homeassistant/components/enigma2/media_player.py b/homeassistant/components/enigma2/media_player.py index 5b0e705f392..85dec4abd94 100644 --- a/homeassistant/components/enigma2/media_player.py +++ b/homeassistant/components/enigma2/media_player.py @@ -1,35 +1,36 @@ """Support for Enigma2 media players.""" import logging +from openwebif.api import CreateDevice import voluptuous as vol from homeassistant.components.media_player import MediaPlayerDevice -from homeassistant.helpers.config_validation import PLATFORM_SCHEMA from homeassistant.components.media_player.const import ( + MEDIA_TYPE_TVSHOW, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, - SUPPORT_TURN_ON, + SUPPORT_SELECT_SOURCE, + SUPPORT_STOP, SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - SUPPORT_STOP, - SUPPORT_SELECT_SOURCE, SUPPORT_VOLUME_STEP, - MEDIA_TYPE_TVSHOW, ) from homeassistant.const import ( CONF_HOST, CONF_NAME, - CONF_USERNAME, CONF_PASSWORD, + CONF_PORT, CONF_SSL, + CONF_USERNAME, STATE_OFF, STATE_ON, STATE_PLAYING, - CONF_PORT, ) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.config_validation import PLATFORM_SCHEMA _LOGGER = logging.getLogger(__name__) @@ -101,8 +102,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): config[CONF_DEEP_STANDBY] = DEFAULT_DEEP_STANDBY config[CONF_SOURCE_BOUQUET] = DEFAULT_SOURCE_BOUQUET - from openwebif.api import CreateDevice - device = CreateDevice( host=config[CONF_HOST], port=config.get(CONF_PORT), diff --git a/homeassistant/components/enocean/__init__.py b/homeassistant/components/enocean/__init__.py index b75c8f001c0..876c7a1f05b 100644 --- a/homeassistant/components/enocean/__init__.py +++ b/homeassistant/components/enocean/__init__.py @@ -1,11 +1,14 @@ """Support for EnOcean devices.""" import logging +from enocean.communicators.serialcommunicator import SerialCommunicator +from enocean.protocol.packet import Packet, RadioPacket +from enocean.utils import combine_hex import voluptuous as vol from homeassistant.const import CONF_DEVICE -from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -34,7 +37,6 @@ class EnOceanDongle: def __init__(self, hass, ser): """Initialize the EnOcean dongle.""" - from enocean.communicators.serialcommunicator import SerialCommunicator self.__communicator = SerialCommunicator(port=ser, callback=self.callback) self.__communicator.start() @@ -53,7 +55,6 @@ class EnOceanDongle: This is the callback function called by python-enocan whenever there is an incoming packet. """ - from enocean.protocol.packet import RadioPacket if isinstance(packet, RadioPacket): _LOGGER.debug("Received radio packet: %s", packet) @@ -76,7 +77,6 @@ class EnOceanDevice(Entity): def _message_received_callback(self, packet): """Handle incoming packets.""" - from enocean.utils import combine_hex if packet.sender_int == combine_hex(self.dev_id): self.value_changed(packet) @@ -84,10 +84,8 @@ class EnOceanDevice(Entity): def value_changed(self, packet): """Update the internal state of the device when a packet arrives.""" - # pylint: disable=no-self-use def send_command(self, data, optional, packet_type): """Send a command via the EnOcean dongle.""" - from enocean.protocol.packet import Packet packet = Packet(packet_type, data=data, optional=optional) self.hass.helpers.dispatcher.dispatcher_send(SIGNAL_SEND_MESSAGE, packet) diff --git a/homeassistant/components/enocean/sensor.py b/homeassistant/components/enocean/sensor.py index 2e6b5bdb986..59ca10da791 100644 --- a/homeassistant/components/enocean/sensor.py +++ b/homeassistant/components/enocean/sensor.py @@ -10,9 +10,12 @@ from homeassistant.const import ( CONF_ID, CONF_NAME, DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, - TEMP_CELSIUS, POWER_WATT, + STATE_CLOSED, + STATE_OPEN, + TEMP_CELSIUS, ) import homeassistant.helpers.config_validation as cv @@ -25,34 +28,44 @@ CONF_RANGE_TO = "range_to" DEFAULT_NAME = "EnOcean sensor" -DEVICE_CLASS_POWER = "powersensor" +SENSOR_TYPE_HUMIDITY = "humidity" +SENSOR_TYPE_POWER = "powersensor" +SENSOR_TYPE_TEMPERATURE = "temperature" +SENSOR_TYPE_WINDOWHANDLE = "windowhandle" SENSOR_TYPES = { - DEVICE_CLASS_HUMIDITY: { + SENSOR_TYPE_HUMIDITY: { "name": "Humidity", "unit": "%", "icon": "mdi:water-percent", "class": DEVICE_CLASS_HUMIDITY, }, - DEVICE_CLASS_POWER: { + SENSOR_TYPE_POWER: { "name": "Power", "unit": POWER_WATT, "icon": "mdi:power-plug", "class": DEVICE_CLASS_POWER, }, - DEVICE_CLASS_TEMPERATURE: { + SENSOR_TYPE_TEMPERATURE: { "name": "Temperature", "unit": TEMP_CELSIUS, "icon": "mdi:thermometer", "class": DEVICE_CLASS_TEMPERATURE, }, + SENSOR_TYPE_WINDOWHANDLE: { + "name": "WindowHandle", + "unit": None, + "icon": "mdi:window", + "class": None, + }, } + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_DEVICE_CLASS, default=DEVICE_CLASS_POWER): cv.string, + vol.Optional(CONF_DEVICE_CLASS, default=SENSOR_TYPE_POWER): cv.string, vol.Optional(CONF_MAX_TEMP, default=40): vol.Coerce(int), vol.Optional(CONF_MIN_TEMP, default=0): vol.Coerce(int), vol.Optional(CONF_RANGE_FROM, default=255): cv.positive_int, @@ -65,9 +78,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up an EnOcean sensor device.""" dev_id = config.get(CONF_ID) dev_name = config.get(CONF_NAME) - dev_class = config.get(CONF_DEVICE_CLASS) + sensor_type = config.get(CONF_DEVICE_CLASS) - if dev_class == DEVICE_CLASS_TEMPERATURE: + if sensor_type == SENSOR_TYPE_TEMPERATURE: temp_min = config.get(CONF_MIN_TEMP) temp_max = config.get(CONF_MAX_TEMP) range_from = config.get(CONF_RANGE_FROM) @@ -80,12 +93,15 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ] ) - elif dev_class == DEVICE_CLASS_HUMIDITY: + elif sensor_type == SENSOR_TYPE_HUMIDITY: add_entities([EnOceanHumiditySensor(dev_id, dev_name)]) - elif dev_class == DEVICE_CLASS_POWER: + elif sensor_type == SENSOR_TYPE_POWER: add_entities([EnOceanPowerSensor(dev_id, dev_name)]) + elif sensor_type == SENSOR_TYPE_WINDOWHANDLE: + add_entities([EnOceanWindowHandle(dev_id, dev_name)]) + class EnOceanSensor(enocean.EnOceanDevice): """Representation of an EnOcean sensor device such as a power meter.""" @@ -140,7 +156,7 @@ class EnOceanPowerSensor(EnOceanSensor): def __init__(self, dev_id, dev_name): """Initialize the EnOcean power sensor device.""" - super().__init__(dev_id, dev_name, DEVICE_CLASS_POWER) + super().__init__(dev_id, dev_name, SENSOR_TYPE_POWER) def value_changed(self, packet): """Update the internal state of the sensor.""" @@ -175,7 +191,7 @@ class EnOceanTemperatureSensor(EnOceanSensor): def __init__(self, dev_id, dev_name, scale_min, scale_max, range_from, range_to): """Initialize the EnOcean temperature sensor device.""" - super().__init__(dev_id, dev_name, DEVICE_CLASS_TEMPERATURE) + super().__init__(dev_id, dev_name, SENSOR_TYPE_TEMPERATURE) self._scale_min = scale_min self._scale_max = scale_max self.range_from = range_from @@ -205,7 +221,7 @@ class EnOceanHumiditySensor(EnOceanSensor): def __init__(self, dev_id, dev_name): """Initialize the EnOcean humidity sensor device.""" - super().__init__(dev_id, dev_name, DEVICE_CLASS_HUMIDITY) + super().__init__(dev_id, dev_name, SENSOR_TYPE_HUMIDITY) def value_changed(self, packet): """Update the internal state of the sensor.""" @@ -214,3 +230,29 @@ class EnOceanHumiditySensor(EnOceanSensor): humidity = packet.data[2] * 100 / 250 self._state = round(humidity, 1) self.schedule_update_ha_state() + + +class EnOceanWindowHandle(EnOceanSensor): + """Representation of an EnOcean window handle device. + + EEPs (EnOcean Equipment Profiles): + - F6-10-00 (Mechanical handle / Hoppe AG) + """ + + def __init__(self, dev_id, dev_name): + """Initialize the EnOcean window handle sensor device.""" + super().__init__(dev_id, dev_name, SENSOR_TYPE_WINDOWHANDLE) + + def value_changed(self, packet): + """Update the internal state of the sensor.""" + + action = (packet.data[1] & 0x70) >> 4 + + if action == 0x07: + self._state = STATE_CLOSED + if action in (0x04, 0x06): + self._state = STATE_OPEN + if action == 0x05: + self._state = "tilt" + + self.schedule_update_ha_state() diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index 5b5f94f7c8c..fc79659ff76 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -3,7 +3,7 @@ "name": "Enphase envoy", "documentation": "https://www.home-assistant.io/integrations/enphase_envoy", "requirements": [ - "envoy_reader==0.8.6" + "envoy_reader==0.11.0" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/enphase_envoy/sensor.py b/homeassistant/components/enphase_envoy/sensor.py index 13784e24d77..a2b50f20eb6 100644 --- a/homeassistant/components/enphase_envoy/sensor.py +++ b/homeassistant/components/enphase_envoy/sensor.py @@ -1,19 +1,22 @@ """Support for Enphase Envoy solar energy monitor.""" import logging +from envoy_reader.envoy_reader import EnvoyReader +import requests import voluptuous as vol -from homeassistant.helpers.entity import Entity from homeassistant.components.sensor import PLATFORM_SCHEMA -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( CONF_IP_ADDRESS, CONF_MONITORED_CONDITIONS, CONF_NAME, - POWER_WATT, + CONF_PASSWORD, + CONF_USERNAME, ENERGY_WATT_HOUR, + POWER_WATT, ) - +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -42,6 +45,8 @@ CONST_DEFAULT_HOST = "envoy" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional(CONF_IP_ADDRESS, default=CONST_DEFAULT_HOST): cv.string, + vol.Optional(CONF_USERNAME, default="envoy"): cv.string, + vol.Optional(CONF_PASSWORD, default=""): cv.string, vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSORS)): vol.All( cv.ensure_list, [vol.In(list(SENSORS))] ), @@ -52,31 +57,42 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Enphase Envoy sensor.""" - from envoy_reader.envoy_reader import EnvoyReader - ip_address = config[CONF_IP_ADDRESS] monitored_conditions = config[CONF_MONITORED_CONDITIONS] name = config[CONF_NAME] + username = config[CONF_USERNAME] + password = config[CONF_PASSWORD] + + envoy_reader = EnvoyReader(ip_address, username, password) entities = [] # Iterate through the list of sensors for condition in monitored_conditions: if condition == "inverters": - inverters = await EnvoyReader(ip_address).inverters_production() + try: + inverters = await envoy_reader.inverters_production() + except requests.exceptions.HTTPError: + _LOGGER.warning( + "Authentication for Inverter data failed during setup: %s", + ip_address, + ) + continue + if isinstance(inverters, dict): for inverter in inverters: entities.append( Envoy( - ip_address, + envoy_reader, condition, f"{name}{SENSORS[condition][0]} {inverter}", SENSORS[condition][1], ) ) + else: entities.append( Envoy( - ip_address, + envoy_reader, condition, f"{name}{SENSORS[condition][0]}", SENSORS[condition][1], @@ -88,13 +104,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class Envoy(Entity): """Implementation of the Enphase Envoy sensors.""" - def __init__(self, ip_address, sensor_type, name, unit): + def __init__(self, envoy_reader, sensor_type, name, unit): """Initialize the sensor.""" - self._ip_address = ip_address + self._envoy_reader = envoy_reader + self._type = sensor_type self._name = name self._unit_of_measurement = unit - self._type = sensor_type self._state = None + self._last_reported = None @property def name(self): @@ -116,12 +133,18 @@ class Envoy(Entity): """Icon to use in the frontend, if any.""" return ICON + @property + def device_state_attributes(self): + """Return the state attributes.""" + if self._type == "inverters": + return {"last_reported": self._last_reported} + + return None + async def async_update(self): """Get the energy production data from the Enphase Envoy.""" - from envoy_reader.envoy_reader import EnvoyReader - if self._type != "inverters": - _state = await getattr(EnvoyReader(self._ip_address), self._type)() + _state = await getattr(self._envoy_reader, self._type)() if isinstance(_state, int): self._state = _state else: @@ -129,9 +152,17 @@ class Envoy(Entity): self._state = None elif self._type == "inverters": - inverters = await (EnvoyReader(self._ip_address).inverters_production()) + try: + inverters = await (self._envoy_reader.inverters_production()) + except requests.exceptions.HTTPError: + _LOGGER.warning( + "Authentication for Inverter data failed during update: %s", + self._envoy_reader.host, + ) + if isinstance(inverters, dict): serial_number = self._name.split(" ")[2] - self._state = inverters[serial_number] + self._state = inverters[serial_number][0] + self._last_reported = inverters[serial_number][1] else: self._state = None diff --git a/homeassistant/components/entur_public_transport/manifest.json b/homeassistant/components/entur_public_transport/manifest.json index b0910f16536..6396ff8e678 100644 --- a/homeassistant/components/entur_public_transport/manifest.json +++ b/homeassistant/components/entur_public_transport/manifest.json @@ -3,8 +3,10 @@ "name": "Entur public transport", "documentation": "https://www.home-assistant.io/integrations/entur_public_transport", "requirements": [ - "enturclient==0.2.0" + "enturclient==0.2.1" ], "dependencies": [], - "codeowners": [] -} + "codeowners": [ + "@hfurubotten" + ] +} \ 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 0f8324ded9e..2ecae21824e 100644 --- a/homeassistant/components/entur_public_transport/sensor.py +++ b/homeassistant/components/entur_public_transport/sensor.py @@ -2,6 +2,7 @@ from datetime import datetime, timedelta import logging +from enturclient import EnturPublicTransportData import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -87,7 +88,6 @@ def due_in_minutes(timestamp: datetime) -> int: async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Entur public transport sensor.""" - from enturclient import EnturPublicTransportData expand = config.get(CONF_EXPAND_PLATFORMS) line_whitelist = config.get(CONF_WHITELIST_LINES) diff --git a/homeassistant/components/environment_canada/camera.py b/homeassistant/components/environment_canada/camera.py index 2a23fb95a18..4ef3e17fc46 100644 --- a/homeassistant/components/environment_canada/camera.py +++ b/homeassistant/components/environment_canada/camera.py @@ -7,17 +7,18 @@ https://home-assistant.io/components/camera.environment_canada/ import datetime import logging +from env_canada import ECRadar import voluptuous as vol from homeassistant.components.camera import PLATFORM_SCHEMA, Camera from homeassistant.const import ( - CONF_NAME, + ATTR_ATTRIBUTION, CONF_LATITUDE, CONF_LONGITUDE, - ATTR_ATTRIBUTION, + CONF_NAME, ) -from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv +from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -46,7 +47,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Environment Canada camera.""" - from env_canada import ECRadar if config.get(CONF_STATION): radar_object = ECRadar( diff --git a/homeassistant/components/environment_canada/sensor.py b/homeassistant/components/environment_canada/sensor.py index a140927c980..1568ba19d6b 100644 --- a/homeassistant/components/environment_canada/sensor.py +++ b/homeassistant/components/environment_canada/sensor.py @@ -8,18 +8,19 @@ from datetime import datetime, timedelta import logging import re +from env_canada import ECData import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - TEMP_CELSIUS, - CONF_LATITUDE, - CONF_LONGITUDE, ATTR_ATTRIBUTION, ATTR_LOCATION, + CONF_LATITUDE, + CONF_LONGITUDE, + TEMP_CELSIUS, ) -from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -56,7 +57,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Environment Canada sensor.""" - from env_canada import ECData if config.get(CONF_STATION): ec_data = ECData( @@ -134,8 +134,11 @@ class ECSensor(Entity): ) elif self.sensor_type == "tendency": self._state = str(value).capitalize() - else: + elif value is not None and len(value) > 255: self._state = value[:255] + _LOGGER.info("Value for %s truncated to 255 characters", self._unique_id) + else: + self._state = value if sensor_data.get("unit") == "C" or self.sensor_type in [ "wind_chill", diff --git a/homeassistant/components/environment_canada/weather.py b/homeassistant/components/environment_canada/weather.py index a4fad083d2a..572543e39c4 100644 --- a/homeassistant/components/environment_canada/weather.py +++ b/homeassistant/components/environment_canada/weather.py @@ -20,8 +20,8 @@ from homeassistant.components.weather import ( WeatherEntity, ) from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_CELSIUS -import homeassistant.util.dt as dt import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/envirophat/sensor.py b/homeassistant/components/envirophat/sensor.py index 2aaeefa48cf..ce1f154f911 100644 --- a/homeassistant/components/envirophat/sensor.py +++ b/homeassistant/components/envirophat/sensor.py @@ -1,12 +1,12 @@ """Support for Enviro pHAT sensors.""" +from datetime import timedelta import importlib import logging -from datetime import timedelta import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import TEMP_CELSIUS, CONF_DISPLAY_OPTIONS, CONF_NAME +from homeassistant.const import CONF_DISPLAY_OPTIONS, CONF_NAME, TEMP_CELSIUS import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle diff --git a/homeassistant/components/envisalink/__init__.py b/homeassistant/components/envisalink/__init__.py index 6cdedf89744..14113537de6 100644 --- a/homeassistant/components/envisalink/__init__.py +++ b/homeassistant/components/envisalink/__init__.py @@ -2,14 +2,15 @@ import asyncio import logging +from pyenvisalink import EnvisalinkAlarmPanel import voluptuous as vol +from homeassistant.const import CONF_HOST, CONF_TIMEOUT, EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.const import EVENT_HOMEASSISTANT_STOP, CONF_TIMEOUT, CONF_HOST -from homeassistant.helpers.entity import Entity from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -98,7 +99,6 @@ SERVICE_SCHEMA = vol.Schema( async def async_setup(hass, config): """Set up for Envisalink devices.""" - from pyenvisalink import EnvisalinkAlarmPanel conf = config.get(DOMAIN) diff --git a/homeassistant/components/envisalink/alarm_control_panel.py b/homeassistant/components/envisalink/alarm_control_panel.py index 663f19c8ed5..7630169dcad 100644 --- a/homeassistant/components/envisalink/alarm_control_panel.py +++ b/homeassistant/components/envisalink/alarm_control_panel.py @@ -3,7 +3,16 @@ import logging import voluptuous as vol -import homeassistant.components.alarm_control_panel as alarm +from homeassistant.components.alarm_control_panel import ( + FORMAT_NUMBER, + AlarmControlPanel, +) +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, + SUPPORT_ALARM_TRIGGER, +) from homeassistant.const import ( ATTR_ENTITY_ID, STATE_ALARM_ARMED_AWAY, @@ -23,6 +32,7 @@ from . import ( CONF_PANIC, CONF_PARTITIONNAME, DATA_EVL, + DOMAIN, PARTITION_SCHEMA, SIGNAL_KEYPAD_UPDATE, SIGNAL_PARTITION_UPDATE, @@ -31,7 +41,7 @@ from . import ( _LOGGER = logging.getLogger(__name__) -SERVICE_ALARM_KEYPRESS = "envisalink_alarm_keypress" +SERVICE_ALARM_KEYPRESS = "alarm_keypress" ATTR_KEYPRESS = "keypress" ALARM_KEYPRESS_SCHEMA = vol.Schema( { @@ -77,7 +87,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= device.async_alarm_keypress(keypress) hass.services.async_register( - alarm.DOMAIN, + DOMAIN, SERVICE_ALARM_KEYPRESS, alarm_keypress_handler, schema=ALARM_KEYPRESS_SCHEMA, @@ -86,7 +96,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= return True -class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel): +class EnvisalinkAlarm(EnvisalinkDevice, AlarmControlPanel): """Representation of an Envisalink-based alarm panel.""" def __init__( @@ -118,7 +128,7 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel): """Regex for code format or None if no code is required.""" if self._code: return None - return alarm.FORMAT_NUMBER + return FORMAT_NUMBER @property def state(self): @@ -141,6 +151,16 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel): state = STATE_ALARM_DISARMED return state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return ( + SUPPORT_ALARM_ARM_HOME + | SUPPORT_ALARM_ARM_AWAY + | SUPPORT_ALARM_ARM_NIGHT + | SUPPORT_ALARM_TRIGGER + ) + async def async_alarm_disarm(self, code=None): """Send disarm command.""" if code: diff --git a/homeassistant/components/envisalink/services.yaml b/homeassistant/components/envisalink/services.yaml index e31aa804059..2a5f91791df 100644 --- a/homeassistant/components/envisalink/services.yaml +++ b/homeassistant/components/envisalink/services.yaml @@ -1,5 +1,15 @@ # Describes the format for available Envisalink services. +alarm_keypress: + description: Send custom keypresses to the alarm. + fields: + entity_id: + description: Name of the alarm control panel to trigger. + example: 'alarm_control_panel.downstairs' + keypress: + description: 'String to send to the alarm panel (1-6 characters).' + example: '*71' + invoke_custom_function: description: > Allows users with DSC panels to trigger a PGM output (1-4). diff --git a/homeassistant/components/epson/const.py b/homeassistant/components/epson/const.py new file mode 100644 index 00000000000..23f3b081d01 --- /dev/null +++ b/homeassistant/components/epson/const.py @@ -0,0 +1,10 @@ +"""Constants for the Epson projector component.""" +DOMAIN = "epson" +SERVICE_SELECT_CMODE = "select_cmode" + +ATTR_CMODE = "cmode" + +DATA_EPSON = "epson" +DEFAULT_NAME = "EPSON Projector" + +SUPPORT_CMODE = 33001 diff --git a/homeassistant/components/epson/media_player.py b/homeassistant/components/epson/media_player.py index 638f012ac7a..b39722c39f3 100644 --- a/homeassistant/components/epson/media_player.py +++ b/homeassistant/components/epson/media_player.py @@ -1,8 +1,7 @@ """Support for Epson projector.""" import logging -import voluptuous as vol - +import epson_projector as epson from epson_projector.const import ( BACK, BUSY, @@ -19,17 +18,16 @@ from epson_projector.const import ( POWER, SOURCE, SOURCE_LIST, - TURN_ON, TURN_OFF, - VOLUME, + TURN_ON, VOL_DOWN, VOL_UP, + VOLUME, ) -import epson_projector as epson +import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( - DOMAIN, SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, @@ -50,16 +48,17 @@ from homeassistant.const import ( from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv +from .const import ( + ATTR_CMODE, + DATA_EPSON, + DEFAULT_NAME, + DOMAIN, + SERVICE_SELECT_CMODE, + SUPPORT_CMODE, +) + _LOGGER = logging.getLogger(__name__) -ATTR_CMODE = "cmode" - -DATA_EPSON = "epson" -DEFAULT_NAME = "EPSON Projector" - -SERVICE_SELECT_CMODE = "epson_select_cmode" -SUPPORT_CMODE = 33001 - SUPPORT_EPSON = ( SUPPORT_TURN_ON | SUPPORT_TURN_OFF diff --git a/homeassistant/components/epson/services.yaml b/homeassistant/components/epson/services.yaml index e69de29bb2d..6e9724c95f7 100644 --- a/homeassistant/components/epson/services.yaml +++ b/homeassistant/components/epson/services.yaml @@ -0,0 +1,9 @@ +select_cmode: + description: Select Color mode of Epson projector + fields: + entity_id: + description: Name of projector + example: 'media_player.epson_projector' + cmode: + description: Name of Cmode + example: 'cinema' diff --git a/homeassistant/components/epsonworkforce/sensor.py b/homeassistant/components/epsonworkforce/sensor.py index b310376e5cc..3bb90eb0644 100644 --- a/homeassistant/components/epsonworkforce/sensor.py +++ b/homeassistant/components/epsonworkforce/sensor.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +from epsonprinter_pkg.epsonprinterapi import EpsonPrinterAPI import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -34,8 +35,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the cartridge sensor.""" host = config.get(CONF_HOST) - from epsonprinter_pkg.epsonprinterapi import EpsonPrinterAPI - api = EpsonPrinterAPI(host) if not api.available: raise PlatformNotReady() diff --git a/homeassistant/components/eq3btsmart/climate.py b/homeassistant/components/eq3btsmart/climate.py index 8499a0de5a0..d0b60c74443 100644 --- a/homeassistant/components/eq3btsmart/climate.py +++ b/homeassistant/components/eq3btsmart/climate.py @@ -1,6 +1,8 @@ """Support for eQ-3 Bluetooth Smart thermostats.""" import logging +# pylint: disable=import-error +from bluepy.btle import BTLEException import eq3bt as eq3 # pylint: disable=import-error import voluptuous as vol @@ -11,9 +13,9 @@ from homeassistant.components.climate.const import ( HVAC_MODE_OFF, PRESET_AWAY, PRESET_BOOST, + PRESET_NONE, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, - PRESET_NONE, ) from homeassistant.const import ( ATTR_TEMPERATURE, @@ -190,8 +192,6 @@ class EQ3BTSmartThermostat(ClimateDevice): def update(self): """Update the data from the thermostat.""" - # pylint: disable=import-error,no-name-in-module - from bluepy.btle import BTLEException try: self._thermostat.update() diff --git a/homeassistant/components/esphome/.translations/da.json b/homeassistant/components/esphome/.translations/da.json index ba84ab40301..db4b4362a5e 100644 --- a/homeassistant/components/esphome/.translations/da.json +++ b/homeassistant/components/esphome/.translations/da.json @@ -18,8 +18,8 @@ "title": "Indtast adgangskode" }, "discovery_confirm": { - "description": "Vil du tilf\u00f8je ESPHome node `{name}` til Home Assistant?", - "title": "Fandt ESPHome node" + "description": "Vil du tilf\u00f8je ESPHome-knudepunkt `{name}` til Home Assistant?", + "title": "Fandt ESPHome-knudepunkt" }, "user": { "data": { diff --git a/homeassistant/components/esphome/.translations/ko.json b/homeassistant/components/esphome/.translations/ko.json index b6bcf3cd1b3..4d8068c801b 100644 --- a/homeassistant/components/esphome/.translations/ko.json +++ b/homeassistant/components/esphome/.translations/ko.json @@ -18,8 +18,8 @@ "title": "\ube44\ubc00\ubc88\ud638 \uc785\ub825" }, "discovery_confirm": { - "description": "Home Assistant \uc5d0 ESPHome node `{name}` \uc744(\ub97c) \ucd94\uac00 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", - "title": "\ubc1c\uacac \ub41c ESPHome node" + "description": "Home Assistant \uc5d0 ESPHome node `{name}` \uc744(\ub97c) \ucd94\uac00\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "\ubc1c\uacac\ub41c ESPHome node" }, "user": { "data": { diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 2ad24e6f75e..cabba95ea7e 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -130,7 +130,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool # ESPHome uses servicecall packet for both events and service calls # Ensure the user can only send events of form 'esphome.xyz' if domain != "esphome": - _LOGGER.error("Can only generate events under esphome " "domain!") + _LOGGER.error("Can only generate events under esphome domain!") return hass.bus.async_fire(service.service, service_data) else: diff --git a/homeassistant/components/esphome/binary_sensor.py b/homeassistant/components/esphome/binary_sensor.py index 64506f69283..fe41bb2f7bb 100644 --- a/homeassistant/components/esphome/binary_sensor.py +++ b/homeassistant/components/esphome/binary_sensor.py @@ -1,5 +1,4 @@ """Support for ESPHome binary sensors.""" -import logging from typing import Optional from aioesphomeapi import BinarySensorInfo, BinarySensorState @@ -8,8 +7,6 @@ from homeassistant.components.binary_sensor import BinarySensorDevice from . import EsphomeEntity, platform_async_setup_entry -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry(hass, entry, async_add_entities): """Set up ESPHome binary sensors based on a config entry.""" diff --git a/homeassistant/components/esphome/climate.py b/homeassistant/components/esphome/climate.py index 5fed8da76ef..960366a8332 100644 --- a/homeassistant/components/esphome/climate.py +++ b/homeassistant/components/esphome/climate.py @@ -2,22 +2,52 @@ import logging from typing import List, Optional -from aioesphomeapi import ClimateInfo, ClimateMode, ClimateState +from aioesphomeapi import ( + ClimateAction, + ClimateFanMode, + ClimateInfo, + ClimateMode, + ClimateState, + ClimateSwingMode, +) from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( ATTR_HVAC_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, - HVAC_MODE_HEAT_COOL, + CURRENT_HVAC_COOL, + CURRENT_HVAC_DRY, + CURRENT_HVAC_FAN, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + CURRENT_HVAC_OFF, + FAN_AUTO, + FAN_DIFFUSE, + FAN_FOCUS, + FAN_HIGH, + FAN_LOW, + FAN_MEDIUM, + FAN_MIDDLE, + FAN_OFF, + FAN_ON, HVAC_MODE_COOL, + HVAC_MODE_DRY, + HVAC_MODE_FAN_ONLY, HVAC_MODE_HEAT, - SUPPORT_TARGET_TEMPERATURE, - SUPPORT_PRESET_MODE, - SUPPORT_TARGET_TEMPERATURE_RANGE, - PRESET_AWAY, + HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, + PRESET_AWAY, PRESET_HOME, + SUPPORT_FAN_MODE, + SUPPORT_PRESET_MODE, + SUPPORT_SWING_MODE, + SUPPORT_TARGET_TEMPERATURE, + SUPPORT_TARGET_TEMPERATURE_RANGE, + SWING_BOTH, + SWING_HORIZONTAL, + SWING_OFF, + SWING_VERTICAL, ) from homeassistant.const import ( ATTR_TEMPERATURE, @@ -57,6 +87,45 @@ def _climate_modes(): ClimateMode.AUTO: HVAC_MODE_HEAT_COOL, ClimateMode.COOL: HVAC_MODE_COOL, ClimateMode.HEAT: HVAC_MODE_HEAT, + ClimateMode.FAN_ONLY: HVAC_MODE_FAN_ONLY, + ClimateMode.DRY: HVAC_MODE_DRY, + } + + +@esphome_map_enum +def _climate_actions(): + return { + ClimateAction.OFF: CURRENT_HVAC_OFF, + ClimateAction.COOLING: CURRENT_HVAC_COOL, + ClimateAction.HEATING: CURRENT_HVAC_HEAT, + ClimateAction.IDLE: CURRENT_HVAC_IDLE, + ClimateAction.DRYING: CURRENT_HVAC_DRY, + ClimateAction.FAN: CURRENT_HVAC_FAN, + } + + +@esphome_map_enum +def _fan_modes(): + return { + ClimateFanMode.ON: FAN_ON, + ClimateFanMode.OFF: FAN_OFF, + ClimateFanMode.AUTO: FAN_AUTO, + ClimateFanMode.LOW: FAN_LOW, + ClimateFanMode.MEDIUM: FAN_MEDIUM, + ClimateFanMode.HIGH: FAN_HIGH, + ClimateFanMode.MIDDLE: FAN_MIDDLE, + ClimateFanMode.FOCUS: FAN_FOCUS, + ClimateFanMode.DIFFUSE: FAN_DIFFUSE, + } + + +@esphome_map_enum +def _swing_modes(): + return { + ClimateSwingMode.OFF: SWING_OFF, + ClimateSwingMode.BOTH: SWING_BOTH, + ClimateSwingMode.VERTICAL: SWING_VERTICAL, + ClimateSwingMode.HORIZONTAL: SWING_HORIZONTAL, } @@ -94,11 +163,27 @@ class EsphomeClimateDevice(EsphomeEntity, ClimateDevice): for mode in self._static_info.supported_modes ] + @property + def fan_modes(self): + """Return the list of available fan modes.""" + return [ + _fan_modes.from_esphome(mode) + for mode in self._static_info.supported_fan_modes + ] + @property def preset_modes(self): """Return preset modes.""" return [PRESET_AWAY, PRESET_HOME] if self._static_info.supports_away else [] + @property + def swing_modes(self): + """Return the list of available swing modes.""" + return [ + _swing_modes.from_esphome(mode) + for mode in self._static_info.supported_swing_modes + ] + @property def target_temperature_step(self) -> float: """Return the supported step of target temperature.""" @@ -125,6 +210,10 @@ class EsphomeClimateDevice(EsphomeEntity, ClimateDevice): features |= SUPPORT_TARGET_TEMPERATURE if self._static_info.supports_away: features |= SUPPORT_PRESET_MODE + if self._static_info.supported_fan_modes: + features |= SUPPORT_FAN_MODE + if self._static_info.supported_swing_modes: + features |= SUPPORT_SWING_MODE return features # https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property @@ -135,11 +224,29 @@ class EsphomeClimateDevice(EsphomeEntity, ClimateDevice): """Return current operation ie. heat, cool, idle.""" return _climate_modes.from_esphome(self._state.mode) + @esphome_state_property + def hvac_action(self) -> Optional[str]: + """Return current action.""" + # HA has no support feature field for hvac_action + if not self._static_info.supports_action: + return None + return _climate_actions.from_esphome(self._state.action) + + @esphome_state_property + def fan_mode(self): + """Return current fan setting.""" + return _fan_modes.from_esphome(self._state.fan_mode) + @esphome_state_property def preset_mode(self): """Return current preset mode.""" return PRESET_AWAY if self._state.away else PRESET_HOME + @esphome_state_property + def swing_mode(self): + """Return current swing mode.""" + return _swing_modes.from_esphome(self._state.swing_mode) + @esphome_state_property def current_temperature(self) -> Optional[float]: """Return the current temperature.""" @@ -183,3 +290,15 @@ class EsphomeClimateDevice(EsphomeEntity, ClimateDevice): """Set preset mode.""" away = preset_mode == PRESET_AWAY await self._client.climate_command(key=self._static_info.key, away=away) + + async def async_set_fan_mode(self, fan_mode: str) -> None: + """Set new fan mode.""" + await self._client.climate_command( + key=self._static_info.key, fan_mode=_fan_modes.from_hass(fan_mode) + ) + + async def async_set_swing_mode(self, swing_mode: str) -> None: + """Set new swing mode.""" + await self._client.climate_command( + key=self._static_info.key, swing_mode=_swing_modes.from_hass(swing_mode) + ) diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 47c00f43463..53289799b43 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -2,6 +2,7 @@ from collections import OrderedDict from typing import Optional +from aioesphomeapi import APIClient, APIConnectionError import voluptuous as vol from homeassistant import config_entries @@ -147,8 +148,6 @@ class EsphomeFlowHandler(config_entries.ConfigFlow): async def fetch_device_info(self): """Fetch device info from API and return any errors.""" - from aioesphomeapi import APIClient, APIConnectionError - cli = APIClient(self.hass.loop, self._host, self._port, "") try: @@ -165,8 +164,6 @@ class EsphomeFlowHandler(config_entries.ConfigFlow): async def try_login(self): """Try logging in to device and return any errors.""" - from aioesphomeapi import APIClient, APIConnectionError - cli = APIClient(self.hass.loop, self._host, self._port, self._password) try: diff --git a/homeassistant/components/esphome/cover.py b/homeassistant/components/esphome/cover.py index 980fc936940..53014991de8 100644 --- a/homeassistant/components/esphome/cover.py +++ b/homeassistant/components/esphome/cover.py @@ -2,7 +2,7 @@ import logging from typing import Optional -from aioesphomeapi import CoverInfo, CoverState +from aioesphomeapi import CoverInfo, CoverOperation, CoverState from homeassistant.components.cover import ( ATTR_POSITION, @@ -82,15 +82,11 @@ class EsphomeCover(EsphomeEntity, CoverDevice): @esphome_state_property def is_opening(self) -> bool: """Return if the cover is opening or not.""" - from aioesphomeapi import CoverOperation - return self._state.current_operation == CoverOperation.IS_OPENING @esphome_state_property def is_closing(self) -> bool: """Return if the cover is closing or not.""" - from aioesphomeapi import CoverOperation - return self._state.current_operation == CoverOperation.IS_CLOSING @esphome_state_property diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index d916e1a90c8..48f1aea2c2d 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -1,22 +1,22 @@ """Runtime entry data for ESPHome stored in hass.data.""" import asyncio -from typing import Any, Callable, Dict, List, Optional, Tuple, Set +from typing import Any, Callable, Dict, List, Optional, Set, Tuple from aioesphomeapi import ( COMPONENT_TYPE_TO_INFO, - DeviceInfo, - EntityInfo, - EntityState, - UserService, BinarySensorInfo, CameraInfo, ClimateInfo, CoverInfo, + DeviceInfo, + EntityInfo, + EntityState, FanInfo, LightInfo, SensorInfo, SwitchInfo, TextSensorInfo, + UserService, ) import attr diff --git a/homeassistant/components/esphome/fan.py b/homeassistant/components/esphome/fan.py index cddb75b41bf..8b9b4b4922c 100644 --- a/homeassistant/components/esphome/fan.py +++ b/homeassistant/components/esphome/fan.py @@ -81,7 +81,6 @@ class EsphomeFan(EsphomeEntity, FanEntity): data["speed"] = _fan_speeds.from_hass(speed) await self._client.fan_command(**data) - # pylint: disable=arguments-differ async def async_turn_off(self, **kwargs) -> None: """Turn off the fan.""" await self._client.fan_command(key=self._static_info.key, state=False) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 724946e6984..549a063528f 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", "requirements": [ - "aioesphomeapi==2.5.0" + "aioesphomeapi==2.6.1" ], "dependencies": [], "zeroconf": ["_esphomelib._tcp.local."], diff --git a/homeassistant/components/etherscan/sensor.py b/homeassistant/components/etherscan/sensor.py index 9cabb2762b0..1c14ce578c1 100644 --- a/homeassistant/components/etherscan/sensor.py +++ b/homeassistant/components/etherscan/sensor.py @@ -1,6 +1,7 @@ """Support for Etherscan sensors.""" from datetime import timedelta +from pyetherscan import get_balance import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -75,7 +76,6 @@ class EtherscanSensor(Entity): def update(self): """Get the latest state of the sensor.""" - from pyetherscan import get_balance if self._token_address: self._state = get_balance(self._address, self._token_address) diff --git a/homeassistant/components/eufy/__init__.py b/homeassistant/components/eufy/__init__.py index 191d6ab5315..eca637ec371 100644 --- a/homeassistant/components/eufy/__init__.py +++ b/homeassistant/components/eufy/__init__.py @@ -1,7 +1,7 @@ """Support for Eufy devices.""" import logging -import lakeside +import lakeside import voluptuous as vol from homeassistant.const import ( diff --git a/homeassistant/components/eufy/light.py b/homeassistant/components/eufy/light.py index 21c26606bdd..570f690307f 100644 --- a/homeassistant/components/eufy/light.py +++ b/homeassistant/components/eufy/light.py @@ -1,5 +1,6 @@ """Support for Eufy lights.""" import logging + import lakeside from homeassistant.components.light import ( @@ -7,16 +8,14 @@ from homeassistant.components.light import ( ATTR_COLOR_TEMP, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, - SUPPORT_COLOR_TEMP, SUPPORT_COLOR, + SUPPORT_COLOR_TEMP, Light, ) - import homeassistant.util.color as color_util - from homeassistant.util.color import ( - color_temperature_mired_to_kelvin as mired_to_kelvin, color_temperature_kelvin_to_mired as kelvin_to_mired, + color_temperature_mired_to_kelvin as mired_to_kelvin, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/eufy/switch.py b/homeassistant/components/eufy/switch.py index 2e13886dd2a..cbc09f4101c 100644 --- a/homeassistant/components/eufy/switch.py +++ b/homeassistant/components/eufy/switch.py @@ -1,5 +1,6 @@ """Support for Eufy switches.""" import logging + import lakeside from homeassistant.components.switch import SwitchDevice diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 29f89dc08d6..3d903e86e30 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -237,11 +237,7 @@ class EvoBroker: loc_idx = params[CONF_LOCATION_IDX] self.config = client.installation_info[loc_idx][GWS][0][TCS][0] - self.tcs = ( - client.locations[loc_idx] # pylint: disable=protected-access - ._gateways[0] - ._control_systems[0] - ) + self.tcs = client.locations[loc_idx]._gateways[0]._control_systems[0] self.temps = None async def save_auth_tokens(self) -> None: diff --git a/homeassistant/components/facebook/notify.py b/homeassistant/components/facebook/notify.py index 452b81c0f16..b75f2628033 100644 --- a/homeassistant/components/facebook/notify.py +++ b/homeassistant/components/facebook/notify.py @@ -6,15 +6,14 @@ from aiohttp.hdrs import CONTENT_TYPE import requests import voluptuous as vol -from homeassistant.const import CONTENT_TYPE_JSON -import homeassistant.helpers.config_validation as cv - from homeassistant.components.notify import ( ATTR_DATA, ATTR_TARGET, PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.const import CONTENT_TYPE_JSON +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/facebox/const.py b/homeassistant/components/facebox/const.py new file mode 100644 index 00000000000..991ec925a98 --- /dev/null +++ b/homeassistant/components/facebox/const.py @@ -0,0 +1,4 @@ +"""Constants for the Facebox component.""" + +DOMAIN = "facebox" +SERVICE_TEACH_FACE = "teach_face" diff --git a/homeassistant/components/facebox/image_processing.py b/homeassistant/components/facebox/image_processing.py index 228cae2f19d..ee6e4d8a6fa 100644 --- a/homeassistant/components/facebox/image_processing.py +++ b/homeassistant/components/facebox/image_processing.py @@ -5,27 +5,29 @@ import logging import requests import voluptuous as vol -from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME -from homeassistant.core import split_entity_id -import homeassistant.helpers.config_validation as cv from homeassistant.components.image_processing import ( - PLATFORM_SCHEMA, - ImageProcessingFaceEntity, ATTR_CONFIDENCE, - CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME, - DOMAIN, + CONF_SOURCE, + PLATFORM_SCHEMA, + ImageProcessingFaceEntity, ) from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_NAME, CONF_IP_ADDRESS, - CONF_PORT, CONF_PASSWORD, + CONF_PORT, CONF_USERNAME, HTTP_BAD_REQUEST, HTTP_OK, HTTP_UNAUTHORIZED, ) +from homeassistant.core import split_entity_id +import homeassistant.helpers.config_validation as cv + +from .const import DOMAIN, SERVICE_TEACH_FACE _LOGGER = logging.getLogger(__name__) @@ -38,7 +40,6 @@ FACEBOX_NAME = "name" CLASSIFIER = "facebox" DATA_FACEBOX = "facebox_classifiers" FILE_PATH = "file_path" -SERVICE_TEACH_FACE = "facebox_teach_face" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( diff --git a/homeassistant/components/facebox/services.yaml b/homeassistant/components/facebox/services.yaml index e69de29bb2d..c6b686efb85 100644 --- a/homeassistant/components/facebox/services.yaml +++ b/homeassistant/components/facebox/services.yaml @@ -0,0 +1,12 @@ +teach_face: + description: Teach facebox a face using a file. + fields: + entity_id: + description: The facebox entity to teach. + example: 'image_processing.facebox' + name: + description: The name of the face to teach. + example: 'my_name' + file_path: + description: The path to the image file. + example: '/images/my_image.jpg' diff --git a/homeassistant/components/fail2ban/sensor.py b/homeassistant/components/fail2ban/sensor.py index 2dc528b2cff..692b48d9db5 100644 --- a/homeassistant/components/fail2ban/sensor.py +++ b/homeassistant/components/fail2ban/sensor.py @@ -5,17 +5,16 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.fail2ban/ """ -import os -import logging - from datetime import timedelta - +import logging +import os import re + import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, CONF_FILE_PATH +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__) diff --git a/homeassistant/components/familyhub/camera.py b/homeassistant/components/familyhub/camera.py index 546d95f24d1..2e4e7085927 100644 --- a/homeassistant/components/familyhub/camera.py +++ b/homeassistant/components/familyhub/camera.py @@ -1,9 +1,10 @@ """Family Hub camera for Samsung Refrigerators.""" import logging +from pyfamilyhublocal import FamilyHubCam import voluptuous as vol -from homeassistant.components.camera import Camera, PLATFORM_SCHEMA +from homeassistant.components.camera import PLATFORM_SCHEMA, Camera from homeassistant.const import CONF_IP_ADDRESS, CONF_NAME from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -22,7 +23,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Family Hub Camera.""" - from pyfamilyhublocal import FamilyHubCam address = config.get(CONF_IP_ADDRESS) name = config.get(CONF_NAME) diff --git a/homeassistant/components/fan/.translations/bg.json b/homeassistant/components/fan/.translations/bg.json new file mode 100644 index 00000000000..f678c870968 --- /dev/null +++ b/homeassistant/components/fan/.translations/bg.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0438 {entity_name}", + "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438 {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} \u0435 \u0438\u0437\u043a\u043b\u044e\u0447\u0435\u043d", + "is_on": "{entity_name} \u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d" + }, + "trigger_type": { + "turned_off": "{entity_name} \u0431\u044a\u0434\u0435 \u0438\u0437\u043a\u043b\u044e\u0447\u0435\u043d", + "turned_on": "{entity_name} \u0431\u044a\u0434\u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fan/.translations/ca.json b/homeassistant/components/fan/.translations/ca.json index 0530ccf5a85..e2f3ce2b0a4 100644 --- a/homeassistant/components/fan/.translations/ca.json +++ b/homeassistant/components/fan/.translations/ca.json @@ -4,7 +4,7 @@ "turn_off": "Apaga {entity_name}", "turn_on": "Enc\u00e9n {entity_name}" }, - "condtion_type": { + "condition_type": { "is_off": "{entity_name} est\u00e0 apagat", "is_on": "{entity_name} est\u00e0 enc\u00e8s" }, diff --git a/homeassistant/components/fan/.translations/da.json b/homeassistant/components/fan/.translations/da.json new file mode 100644 index 00000000000..0c9556bfedb --- /dev/null +++ b/homeassistant/components/fan/.translations/da.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "Sluk {entity_name}", + "turn_on": "T\u00e6nd for {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} er slukket", + "is_on": "{entity_name} er t\u00e6ndt" + }, + "trigger_type": { + "turned_off": "{entity_name} blev slukket", + "turned_on": "{entity_name} blev t\u00e6ndt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fan/.translations/de.json b/homeassistant/components/fan/.translations/de.json index 9ac3d999370..9c3559b7cfc 100644 --- a/homeassistant/components/fan/.translations/de.json +++ b/homeassistant/components/fan/.translations/de.json @@ -4,7 +4,7 @@ "turn_off": "Schalte {entity_name} aus.", "turn_on": "Schalte {entity_name} ein." }, - "condtion_type": { + "condition_type": { "is_off": "{entity_name} ist ausgeschaltet", "is_on": "{entity_name} ist eingeschaltet" }, diff --git a/homeassistant/components/fan/.translations/en.json b/homeassistant/components/fan/.translations/en.json index b085e7baa45..c27d983ca2e 100644 --- a/homeassistant/components/fan/.translations/en.json +++ b/homeassistant/components/fan/.translations/en.json @@ -4,7 +4,7 @@ "turn_off": "Turn off {entity_name}", "turn_on": "Turn on {entity_name}" }, - "condtion_type": { + "condition_type": { "is_off": "{entity_name} is off", "is_on": "{entity_name} is on" }, diff --git a/homeassistant/components/fan/.translations/es.json b/homeassistant/components/fan/.translations/es.json index d92153a6302..4ceefe9c721 100644 --- a/homeassistant/components/fan/.translations/es.json +++ b/homeassistant/components/fan/.translations/es.json @@ -4,7 +4,7 @@ "turn_off": "Desactivar {entity_name}", "turn_on": "Activar {entity_name}" }, - "condtion_type": { + "condition_type": { "is_off": "{entity_name} est\u00e1 desactivado", "is_on": "{entity_name} est\u00e1 activado" }, diff --git a/homeassistant/components/fan/.translations/fr.json b/homeassistant/components/fan/.translations/fr.json index 5c5a65b6bcd..e6944dab781 100644 --- a/homeassistant/components/fan/.translations/fr.json +++ b/homeassistant/components/fan/.translations/fr.json @@ -4,7 +4,7 @@ "turn_off": "\u00c9teindre {entity_name}", "turn_on": "Allumer {entity_name}" }, - "condtion_type": { + "condition_type": { "is_off": "{entity_name} est d\u00e9sactiv\u00e9", "is_on": "{entity_name} est activ\u00e9" }, diff --git a/homeassistant/components/fan/.translations/hu.json b/homeassistant/components/fan/.translations/hu.json new file mode 100644 index 00000000000..b559f29c581 --- /dev/null +++ b/homeassistant/components/fan/.translations/hu.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "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" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fan/.translations/it.json b/homeassistant/components/fan/.translations/it.json index b62d80c793b..4fab847f1cb 100644 --- a/homeassistant/components/fan/.translations/it.json +++ b/homeassistant/components/fan/.translations/it.json @@ -4,7 +4,7 @@ "turn_off": "Spegnere {entity_name}", "turn_on": "Accendere {entity_name}" }, - "condtion_type": { + "condition_type": { "is_off": "{entity_name} \u00e8 spento", "is_on": "{entity_name} \u00e8 acceso" }, diff --git a/homeassistant/components/fan/.translations/ko.json b/homeassistant/components/fan/.translations/ko.json new file mode 100644 index 00000000000..dec2a711e57 --- /dev/null +++ b/homeassistant/components/fan/.translations/ko.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "{entity_name} \ub044\uae30", + "turn_on": "{entity_name} \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" + }, + "trigger_type": { + "turned_off": "{entity_name} \uc774(\uac00) \uaebc\uc9c8 \ub54c", + "turned_on": "{entity_name} \uc774(\uac00) \ucf1c\uc9c8 \ub54c" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fan/.translations/lb.json b/homeassistant/components/fan/.translations/lb.json index 316a77d471d..f5170949bad 100644 --- a/homeassistant/components/fan/.translations/lb.json +++ b/homeassistant/components/fan/.translations/lb.json @@ -4,7 +4,7 @@ "turn_off": "{entity_name} ausschalten", "turn_on": "{entity_name} uschalten" }, - "condtion_type": { + "condition_type": { "is_off": "{entity_name} ass aus", "is_on": "{entity_name} ass un" }, diff --git a/homeassistant/components/fan/.translations/nl.json b/homeassistant/components/fan/.translations/nl.json new file mode 100644 index 00000000000..4837b301ea7 --- /dev/null +++ b/homeassistant/components/fan/.translations/nl.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "Schakel {entity_name} uit", + "turn_on": "Schakel {entity_name} in" + }, + "condition_type": { + "is_off": "{entity_name} is uitgeschakeld", + "is_on": "{entity_name} is ingeschakeld" + }, + "trigger_type": { + "turned_off": "{entity_name} uitgeschakeld", + "turned_on": "{entity_name} ingeschakeld" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fan/.translations/no.json b/homeassistant/components/fan/.translations/no.json index 73917ac45c4..aa6320f0a65 100644 --- a/homeassistant/components/fan/.translations/no.json +++ b/homeassistant/components/fan/.translations/no.json @@ -4,7 +4,7 @@ "turn_off": "Sl\u00e5 av {entity_name}", "turn_on": "Sl\u00e5 p\u00e5 {entity_name}" }, - "condtion_type": { + "condition_type": { "is_off": "{entity_name} er av", "is_on": "{entity_name} er p\u00e5" }, diff --git a/homeassistant/components/fan/.translations/pl.json b/homeassistant/components/fan/.translations/pl.json index 424794a5b64..709a63c2389 100644 --- a/homeassistant/components/fan/.translations/pl.json +++ b/homeassistant/components/fan/.translations/pl.json @@ -4,7 +4,7 @@ "turn_off": "wy\u0142\u0105cz {entity_name}", "turn_on": "w\u0142\u0105cz {entity_name}" }, - "condtion_type": { + "condition_type": { "is_off": "wentylator (entity_name} jest wy\u0142\u0105czony", "is_on": "wentylator (entity_name} jest w\u0142\u0105czony" }, diff --git a/homeassistant/components/fan/.translations/pt-BR.json b/homeassistant/components/fan/.translations/pt-BR.json new file mode 100644 index 00000000000..6b95464bdbc --- /dev/null +++ b/homeassistant/components/fan/.translations/pt-BR.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condition_type": { + "is_off": "{entity_name} est\u00e1 desligado", + "is_on": "{entity_name} est\u00e1 ligado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fan/.translations/pt.json b/homeassistant/components/fan/.translations/pt.json index a76550cbedd..ab78bc776bd 100644 --- a/homeassistant/components/fan/.translations/pt.json +++ b/homeassistant/components/fan/.translations/pt.json @@ -4,7 +4,7 @@ "turn_off": "Desligar {entity_name}", "turn_on": "Ligar {entity_name}" }, - "condtion_type": { + "condition_type": { "is_off": "{entity_name} est\u00e1 desligada", "is_on": "{entity_name} est\u00e1 ligada" }, diff --git a/homeassistant/components/fan/.translations/ru.json b/homeassistant/components/fan/.translations/ru.json index 4fd5ebe28c5..157c78975cb 100644 --- a/homeassistant/components/fan/.translations/ru.json +++ b/homeassistant/components/fan/.translations/ru.json @@ -4,7 +4,7 @@ "turn_off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0438\u0442\u044c {entity_name}", "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c {entity_name}" }, - "condtion_type": { + "condition_type": { "is_off": "{entity_name} \u0432 \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", "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" }, diff --git a/homeassistant/components/fan/.translations/sl.json b/homeassistant/components/fan/.translations/sl.json index a5de109f764..a2bca3352ab 100644 --- a/homeassistant/components/fan/.translations/sl.json +++ b/homeassistant/components/fan/.translations/sl.json @@ -4,7 +4,7 @@ "turn_off": "Izklopite {entity_name}", "turn_on": "Vklopite {entity_name}" }, - "condtion_type": { + "condition_type": { "is_off": "{entity_name} je izklopljen", "is_on": "{entity_name} je vklopljen" }, diff --git a/homeassistant/components/fan/.translations/zh-Hant.json b/homeassistant/components/fan/.translations/zh-Hant.json index 4b34f6e0165..78c0d991125 100644 --- a/homeassistant/components/fan/.translations/zh-Hant.json +++ b/homeassistant/components/fan/.translations/zh-Hant.json @@ -4,7 +4,7 @@ "turn_off": "\u95dc\u9589 {entity_name}", "turn_on": "\u958b\u555f {entity_name}" }, - "condtion_type": { + "condition_type": { "is_off": "{entity_name} \u95dc\u9589", "is_on": "{entity_name} \u958b\u555f" }, diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index 4c6cee2927c..c3111a3aeb7 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -7,16 +7,15 @@ from typing import Optional import voluptuous as vol from homeassistant.components import group -from homeassistant.const import SERVICE_TURN_ON, SERVICE_TOGGLE, SERVICE_TURN_OFF -from homeassistant.loader import bind_hass -from homeassistant.helpers.entity import ToggleEntity -from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.const import SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa: F401 - ENTITY_SERVICE_SCHEMA, PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import ToggleEntity +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.loader import bind_hass _LOGGER = logging.getLogger(__name__) @@ -57,20 +56,6 @@ PROP_TO_ATTR = { "current_direction": ATTR_DIRECTION, } -FAN_SET_SPEED_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_SPEED): cv.string} -) - -FAN_TURN_ON_SCHEMA = ENTITY_SERVICE_SCHEMA.extend({vol.Optional(ATTR_SPEED): cv.string}) - -FAN_OSCILLATE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_OSCILLATING): cv.boolean} -) - -FAN_SET_DIRECTION_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Optional(ATTR_DIRECTION): cv.string} -) - @bind_hass def is_on(hass, entity_id: Optional[str] = None) -> bool: @@ -89,22 +74,22 @@ async def async_setup(hass, config: dict): await component.async_setup(config) component.async_register_entity_service( - SERVICE_TURN_ON, FAN_TURN_ON_SCHEMA, "async_turn_on" + SERVICE_TURN_ON, {vol.Optional(ATTR_SPEED): cv.string}, "async_turn_on" + ) + component.async_register_entity_service(SERVICE_TURN_OFF, {}, "async_turn_off") + component.async_register_entity_service(SERVICE_TOGGLE, {}, "async_toggle") + component.async_register_entity_service( + SERVICE_SET_SPEED, {vol.Required(ATTR_SPEED): cv.string}, "async_set_speed" ) component.async_register_entity_service( - SERVICE_TURN_OFF, ENTITY_SERVICE_SCHEMA, "async_turn_off" + SERVICE_OSCILLATE, + {vol.Required(ATTR_OSCILLATING): cv.boolean}, + "async_oscillate", ) component.async_register_entity_service( - SERVICE_TOGGLE, ENTITY_SERVICE_SCHEMA, "async_toggle" - ) - component.async_register_entity_service( - SERVICE_SET_SPEED, FAN_SET_SPEED_SCHEMA, "async_set_speed" - ) - component.async_register_entity_service( - SERVICE_OSCILLATE, FAN_OSCILLATE_SCHEMA, "async_oscillate" - ) - component.async_register_entity_service( - SERVICE_SET_DIRECTION, FAN_SET_DIRECTION_SCHEMA, "async_set_direction" + SERVICE_SET_DIRECTION, + {vol.Optional(ATTR_DIRECTION): cv.string}, + "async_set_direction", ) return True diff --git a/homeassistant/components/fan/device_action.py b/homeassistant/components/fan/device_action.py index b26f632a775..a5d35d741b6 100644 --- a/homeassistant/components/fan/device_action.py +++ b/homeassistant/components/fan/device_action.py @@ -1,19 +1,21 @@ """Provides device automations for Fan.""" -from typing import Optional, List +from typing import List, Optional + import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, - CONF_DOMAIN, - CONF_TYPE, CONF_DEVICE_ID, + CONF_DOMAIN, CONF_ENTITY_ID, - SERVICE_TURN_ON, + CONF_TYPE, SERVICE_TURN_OFF, + SERVICE_TURN_ON, ) -from homeassistant.core import HomeAssistant, Context +from homeassistant.core import Context, HomeAssistant from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv + from . import DOMAIN ACTION_TYPES = {"turn_on", "turn_off"} diff --git a/homeassistant/components/fan/device_condition.py b/homeassistant/components/fan/device_condition.py index 8b567fcd4c9..c69f28c10e9 100644 --- a/homeassistant/components/fan/device_condition.py +++ b/homeassistant/components/fan/device_condition.py @@ -1,21 +1,23 @@ """Provide the device automations for Fan.""" from typing import Dict, List + import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, CONF_CONDITION, - CONF_DOMAIN, - CONF_TYPE, CONF_DEVICE_ID, + CONF_DOMAIN, CONF_ENTITY_ID, + CONF_TYPE, STATE_OFF, STATE_ON, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import condition, config_validation as cv, entity_registry -from homeassistant.helpers.typing import ConfigType, TemplateVarsType from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA +from homeassistant.helpers.typing import ConfigType, TemplateVarsType + from . import DOMAIN CONDITION_TYPES = {"is_on", "is_off"} diff --git a/homeassistant/components/fan/device_trigger.py b/homeassistant/components/fan/device_trigger.py index 3e917e0ae79..3bfeb5ee36b 100644 --- a/homeassistant/components/fan/device_trigger.py +++ b/homeassistant/components/fan/device_trigger.py @@ -1,21 +1,23 @@ """Provides device automations for Fan.""" from typing import List + import voluptuous as vol +from homeassistant.components.automation import AutomationActionType, state +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA from homeassistant.const import ( - CONF_DOMAIN, - CONF_TYPE, - CONF_PLATFORM, CONF_DEVICE_ID, + CONF_DOMAIN, CONF_ENTITY_ID, - STATE_ON, + CONF_PLATFORM, + CONF_TYPE, STATE_OFF, + STATE_ON, ) -from homeassistant.core import HomeAssistant, CALLBACK_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 homeassistant.components.automation import state, AutomationActionType -from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA + from . import DOMAIN TRIGGER_TYPES = {"turned_on", "turned_off"} diff --git a/homeassistant/components/fan/reproduce_state.py b/homeassistant/components/fan/reproduce_state.py index 1053861e2bf..2692ac7ee5c 100644 --- a/homeassistant/components/fan/reproduce_state.py +++ b/homeassistant/components/fan/reproduce_state.py @@ -6,19 +6,19 @@ from typing import Iterable, Optional from homeassistant.const import ( ATTR_ENTITY_ID, - STATE_ON, - STATE_OFF, SERVICE_TURN_OFF, SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, ) from homeassistant.core import Context, State from homeassistant.helpers.typing import HomeAssistantType from . import ( - DOMAIN, ATTR_DIRECTION, ATTR_OSCILLATING, ATTR_SPEED, + DOMAIN, SERVICE_OSCILLATE, SERVICE_SET_DIRECTION, SERVICE_SET_SPEED, diff --git a/homeassistant/components/fan/services.yaml b/homeassistant/components/fan/services.yaml index 0e3978690e6..ee478950095 100644 --- a/homeassistant/components/fan/services.yaml +++ b/homeassistant/components/fan/services.yaml @@ -53,161 +53,3 @@ set_direction: direction: description: The direction to rotate. Either 'forward' or 'reverse' example: 'forward' - -xiaomi_miio_set_buzzer_on: - description: Turn the buzzer on. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'fan.xiaomi_miio_device' - -xiaomi_miio_set_buzzer_off: - description: Turn the buzzer off. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'fan.xiaomi_miio_device' - -xiaomi_miio_set_led_on: - description: Turn the led on. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'fan.xiaomi_miio_device' - -xiaomi_miio_set_led_off: - description: Turn the led off. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'fan.xiaomi_miio_device' - -xiaomi_miio_set_child_lock_on: - description: Turn the child lock on. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'fan.xiaomi_miio_device' - -xiaomi_miio_set_child_lock_off: - description: Turn the child lock off. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'fan.xiaomi_miio_device' - -xiaomi_miio_set_favorite_level: - description: Set the favorite level. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'fan.xiaomi_miio_device' - level: - description: Level, between 0 and 16. - example: 1 - -xiaomi_miio_set_led_brightness: - description: Set the led brightness. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'fan.xiaomi_miio_device' - brightness: - description: Brightness (0 = Bright, 1 = Dim, 2 = Off) - example: 1 - -xiaomi_miio_set_auto_detect_on: - description: Turn the auto detect on. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'fan.xiaomi_miio_device' - -xiaomi_miio_set_auto_detect_off: - description: Turn the auto detect off. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'fan.xiaomi_miio_device' - -xiaomi_miio_set_learn_mode_on: - description: Turn the learn mode on. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'fan.xiaomi_miio_device' - -xiaomi_miio_set_learn_mode_off: - description: Turn the learn mode off. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'fan.xiaomi_miio_device' - -xiaomi_miio_set_volume: - description: Set the sound volume. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'fan.xiaomi_miio_device' - volume: - description: Volume, between 0 and 100. - example: 50 - -xiaomi_miio_reset_filter: - description: Reset the filter lifetime and usage. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'fan.xiaomi_miio_device' - -xiaomi_miio_set_extra_features: - description: Manipulates a storage register which advertises extra features. The Mi Home app evaluates the value. A feature called "turbo mode" is unlocked in the app on value 1. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'fan.xiaomi_miio_device' - features: - description: Integer, known values are 0 (default) and 1 (turbo mode). - example: 1 - -xiaomi_miio_set_target_humidity: - description: Set the target humidity. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'fan.xiaomi_miio_device' - humidity: - description: Target humidity. Allowed values are 30, 40, 50, 60, 70 and 80. - example: 50 - -xiaomi_miio_set_dry_on: - description: Turn the dry mode on. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'fan.xiaomi_miio_device' - -xiaomi_miio_set_dry_off: - description: Turn the dry mode off. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'fan.xiaomi_miio_device' - -wemo_set_humidity: - description: Set the target humidity of WeMo humidifier devices. - fields: - entity_id: - description: Names of the WeMo humidifier entities (1 or more entity_ids are required). - example: 'fan.wemo_humidifier' - target_humidity: - description: Target humidity. This is a float value between 0 and 100, but will be mapped to the humidity levels that WeMo humidifiers support (45, 50, 55, 60, and 100/Max) by rounding the value down to the nearest supported value. - example: 56.5 - -wemo_reset_filter_life: - description: Reset the WeMo Humidifier's filter life to 100%. - fields: - entity_id: - description: Names of the WeMo humidifier entities (1 or more entity_ids are required). - example: 'fan.wemo_humidifier' diff --git a/homeassistant/components/fan/strings.json b/homeassistant/components/fan/strings.json index 134119f41ff..98c3012c123 100644 --- a/homeassistant/components/fan/strings.json +++ b/homeassistant/components/fan/strings.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_on": "{entity_name} is on", "is_off": "{entity_name} is off" }, diff --git a/homeassistant/components/fastdotcom/__init__.py b/homeassistant/components/fastdotcom/__init__.py index b070eef0310..e0a4782493e 100644 --- a/homeassistant/components/fastdotcom/__init__.py +++ b/homeassistant/components/fastdotcom/__init__.py @@ -1,11 +1,12 @@ """Support for testing internet speed via Fast.com.""" -import logging from datetime import timedelta +import logging +from fastdotcom import fast_com import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_SCAN_INTERVAL +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.event import async_track_time_interval @@ -63,7 +64,6 @@ class SpeedtestData: def update(self, now=None): """Get the latest data from fast.com.""" - from fastdotcom import fast_com _LOGGER.debug("Executing fast.com speedtest") self.data = {"download": fast_com()} diff --git a/homeassistant/components/fastdotcom/manifest.json b/homeassistant/components/fastdotcom/manifest.json index 3655ce22ba7..2e47248d778 100644 --- a/homeassistant/components/fastdotcom/manifest.json +++ b/homeassistant/components/fastdotcom/manifest.json @@ -6,5 +6,7 @@ "fastdotcom==0.0.3" ], "dependencies": [], - "codeowners": [] + "codeowners": [ + "@rohankapoorcom" + ] } diff --git a/homeassistant/components/feedreader/__init__.py b/homeassistant/components/feedreader/__init__.py index 27b164e4edf..2643607c3a8 100644 --- a/homeassistant/components/feedreader/__init__.py +++ b/homeassistant/components/feedreader/__init__.py @@ -2,15 +2,15 @@ from datetime import datetime, timedelta from logging import getLogger from os.path import exists -from threading import Lock import pickle +from threading import Lock -import voluptuous as vol import feedparser +import voluptuous as vol -from homeassistant.const import EVENT_HOMEASSISTANT_START, CONF_SCAN_INTERVAL -from homeassistant.helpers.event import track_time_interval +from homeassistant.const import CONF_SCAN_INTERVAL, EVENT_HOMEASSISTANT_START import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import track_time_interval _LOGGER = getLogger(__name__) @@ -131,7 +131,7 @@ class FeedManager: """Filter the entries provided and return the ones to keep.""" if len(self._feed.entries) > self._max_entries: _LOGGER.debug( - "Processing only the first %s entries " "in feed %s", + "Processing only the first %s entries in feed %s", self._max_entries, self._url, ) diff --git a/homeassistant/components/ffmpeg/__init__.py b/homeassistant/components/ffmpeg/__init__.py index 673a34230fc..bc402b46fb2 100644 --- a/homeassistant/components/ffmpeg/__init__.py +++ b/homeassistant/components/ffmpeg/__init__.py @@ -2,20 +2,20 @@ import logging import re -import voluptuous as vol from haffmpeg.tools import FFVersion +import voluptuous as vol -from homeassistant.core import callback from homeassistant.const import ( ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.helpers.dispatcher import ( - async_dispatcher_send, - async_dispatcher_connect, -) +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.entity import Entity DOMAIN = "ffmpeg" diff --git a/homeassistant/components/ffmpeg/camera.py b/homeassistant/components/ffmpeg/camera.py index 0f500176933..db3eb5621ff 100644 --- a/homeassistant/components/ffmpeg/camera.py +++ b/homeassistant/components/ffmpeg/camera.py @@ -2,11 +2,11 @@ import asyncio import logging -import voluptuous as vol from haffmpeg.camera import CameraMjpeg -from haffmpeg.tools import ImageFrame, IMAGE_JPEG +from haffmpeg.tools import IMAGE_JPEG, ImageFrame +import voluptuous as vol -from homeassistant.components.camera import PLATFORM_SCHEMA, Camera, SUPPORT_STREAM +from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_STREAM, Camera from homeassistant.const import CONF_NAME from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/ffmpeg_motion/binary_sensor.py b/homeassistant/components/ffmpeg_motion/binary_sensor.py index 235a9e4b009..294fcc2518f 100644 --- a/homeassistant/components/ffmpeg_motion/binary_sensor.py +++ b/homeassistant/components/ffmpeg_motion/binary_sensor.py @@ -1,19 +1,20 @@ """Provides a binary sensor which is a collection of ffmpeg tools.""" import logging +import haffmpeg.sensor as ffmpeg_sensor import voluptuous as vol -from homeassistant.core import callback -import homeassistant.helpers.config_validation as cv -from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice from homeassistant.components.ffmpeg import ( - FFmpegBase, - DATA_FFMPEG, - CONF_INPUT, CONF_EXTRA_ARGUMENTS, CONF_INITIAL_STATE, + CONF_INPUT, + DATA_FFMPEG, + FFmpegBase, ) from homeassistant.const import CONF_NAME +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -87,10 +88,11 @@ class FFmpegMotion(FFmpegBinarySensor): def __init__(self, hass, manager, config): """Initialize FFmpeg motion binary sensor.""" - from haffmpeg.sensor import SensorMotion super().__init__(config) - self.ffmpeg = SensorMotion(manager.binary, hass.loop, self._async_callback) + self.ffmpeg = ffmpeg_sensor.SensorMotion( + manager.binary, hass.loop, self._async_callback + ) async def _async_start_ffmpeg(self, entity_ids): """Start a FFmpeg instance. diff --git a/homeassistant/components/ffmpeg_noise/binary_sensor.py b/homeassistant/components/ffmpeg_noise/binary_sensor.py index 00e5dbb682f..6ada2bb2748 100644 --- a/homeassistant/components/ffmpeg_noise/binary_sensor.py +++ b/homeassistant/components/ffmpeg_noise/binary_sensor.py @@ -1,19 +1,20 @@ """Provides a binary sensor which is a collection of ffmpeg tools.""" import logging +import haffmpeg.sensor as ffmpeg_sensor import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.binary_sensor import PLATFORM_SCHEMA -from homeassistant.components.ffmpeg_motion.binary_sensor import FFmpegBinarySensor from homeassistant.components.ffmpeg import ( - DATA_FFMPEG, - CONF_INPUT, - CONF_OUTPUT, CONF_EXTRA_ARGUMENTS, CONF_INITIAL_STATE, + CONF_INPUT, + CONF_OUTPUT, + DATA_FFMPEG, ) +from homeassistant.components.ffmpeg_motion.binary_sensor import FFmpegBinarySensor from homeassistant.const import CONF_NAME +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -54,10 +55,11 @@ class FFmpegNoise(FFmpegBinarySensor): def __init__(self, hass, manager, config): """Initialize FFmpeg noise binary sensor.""" - from haffmpeg.sensor import SensorNoise super().__init__(config) - self.ffmpeg = SensorNoise(manager.binary, hass.loop, self._async_callback) + self.ffmpeg = ffmpeg_sensor.SensorNoise( + manager.binary, hass.loop, self._async_callback + ) async def _async_start_ffmpeg(self, entity_ids): """Start a FFmpeg instance. diff --git a/homeassistant/components/fibaro/__init__.py b/homeassistant/components/fibaro/__init__.py index f500b386643..aeb7c0879e0 100644 --- a/homeassistant/components/fibaro/__init__.py +++ b/homeassistant/components/fibaro/__init__.py @@ -1,7 +1,9 @@ """Support for the Fibaro devices.""" -import logging from collections import defaultdict +import logging from typing import Optional + +from fiblary3.client.v4.client import Client as FibaroClient, StateHandler import voluptuous as vol from homeassistant.const import ( @@ -109,7 +111,6 @@ class FibaroController: def __init__(self, config): """Initialize the Fibaro controller.""" - from fiblary3.client.v4.client import Client as FibaroClient self._client = FibaroClient( config[CONF_URL], config[CONF_USERNAME], config[CONF_PASSWORD] @@ -133,11 +134,11 @@ class FibaroController: info = self._client.info.get() self.hub_serial = slugify(info.serialNumber) except AssertionError: - _LOGGER.error("Can't connect to Fibaro HC. " "Please check URL.") + _LOGGER.error("Can't connect to Fibaro HC. Please check URL.") return False if login is None or login.status is False: _LOGGER.error( - "Invalid login for Fibaro HC. " "Please check username and password" + "Invalid login for Fibaro HC. Please check username and password" ) return False @@ -148,8 +149,6 @@ class FibaroController: def enable_state_handler(self): """Start StateHandler thread for monitoring updates.""" - from fiblary3.client.v4.client import StateHandler - self._state_handler = StateHandler(self._client, self._on_state_change) def disable_state_handler(self): @@ -381,7 +380,7 @@ class FibaroDevice(Entity): def dont_know_message(self, action): """Make a warning in case we don't know how to perform an action.""" _LOGGER.warning( - "Not sure how to setValue: %s " "(available actions: %s)", + "Not sure how to setValue: %s (available actions: %s)", str(self.ha_id), str(self.fibaro_device.actions), ) diff --git a/homeassistant/components/fido/sensor.py b/homeassistant/components/fido/sensor.py index 8814a2406c5..086ae87a529 100644 --- a/homeassistant/components/fido/sensor.py +++ b/homeassistant/components/fido/sensor.py @@ -7,21 +7,23 @@ https://www.fido.ca/pages/#/my-account/wireless For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.fido/ """ -import logging from datetime import timedelta +import logging +from pyfido import FidoClient +from pyfido.client import PyFidoError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_USERNAME, - CONF_PASSWORD, - CONF_NAME, CONF_MONITORED_VARIABLES, + CONF_NAME, + CONF_PASSWORD, + CONF_USERNAME, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -147,7 +149,6 @@ class FidoData: def __init__(self, username, password, httpsession): """Initialize the data object.""" - from pyfido import FidoClient self.client = FidoClient(username, password, REQUESTS_TIMEOUT, httpsession) self.data = {} @@ -155,7 +156,6 @@ class FidoData: @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self): """Get the latest data from Fido.""" - from pyfido.client import PyFidoError try: await self.client.fetch_data() diff --git a/homeassistant/components/file/notify.py b/homeassistant/components/file/notify.py index b190bf5d121..4cd83e64a83 100644 --- a/homeassistant/components/file/notify.py +++ b/homeassistant/components/file/notify.py @@ -4,16 +4,15 @@ import os import voluptuous as vol -from homeassistant.const import CONF_FILENAME -import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt_util - from homeassistant.components.notify import ( ATTR_TITLE, ATTR_TITLE_DEFAULT, PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.const import CONF_FILENAME +import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util CONF_TIMESTAMP = "timestamp" diff --git a/homeassistant/components/file/sensor.py b/homeassistant/components/file/sensor.py index 60f04b18f24..96ae885ca77 100644 --- a/homeassistant/components/file/sensor.py +++ b/homeassistant/components/file/sensor.py @@ -1,12 +1,12 @@ """Support for sensor value(s) stored in local files.""" -import os import logging +import os import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_VALUE_TEMPLATE, CONF_NAME, CONF_UNIT_OF_MEASUREMENT +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__) diff --git a/homeassistant/components/filesize/sensor.py b/homeassistant/components/filesize/sensor.py index af9375aad05..8c6cd30b118 100644 --- a/homeassistant/components/filesize/sensor.py +++ b/homeassistant/components/filesize/sensor.py @@ -5,9 +5,9 @@ import os import voluptuous as vol -from homeassistant.helpers.entity import Entity -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/filter/manifest.json b/homeassistant/components/filter/manifest.json index f28007ba552..f84a8b5192f 100644 --- a/homeassistant/components/filter/manifest.json +++ b/homeassistant/components/filter/manifest.json @@ -3,8 +3,6 @@ "name": "Filter", "documentation": "https://www.home-assistant.io/integrations/filter", "requirements": [], - "dependencies": [], - "codeowners": [ - "@dgomes" - ] + "dependencies": ["history"], + "codeowners": ["@dgomes"] } diff --git a/homeassistant/components/filter/sensor.py b/homeassistant/components/filter/sensor.py index 81c4623c53f..eeb0d32f51c 100644 --- a/homeassistant/components/filter/sensor.py +++ b/homeassistant/components/filter/sensor.py @@ -1,31 +1,31 @@ """Allows the creation of a sensor that filters state property.""" -import logging -import statistics -from collections import deque, Counter -from numbers import Number -from functools import partial +from collections import Counter, deque from copy import copy from datetime import timedelta +from functools import partial +import logging +from numbers import Number +import statistics from typing import Optional import voluptuous as vol -from homeassistant.core import callback +from homeassistant.components import history from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, - CONF_ENTITY_ID, - ATTR_UNIT_OF_MEASUREMENT, ATTR_ENTITY_ID, ATTR_ICON, - STATE_UNKNOWN, + ATTR_UNIT_OF_MEASUREMENT, + CONF_ENTITY_ID, + CONF_NAME, STATE_UNAVAILABLE, + STATE_UNKNOWN, ) +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.util.decorator import Registry from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_state_change -from homeassistant.components import history +from homeassistant.util.decorator import Registry import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/fints/sensor.py b/homeassistant/components/fints/sensor.py index 376ea2c0f9d..d81f353c222 100644 --- a/homeassistant/components/fints/sensor.py +++ b/homeassistant/components/fints/sensor.py @@ -3,10 +3,13 @@ from collections import namedtuple from datetime import timedelta import logging + +from fints.client import FinTS3PinTanClient +from fints.dialog import FinTSDialogError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_USERNAME, CONF_PIN, CONF_URL, CONF_NAME +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 @@ -118,7 +121,6 @@ class FinTsClient: the client objects. If that ever changes, consider caching the client object and also think about potential concurrency problems. """ - from fints.client import FinTS3PinTanClient return FinTS3PinTanClient( self._credentials.blz, @@ -129,7 +131,6 @@ class FinTsClient: def detect_accounts(self): """Identify the accounts of the bank.""" - from fints.dialog import FinTSDialogError balance_accounts = [] holdings_accounts = [] diff --git a/homeassistant/components/fitbit/sensor.py b/homeassistant/components/fitbit/sensor.py index 0d4b8d61e7a..5ddb63ef899 100644 --- a/homeassistant/components/fitbit/sensor.py +++ b/homeassistant/components/fitbit/sensor.py @@ -1,7 +1,7 @@ """Support for the Fitbit API.""" -import os -import logging import datetime +import logging +import os import time from fitbit import Fitbit @@ -9,17 +9,15 @@ from fitbit.api import FitbitOauth2Client from oauthlib.oauth2.rfc6749.errors import MismatchingStateError, MissingTokenError import voluptuous as vol -from homeassistant.core import callback from homeassistant.components.http import HomeAssistantView from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ATTR_ATTRIBUTION -from homeassistant.const import CONF_UNIT_SYSTEM +from homeassistant.const import ATTR_ATTRIBUTION, CONF_UNIT_SYSTEM +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 -import homeassistant.helpers.config_validation as cv from homeassistant.util.json import load_json, save_json - _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/fixer/sensor.py b/homeassistant/components/fixer/sensor.py index a97f77138db..e3dfd432a41 100644 --- a/homeassistant/components/fixer/sensor.py +++ b/homeassistant/components/fixer/sensor.py @@ -2,6 +2,8 @@ from datetime import timedelta import logging +from fixerio import Fixerio +from fixerio.exceptions import FixerioException import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -35,7 +37,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Fixer.io sensor.""" - from fixerio import Fixerio, exceptions api_key = config.get(CONF_API_KEY) name = config.get(CONF_NAME) @@ -43,7 +44,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): try: Fixerio(symbols=[target], access_key=api_key).latest() - except exceptions.FixerioException: + except FixerioException: _LOGGER.error("One of the given currencies is not supported") return @@ -102,7 +103,6 @@ class ExchangeData: def __init__(self, target_currency, api_key): """Initialize the data object.""" - from fixerio import Fixerio self.api_key = api_key self.rate = None diff --git a/homeassistant/components/fleetgo/device_tracker.py b/homeassistant/components/fleetgo/device_tracker.py index 0561530345c..5a922ed4b92 100644 --- a/homeassistant/components/fleetgo/device_tracker.py +++ b/homeassistant/components/fleetgo/device_tracker.py @@ -2,11 +2,12 @@ import logging import requests +from ritassist import API import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import PLATFORM_SCHEMA -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import track_utc_time_change _LOGGER = logging.getLogger(__name__) @@ -40,7 +41,6 @@ class FleetGoDeviceScanner: def __init__(self, config, see): """Initialize FleetGoDeviceScanner.""" - from ritassist import API self._include = config.get(CONF_INCLUDE) self._see = see diff --git a/homeassistant/components/flexit/climate.py b/homeassistant/components/flexit/climate.py index 951033849b6..34ddd9a8ffa 100644 --- a/homeassistant/components/flexit/climate.py +++ b/homeassistant/components/flexit/climate.py @@ -13,26 +13,28 @@ https://home-assistant.io/components/climate.flexit/ """ import logging from typing import List + +from pyflexit.pyflexit import pyflexit import voluptuous as vol -from homeassistant.const import ( - CONF_NAME, - CONF_SLAVE, - TEMP_CELSIUS, - ATTR_TEMPERATURE, - DEVICE_DEFAULT_NAME, -) -from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice from homeassistant.components.climate.const import ( - SUPPORT_TARGET_TEMPERATURE, - SUPPORT_FAN_MODE, HVAC_MODE_COOL, + SUPPORT_FAN_MODE, + SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.components.modbus import ( CONF_HUB, DEFAULT_HUB, DOMAIN as MODBUS_DOMAIN, ) +from homeassistant.const import ( + ATTR_TEMPERATURE, + CONF_NAME, + CONF_SLAVE, + DEVICE_DEFAULT_NAME, + TEMP_CELSIUS, +) import homeassistant.helpers.config_validation as cv PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -61,8 +63,6 @@ class Flexit(ClimateDevice): def __init__(self, hub, modbus_slave, name): """Initialize the unit.""" - from pyflexit import pyflexit - self._hub = hub self._name = name self._slave = modbus_slave @@ -79,7 +79,7 @@ class Flexit(ClimateDevice): self._heating = None self._cooling = None self._alarm = False - self.unit = pyflexit.pyflexit(hub, modbus_slave) + self.unit = pyflexit(hub, modbus_slave) @property def supported_features(self): diff --git a/homeassistant/components/flic/binary_sensor.py b/homeassistant/components/flic/binary_sensor.py index 416d39e5332..4f2f229977f 100644 --- a/homeassistant/components/flic/binary_sensor.py +++ b/homeassistant/components/flic/binary_sensor.py @@ -3,24 +3,24 @@ import logging import threading from pyflic import ( - FlicClient, ButtonConnectionChannel, ClickType, ConnectionStatus, + FlicClient, ScanWizard, ScanWizardResult, ) import voluptuous as vol -import homeassistant.helpers.config_validation as cv +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice from homeassistant.const import ( + CONF_DISCOVERY, CONF_HOST, CONF_PORT, - CONF_DISCOVERY, CONF_TIMEOUT, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/flock/notify.py b/homeassistant/components/flock/notify.py index ba52d3b4beb..a71601ea2c4 100644 --- a/homeassistant/components/flock/notify.py +++ b/homeassistant/components/flock/notify.py @@ -5,12 +5,11 @@ import logging import async_timeout import voluptuous as vol +from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService - _LOGGER = logging.getLogger(__name__) _RESOURCE = "https://api.flock.com/hooks/sendMessage/" diff --git a/homeassistant/components/flume/__init__.py b/homeassistant/components/flume/__init__.py new file mode 100644 index 00000000000..ab626e1f156 --- /dev/null +++ b/homeassistant/components/flume/__init__.py @@ -0,0 +1 @@ +"""The Flume component.""" diff --git a/homeassistant/components/flume/manifest.json b/homeassistant/components/flume/manifest.json new file mode 100644 index 00000000000..6a9fb7a1fd8 --- /dev/null +++ b/homeassistant/components/flume/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "flume", + "name": "Flume", + "documentation": "https://www.home-assistant.io/integrations/flume/", + "requirements": [ + "pyflume==0.2.4" + ], + "dependencies": [], + "codeowners": ["@ChrisMandich"] + } + diff --git a/homeassistant/components/flume/sensor.py b/homeassistant/components/flume/sensor.py new file mode 100644 index 00000000000..e96ce0d96ef --- /dev/null +++ b/homeassistant/components/flume/sensor.py @@ -0,0 +1,94 @@ +"""Sensor for displaying the number of result from Flume.""" +from datetime import timedelta +import logging + +from pyflume import FlumeData, FlumeDeviceList +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity + +LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = "Flume Sensor" + +CONF_CLIENT_ID = "client_id" +CONF_CLIENT_SECRET = "client_secret" +FLUME_TYPE_SENSOR = 2 + +SCAN_INTERVAL = timedelta(minutes=1) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_CLIENT_ID): cv.string, + vol.Required(CONF_CLIENT_SECRET): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + } +) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Flume sensor.""" + username = config[CONF_USERNAME] + password = config[CONF_PASSWORD] + client_id = config[CONF_CLIENT_ID] + client_secret = config[CONF_CLIENT_SECRET] + flume_token_file = hass.config.path("FLUME_TOKEN_FILE") + time_zone = str(hass.config.time_zone) + name = config[CONF_NAME] + flume_entity_list = [] + + flume_devices = FlumeDeviceList( + username, password, client_id, client_secret, flume_token_file + ) + + for device in flume_devices.device_list: + if device["type"] == FLUME_TYPE_SENSOR: + flume = FlumeData( + username, + password, + client_id, + client_secret, + device["id"], + time_zone, + SCAN_INTERVAL, + flume_token_file, + ) + flume_entity_list.append(FlumeSensor(flume, f"{name} {device['id']}")) + + if flume_entity_list: + add_entities(flume_entity_list, True) + + +class FlumeSensor(Entity): + """Representation of the Flume sensor.""" + + def __init__(self, flume, name): + """Initialize the Flume sensor.""" + self.flume = flume + self._name = name + self._state = None + + @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 unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return "gal" + + def update(self): + """Get the latest data and updates the states.""" + self.flume.update() + self._state = self.flume.value diff --git a/homeassistant/components/flunearyou/sensor.py b/homeassistant/components/flunearyou/sensor.py index 0df61fd24e1..86a97cce8c7 100644 --- a/homeassistant/components/flunearyou/sensor.py +++ b/homeassistant/components/flunearyou/sensor.py @@ -1,19 +1,21 @@ """Support for user- and CDC-based flu info sensors from Flu Near You.""" -import logging from datetime import timedelta +import logging +from pyflunearyou import Client +from pyflunearyou.errors import FluNearYouError import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_STATE, CONF_LATITUDE, - CONF_MONITORED_CONDITIONS, CONF_LONGITUDE, + CONF_MONITORED_CONDITIONS, ) from homeassistant.helpers import aiohttp_client +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -80,8 +82,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Configure the platform and add the sensors.""" - from pyflunearyou import Client - websession = aiohttp_client.async_get_clientsession(hass) latitude = config.get(CONF_LATITUDE, hass.config.latitude) @@ -219,8 +219,6 @@ class FluNearYouData: @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self): """Update Flu Near You data.""" - from pyflunearyou.errors import FluNearYouError - for key, method in [ (CATEGORY_CDC_REPORT, self._client.cdc_reports.status_by_coordinates), (CATEGORY_USER_REPORT, self._client.user_reports.status_by_coordinates), diff --git a/homeassistant/components/flux/switch.py b/homeassistant/components/flux/switch.py index 404067d4107..f22b6335911 100644 --- a/homeassistant/components/flux/switch.py +++ b/homeassistant/components/flux/switch.py @@ -11,9 +11,7 @@ import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.light import ( - is_on, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, @@ -22,29 +20,31 @@ from homeassistant.components.light import ( ATTR_XY_COLOR, DOMAIN as LIGHT_DOMAIN, VALID_TRANSITION, + is_on, ) from homeassistant.components.switch import DOMAIN, SwitchDevice from homeassistant.const import ( ATTR_ENTITY_ID, - CONF_NAME, - CONF_PLATFORM, CONF_LIGHTS, CONF_MODE, + CONF_NAME, + CONF_PLATFORM, SERVICE_TURN_ON, STATE_ON, SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.sun import get_astral_event_date from homeassistant.util import slugify from homeassistant.util.color import ( - color_temperature_to_rgb, color_RGB_to_xy_brightness, color_temperature_kelvin_to_mired, + color_temperature_to_rgb, ) -from homeassistant.util.dt import utcnow as dt_utcnow, as_local +from homeassistant.util.dt import as_local, utcnow as dt_utcnow _LOGGER = logging.getLogger(__name__) @@ -323,7 +323,7 @@ class FluxSwitch(SwitchDevice, RestoreEntity): elif self._mode == MODE_RGB: await async_set_lights_rgb(self.hass, self._lights, rgb, self._transition) _LOGGER.debug( - "Lights updated to rgb:%s, %s%% " "of %s cycle complete at %s", + "Lights updated to rgb:%s, %s%% of %s cycle complete at %s", rgb, round(percentage_complete * 100), time_state, diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index 7973956848a..16db60abbc0 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -1,28 +1,28 @@ """Support for Flux lights.""" import logging -import socket import random +import socket from flux_led import BulbScanner, WifiLedBulb import voluptuous as vol -from homeassistant.const import CONF_DEVICES, CONF_NAME, CONF_PROTOCOL, ATTR_MODE from homeassistant.components.light import ( ATTR_BRIGHTNESS, - ATTR_HS_COLOR, - ATTR_EFFECT, - ATTR_WHITE_VALUE, ATTR_COLOR_TEMP, + ATTR_EFFECT, + ATTR_HS_COLOR, + ATTR_WHITE_VALUE, EFFECT_COLORLOOP, EFFECT_RANDOM, - SUPPORT_BRIGHTNESS, - SUPPORT_EFFECT, - SUPPORT_COLOR, - SUPPORT_WHITE_VALUE, - SUPPORT_COLOR_TEMP, - Light, PLATFORM_SCHEMA, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, + SUPPORT_COLOR_TEMP, + SUPPORT_EFFECT, + SUPPORT_WHITE_VALUE, + Light, ) +from homeassistant.const import ATTR_MODE, CONF_DEVICES, CONF_NAME, CONF_PROTOCOL import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util diff --git a/homeassistant/components/folder/sensor.py b/homeassistant/components/folder/sensor.py index e9e4ea680c4..a706ab2a0b5 100644 --- a/homeassistant/components/folder/sensor.py +++ b/homeassistant/components/folder/sensor.py @@ -6,9 +6,9 @@ import os import voluptuous as vol -from homeassistant.helpers.entity import Entity -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/folder_watcher/__init__.py b/homeassistant/components/folder_watcher/__init__.py index b328744aaba..d99e4928cc5 100644 --- a/homeassistant/components/folder_watcher/__init__.py +++ b/homeassistant/components/folder_watcher/__init__.py @@ -3,6 +3,8 @@ import logging import os import voluptuous as vol +from watchdog.events import PatternMatchingEventHandler +from watchdog.observers import Observer from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP import homeassistant.helpers.config_validation as cv @@ -50,7 +52,6 @@ def setup(hass, config): def create_event_handler(patterns, hass): """Return the Watchdog EventHandler object.""" - from watchdog.events import PatternMatchingEventHandler class EventHandler(PatternMatchingEventHandler): """Class for handling Watcher events.""" @@ -99,8 +100,6 @@ class Watcher: def __init__(self, path, patterns, hass): """Initialise the watchdog observer.""" - from watchdog.observers import Observer - self._observer = Observer() self._observer.schedule( create_event_handler(patterns, hass), path, recursive=True diff --git a/homeassistant/components/foobot/sensor.py b/homeassistant/components/foobot/sensor.py index 8d3cf6de27d..efb74e2cc9a 100644 --- a/homeassistant/components/foobot/sensor.py +++ b/homeassistant/components/foobot/sensor.py @@ -1,26 +1,26 @@ """Support for the Foobot indoor air quality monitor.""" import asyncio -import logging from datetime import timedelta +import logging import aiohttp +from foobot_async import FoobotClient import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.exceptions import PlatformNotReady from homeassistant.const import ( - ATTR_TIME, ATTR_TEMPERATURE, + ATTR_TIME, CONF_TOKEN, CONF_USERNAME, TEMP_CELSIUS, ) +from homeassistant.exceptions import PlatformNotReady 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 from homeassistant.util import Throttle - _LOGGER = logging.getLogger(__name__) ATTR_HUMIDITY = "humidity" @@ -51,8 +51,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the devices associated with the account.""" - from foobot_async import FoobotClient - token = config.get(CONF_TOKEN) username = config.get(CONF_USERNAME) diff --git a/homeassistant/components/fortigate/__init__.py b/homeassistant/components/fortigate/__init__.py index d1f6eb52333..6de55ae3d65 100644 --- a/homeassistant/components/fortigate/__init__.py +++ b/homeassistant/components/fortigate/__init__.py @@ -1,12 +1,13 @@ """Fortigate integration.""" import logging +from pyFGT.fortigate import FGTConnectionError, FortiGate import voluptuous as vol from homeassistant.const import ( + CONF_API_KEY, CONF_DEVICES, CONF_HOST, - CONF_API_KEY, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, ) @@ -52,8 +53,6 @@ async def async_setup(hass, config): async def async_setup_fortigate(hass, config, host, user, api_key, devices): """Start up the Fortigate component platforms.""" - from pyFGT.fortigate import FGTConnectionError, FortiGate - fgt = FortiGate(host, user, apikey=api_key, disable_request_warnings=True) try: diff --git a/homeassistant/components/fortios/device_tracker.py b/homeassistant/components/fortios/device_tracker.py index 51bce5429f6..2b2d14f60e0 100644 --- a/homeassistant/components/fortios/device_tracker.py +++ b/homeassistant/components/fortios/device_tracker.py @@ -5,17 +5,16 @@ This component is part of the device_tracker platform. """ import logging -import voluptuous as vol from fortiosapi import FortiOSAPI +import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner, ) -from homeassistant.const import CONF_HOST, CONF_TOKEN -from homeassistant.const import CONF_VERIFY_SSL +from homeassistant.const import CONF_HOST, CONF_TOKEN, CONF_VERIFY_SSL +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) DEFAULT_VERIFY_SSL = False diff --git a/homeassistant/components/foscam/camera.py b/homeassistant/components/foscam/camera.py index 0e2ca4073bf..f4ec6556894 100644 --- a/homeassistant/components/foscam/camera.py +++ b/homeassistant/components/foscam/camera.py @@ -1,26 +1,26 @@ """This component provides basic support for Foscam IP cameras.""" -import logging import asyncio +import logging from libpyfoscam import FoscamCamera - import voluptuous as vol -from homeassistant.components.camera import Camera, PLATFORM_SCHEMA, SUPPORT_STREAM +from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_STREAM, Camera from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_NAME, - CONF_USERNAME, CONF_PASSWORD, CONF_PORT, - ATTR_ENTITY_ID, + CONF_USERNAME, ) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.service import async_extract_entity_ids -from .const import DOMAIN as FOSCAM_DOMAIN -from .const import DATA as FOSCAM_DATA -from .const import ENTITIES as FOSCAM_ENTITIES - +from .const import ( + DATA as FOSCAM_DATA, + DOMAIN as FOSCAM_DOMAIN, + ENTITIES as FOSCAM_ENTITIES, +) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/foursquare/__init__.py b/homeassistant/components/foursquare/__init__.py index 7efb989e142..af15c4e5fa8 100644 --- a/homeassistant/components/foursquare/__init__.py +++ b/homeassistant/components/foursquare/__init__.py @@ -4,9 +4,9 @@ import logging import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_ACCESS_TOKEN, HTTP_BAD_REQUEST from homeassistant.components.http import HomeAssistantView +from homeassistant.const import CONF_ACCESS_TOKEN, HTTP_BAD_REQUEST +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -103,7 +103,7 @@ class FoursquarePushReceiver(HomeAssistantView): if self.push_secret != secret: _LOGGER.error( - "Received Foursquare push with invalid" "push secret: %s", secret + "Received Foursquare push with invalid push secret: %s", secret ) return self.json_message("Incorrect secret", HTTP_BAD_REQUEST) diff --git a/homeassistant/components/free_mobile/notify.py b/homeassistant/components/free_mobile/notify.py index 5733e3c19c0..8b5273f39d1 100644 --- a/homeassistant/components/free_mobile/notify.py +++ b/homeassistant/components/free_mobile/notify.py @@ -1,13 +1,13 @@ -"""Support for thr Free Mobile SMS platform.""" +"""Support for Free Mobile SMS platform.""" import logging +from freesms import FreeClient import voluptuous as vol +from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService from homeassistant.const import CONF_ACCESS_TOKEN, CONF_USERNAME import homeassistant.helpers.config_validation as cv -from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService - _LOGGER = logging.getLogger(__name__) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -25,8 +25,6 @@ class FreeSMSNotificationService(BaseNotificationService): def __init__(self, username, access_token): """Initialize the service.""" - from freesms import FreeClient - self.free_client = FreeClient(username, access_token) def send_message(self, message="", **kwargs): diff --git a/homeassistant/components/freebox/__init__.py b/homeassistant/components/freebox/__init__.py index 0bffedd46dc..58426334dea 100644 --- a/homeassistant/components/freebox/__init__.py +++ b/homeassistant/components/freebox/__init__.py @@ -2,6 +2,8 @@ import logging import socket +from aiofreepybox import Freepybox +from aiofreepybox.exceptions import HttpRequestError import voluptuous as vol from homeassistant.components.discovery import SERVICE_FREEBOX @@ -49,8 +51,6 @@ async def async_setup(hass, config): async def async_setup_freebox(hass, config, host, port): """Start up the Freebox component platforms.""" - from aiofreepybox import Freepybox - from aiofreepybox.exceptions import HttpRequestError app_desc = { "app_id": "hass", @@ -60,7 +60,7 @@ async def async_setup_freebox(hass, config, host, port): } token_file = hass.config.path(FREEBOX_CONFIG_FILE) - api_version = "v4" + api_version = "v6" fbx = Freepybox(app_desc=app_desc, token_file=token_file, api_version=api_version) @@ -71,6 +71,12 @@ async def async_setup_freebox(hass, config, host, port): else: hass.data[DATA_FREEBOX] = fbx + async def async_freebox_reboot(call): + """Handle reboot service call.""" + await fbx.system.reboot() + + hass.services.async_register(DOMAIN, "reboot", async_freebox_reboot) + hass.async_create_task(async_load_platform(hass, "sensor", DOMAIN, {}, config)) hass.async_create_task( async_load_platform(hass, "device_tracker", DOMAIN, {}, config) diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py index e8f96586300..61ec670d217 100644 --- a/homeassistant/components/freebox/sensor.py +++ b/homeassistant/components/freebox/sensor.py @@ -18,6 +18,8 @@ class FbxSensor(Entity): """Representation of a freebox sensor.""" _name = "generic" + _unit = None + _icon = None def __init__(self, fbx): """Initialize the sensor.""" @@ -30,6 +32,16 @@ class FbxSensor(Entity): """Return the name of the sensor.""" return self._name + @property + def unit_of_measurement(self): + """Return the unit of the sensor.""" + return self._unit + + @property + def icon(self): + """Return the icon of the sensor.""" + return self._icon + @property def state(self): """Return the state of the sensor.""" @@ -45,11 +57,7 @@ class FbxRXSensor(FbxSensor): _name = "Freebox download speed" _unit = "KB/s" - - @property - def unit_of_measurement(self): - """Define the unit.""" - return self._unit + _icon = "mdi:download-network" async def async_update(self): """Get the value from fetched datas.""" @@ -63,11 +71,7 @@ class FbxTXSensor(FbxSensor): _name = "Freebox upload speed" _unit = "KB/s" - - @property - def unit_of_measurement(self): - """Define the unit.""" - return self._unit + _icon = "mdi:upload-network" async def async_update(self): """Get the value from fetched datas.""" diff --git a/homeassistant/components/freebox/services.yaml b/homeassistant/components/freebox/services.yaml new file mode 100644 index 00000000000..be7afa60562 --- /dev/null +++ b/homeassistant/components/freebox/services.yaml @@ -0,0 +1,5 @@ +# Freebox service entries description. + +reboot: + # Description of the service + description: Reboots the Freebox. diff --git a/homeassistant/components/freedns/__init__.py b/homeassistant/components/freedns/__init__.py index 30f80280c1f..7aa34c8780e 100644 --- a/homeassistant/components/freedns/__init__.py +++ b/homeassistant/components/freedns/__init__.py @@ -1,14 +1,14 @@ """Integrate with FreeDNS Dynamic DNS service at freedns.afraid.org.""" import asyncio -import logging from datetime import timedelta +import logging import aiohttp import async_timeout import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_ACCESS_TOKEN, CONF_SCAN_INTERVAL, CONF_URL +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/fritz/device_tracker.py b/homeassistant/components/fritz/device_tracker.py index ab4deec96f7..e2382490cde 100644 --- a/homeassistant/components/fritz/device_tracker.py +++ b/homeassistant/components/fritz/device_tracker.py @@ -60,7 +60,7 @@ class FritzBoxScanner(DeviceScanner): self._update_info() else: _LOGGER.error( - "Failed to establish connection to FRITZ!Box " "with IP: %s", self.host + "Failed to establish connection to FRITZ!Box with IP: %s", self.host ) def scan_devices(self): @@ -79,6 +79,14 @@ class FritzBoxScanner(DeviceScanner): return None return ret + def get_extra_attributes(self, device): + """Return the attributes (ip, mac) of the given device or None if is not known.""" + ip_device = self.fritz_box.get_specific_host_entry(device).get("NewIPAddress") + + if not ip_device: + return None + return {"ip": ip_device, "mac": device} + def _update_info(self): """Retrieve latest information from the FRITZ!Box.""" if not self.success_init: diff --git a/homeassistant/components/fritzbox/climate.py b/homeassistant/components/fritzbox/climate.py index 40c16930206..115f7f8e644 100644 --- a/homeassistant/components/fritzbox/climate.py +++ b/homeassistant/components/fritzbox/climate.py @@ -7,11 +7,11 @@ from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( ATTR_HVAC_MODE, HVAC_MODE_HEAT, - PRESET_ECO, - PRESET_COMFORT, - SUPPORT_TARGET_TEMPERATURE, HVAC_MODE_OFF, + PRESET_COMFORT, + PRESET_ECO, SUPPORT_PRESET_MODE, + SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ( ATTR_BATTERY_LEVEL, diff --git a/homeassistant/components/fritzbox_callmonitor/sensor.py b/homeassistant/components/fritzbox_callmonitor/sensor.py index b1d601ce382..600420db859 100644 --- a/homeassistant/components/fritzbox_callmonitor/sensor.py +++ b/homeassistant/components/fritzbox_callmonitor/sensor.py @@ -60,6 +60,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up Fritz!Box call monitor sensor platform.""" name = config.get(CONF_NAME) host = config.get(CONF_HOST) + # Try to resolve a hostname; if it is already an IP, it will be returned as-is + try: + host = socket.gethostbyname(host) + except socket.error: + _LOGGER.error("Could not resolve hostname %s", host) + return port = config.get(CONF_PORT) username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) diff --git a/homeassistant/components/fronius/sensor.py b/homeassistant/components/fronius/sensor.py index ff0694afaab..27e2531c9f9 100644 --- a/homeassistant/components/fronius/sensor.py +++ b/homeassistant/components/fronius/sensor.py @@ -2,24 +2,23 @@ import copy from datetime import timedelta import logging -import voluptuous as vol from pyfronius import Fronius +import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_RESOURCE, - CONF_SENSOR_TYPE, CONF_DEVICE, CONF_MONITORED_CONDITIONS, + CONF_RESOURCE, CONF_SCAN_INTERVAL, + CONF_SENSOR_TYPE, ) 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__) CONF_SCOPE = "scope" diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 7ef2bd38644..efb1c34653b 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -242,6 +242,7 @@ def _frontend_root(dev_repo_path): if dev_repo_path is not None: return pathlib.Path(dev_repo_path) / "hass_frontend" # Keep import here so that we can import frontend without installing reqs + # pylint: disable=import-outside-toplevel import hass_frontend return hass_frontend.where() diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 6c59ea38fe4..75d02baaeeb 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20191115.0" + "home-assistant-frontend==20191204.1" ], "dependencies": [ "api", diff --git a/homeassistant/components/frontend/storage.py b/homeassistant/components/frontend/storage.py index 75b7b356ef9..2f68c5f8e01 100644 --- a/homeassistant/components/frontend/storage.py +++ b/homeassistant/components/frontend/storage.py @@ -1,10 +1,10 @@ """API for persistent storage for the frontend.""" from functools import wraps + import voluptuous as vol from homeassistant.components import websocket_api - # mypy: allow-untyped-calls, allow-untyped-defs DATA_STORAGE = "frontend_storage" diff --git a/homeassistant/components/garadget/cover.py b/homeassistant/components/garadget/cover.py index d487c39db6b..0eeb5f2b8f9 100644 --- a/homeassistant/components/garadget/cover.py +++ b/homeassistant/components/garadget/cover.py @@ -4,19 +4,19 @@ import logging import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.components.cover import CoverDevice, PLATFORM_SCHEMA -from homeassistant.helpers.event import track_utc_time_change +from homeassistant.components.cover import PLATFORM_SCHEMA, CoverDevice from homeassistant.const import ( - CONF_DEVICE, - CONF_USERNAME, - CONF_PASSWORD, CONF_ACCESS_TOKEN, + CONF_COVERS, + CONF_DEVICE, CONF_NAME, + CONF_PASSWORD, + CONF_USERNAME, STATE_CLOSED, STATE_OPEN, - CONF_COVERS, ) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import track_utc_time_change _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/gearbest/sensor.py b/homeassistant/components/gearbest/sensor.py index bad9b335e73..b9b2a35b89d 100644 --- a/homeassistant/components/gearbest/sensor.py +++ b/homeassistant/components/gearbest/sensor.py @@ -1,15 +1,16 @@ """Parse prices of an item from gearbest.""" -import logging from datetime import timedelta +import logging +from gearbest_parser import CurrencyConverter, GearbestParser import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_CURRENCY, CONF_ID, CONF_NAME, CONF_URL import homeassistant.helpers.config_validation as cv -from homeassistant.util import Throttle from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import track_time_interval -from homeassistant.const import CONF_NAME, CONF_ID, CONF_URL, CONF_CURRENCY +from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -41,7 +42,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Gearbest sensor.""" - from gearbest_parser import CurrencyConverter currency = config.get(CONF_CURRENCY) @@ -71,7 +71,6 @@ class GearbestSensor(Entity): def __init__(self, converter, item, currency): """Initialize the sensor.""" - from gearbest_parser import GearbestParser self._name = item.get(CONF_NAME) self._parser = GearbestParser() diff --git a/homeassistant/components/geizhals/sensor.py b/homeassistant/components/geizhals/sensor.py index 28fe10ec5f5..9d5605cc404 100644 --- a/homeassistant/components/geizhals/sensor.py +++ b/homeassistant/components/geizhals/sensor.py @@ -1,14 +1,15 @@ """Parse prices of a device from geizhals.""" -import logging from datetime import timedelta +import logging +from geizhals import Device, Geizhals import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -import homeassistant.helpers.config_validation as cv -from homeassistant.util import Throttle -from homeassistant.helpers.entity import Entity from homeassistant.const import CONF_NAME +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -45,7 +46,6 @@ class Geizwatch(Entity): def __init__(self, name, description, product_id, domain): """Initialize the sensor.""" - from geizhals import Device, Geizhals # internal self._name = name diff --git a/homeassistant/components/generic/camera.py b/homeassistant/components/generic/camera.py index 01d2fb948ed..3d39d75ff4a 100644 --- a/homeassistant/components/generic/camera.py +++ b/homeassistant/components/generic/camera.py @@ -8,24 +8,24 @@ import requests from requests.auth import HTTPDigestAuth import voluptuous as vol -from homeassistant.const import ( - CONF_NAME, - CONF_USERNAME, - CONF_PASSWORD, - CONF_AUTHENTICATION, - HTTP_BASIC_AUTHENTICATION, - HTTP_DIGEST_AUTHENTICATION, - CONF_VERIFY_SSL, -) -from homeassistant.exceptions import TemplateError from homeassistant.components.camera import ( - PLATFORM_SCHEMA, DEFAULT_CONTENT_TYPE, + PLATFORM_SCHEMA, SUPPORT_STREAM, Camera, ) -from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.const import ( + CONF_AUTHENTICATION, + CONF_NAME, + CONF_PASSWORD, + CONF_USERNAME, + CONF_VERIFY_SSL, + HTTP_BASIC_AUTHENTICATION, + HTTP_DIGEST_AUTHENTICATION, +) +from homeassistant.exceptions import TemplateError from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.aiohttp_client import async_get_clientsession _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/generic_thermostat/climate.py b/homeassistant/components/generic_thermostat/climate.py index b765dbbfda4..58514934fc7 100644 --- a/homeassistant/components/generic_thermostat/climate.py +++ b/homeassistant/components/generic_thermostat/climate.py @@ -15,9 +15,9 @@ from homeassistant.components.climate.const import ( HVAC_MODE_HEAT, HVAC_MODE_OFF, PRESET_AWAY, + PRESET_NONE, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, - PRESET_NONE, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -214,7 +214,7 @@ class GenericThermostat(ClimateDevice, RestoreEntity): else: self._target_temp = self.min_temp _LOGGER.warning( - "Undefined target temperature," "falling back to %s", + "Undefined target temperature, falling back to %s", self._target_temp, ) else: @@ -298,16 +298,12 @@ class GenericThermostat(ClimateDevice, RestoreEntity): @property def preset_mode(self): """Return the current preset mode, e.g., home, away, temp.""" - if self._is_away: - return PRESET_AWAY - return None + return PRESET_AWAY if self._is_away else PRESET_NONE @property def preset_modes(self): - """Return a list of available preset modes.""" - if self._away_temp: - return [PRESET_NONE, PRESET_AWAY] - return None + """Return a list of available preset modes or PRESET_NONE if _away_temp is undefined.""" + return [PRESET_NONE, PRESET_AWAY] if self._away_temp else PRESET_NONE async def async_set_hvac_mode(self, hvac_mode): """Set hvac mode.""" diff --git a/homeassistant/components/geniushub/switch.py b/homeassistant/components/geniushub/switch.py index 79d14417dd4..b73c9a89041 100644 --- a/homeassistant/components/geniushub/switch.py +++ b/homeassistant/components/geniushub/switch.py @@ -1,5 +1,5 @@ """Support for Genius Hub switch/outlet devices.""" -from homeassistant.components.switch import SwitchDevice, DEVICE_CLASS_OUTLET +from homeassistant.components.switch import DEVICE_CLASS_OUTLET, SwitchDevice from homeassistant.helpers.typing import ConfigType, HomeAssistantType from . import DOMAIN, GeniusZone diff --git a/homeassistant/components/geo_json_events/geo_location.py b/homeassistant/components/geo_json_events/geo_location.py index 2bf309e2450..2f881232495 100644 --- a/homeassistant/components/geo_json_events/geo_location.py +++ b/homeassistant/components/geo_json_events/geo_location.py @@ -3,6 +3,7 @@ from datetime import timedelta import logging from typing import Optional +from geojson_client.generic_feed import GenericFeedManager import voluptuous as vol from homeassistant.components.geo_location import PLATFORM_SCHEMA, GeolocationEvent @@ -71,7 +72,6 @@ class GeoJsonFeedEntityManager: self, hass, add_entities, scan_interval, coordinates, url, radius_in_km ): """Initialize the GeoJSON Feed Manager.""" - from geojson_client.generic_feed import GenericFeedManager self._hass = hass self._feed_manager = GenericFeedManager( diff --git a/homeassistant/components/geo_location/__init__.py b/homeassistant/components/geo_location/__init__.py index e5c587f93ed..6142fa22209 100644 --- a/homeassistant/components/geo_location/__init__.py +++ b/homeassistant/components/geo_location/__init__.py @@ -11,7 +11,6 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent - # mypy: allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/geo_rss_events/manifest.json b/homeassistant/components/geo_rss_events/manifest.json index c681807ad01..b8949286dea 100644 --- a/homeassistant/components/geo_rss_events/manifest.json +++ b/homeassistant/components/geo_rss_events/manifest.json @@ -3,7 +3,7 @@ "name": "Geo RSS events", "documentation": "https://www.home-assistant.io/integrations/geo_rss_events", "requirements": [ - "georss_generic_client==0.2" + "georss_generic_client==0.3" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/geo_rss_events/sensor.py b/homeassistant/components/geo_rss_events/sensor.py index 39e6c5c7e82..b8891cdef0d 100644 --- a/homeassistant/components/geo_rss_events/sensor.py +++ b/homeassistant/components/geo_rss_events/sensor.py @@ -8,23 +8,23 @@ and grouped by category. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.geo_rss_events/ """ -import logging from datetime import timedelta +import logging -import voluptuous as vol from georss_client import UPDATE_OK, UPDATE_OK_NO_DATA from georss_client.generic_feed import GenericFeed +import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_UNIT_OF_MEASUREMENT, - CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE, + CONF_NAME, CONF_RADIUS, + CONF_UNIT_OF_MEASUREMENT, CONF_URL, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/geofency/.translations/da.json b/homeassistant/components/geofency/.translations/da.json index 1390dfb504a..6e9443af5e8 100644 --- a/homeassistant/components/geofency/.translations/da.json +++ b/homeassistant/components/geofency/.translations/da.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "not_internet_accessible": "Dit Home Assistant system skal v\u00e6re tilg\u00e6ngeligt fra internettet for at modtage Geofency meddelelser.", + "not_internet_accessible": "Din Home Assistant-instans skal v\u00e6re tilg\u00e6ngelig fra internettet for at modtage Geofency-meddelelser.", "one_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning" }, "create_entry": { - "default": "For at sende begivenheder til Home Assistant skal du konfigurere webhook funktionen i Geofency.\n\n Udfyld f\u00f8lgende oplysninger: \n\n - URL: `{webhook_url}`\n - Metode: POST\n \n Se [dokumentationen]({docs_url}) for yderligere oplysninger." + "default": "For at sende h\u00e6ndelser til Home Assistant skal du konfigurere webhook-funktionen i Geofency.\n\n Udfyld f\u00f8lgende oplysninger: \n\n - Webadresse: `{webhook_url}`\n - Metode: POST\n \nSe [dokumentationen]({docs_url}) for yderligere oplysninger." }, "step": { "user": { diff --git a/homeassistant/components/geofency/.translations/ko.json b/homeassistant/components/geofency/.translations/ko.json index 42ff061a151..37f5ef0e76a 100644 --- a/homeassistant/components/geofency/.translations/ko.json +++ b/homeassistant/components/geofency/.translations/ko.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "Geofency Webhook \uc744 \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "Geofency Webhook \uc744 \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "Geofency Webhook \uc124\uc815" } }, diff --git a/homeassistant/components/geofency/__init__.py b/homeassistant/components/geofency/__init__.py index 9d8e0b29f5d..9afc9a8bfac 100644 --- a/homeassistant/components/geofency/__init__.py +++ b/homeassistant/components/geofency/__init__.py @@ -18,8 +18,8 @@ from homeassistant.helpers import config_entry_flow import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.util import slugify -from .const import DOMAIN +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/geofency/config_flow.py b/homeassistant/components/geofency/config_flow.py index 1a87502df2a..2d8bce86d74 100644 --- a/homeassistant/components/geofency/config_flow.py +++ b/homeassistant/components/geofency/config_flow.py @@ -1,7 +1,7 @@ """Config flow for Geofency.""" from homeassistant.helpers import config_entry_flow -from .const import DOMAIN +from .const import DOMAIN config_entry_flow.register_webhook_flow( DOMAIN, diff --git a/homeassistant/components/geofency/device_tracker.py b/homeassistant/components/geofency/device_tracker.py index 09e9d46ce6d..49bd70192ef 100644 --- a/homeassistant/components/geofency/device_tracker.py +++ b/homeassistant/components/geofency/device_tracker.py @@ -1,13 +1,13 @@ """Support for the Geofency device tracker platform.""" import logging -from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE -from homeassistant.core import callback from homeassistant.components.device_tracker import SOURCE_TYPE_GPS from homeassistant.components.device_tracker.config_entry import TrackerEntity +from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE +from homeassistant.core import callback +from homeassistant.helpers import device_registry from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers import device_registry from . import DOMAIN as GF_DOMAIN, TRACKER_UPDATE diff --git a/homeassistant/components/geonetnz_quakes/.translations/hu.json b/homeassistant/components/geonetnz_quakes/.translations/hu.json index 42de5a13142..4a163d24b75 100644 --- a/homeassistant/components/geonetnz_quakes/.translations/hu.json +++ b/homeassistant/components/geonetnz_quakes/.translations/hu.json @@ -5,7 +5,7 @@ "data": { "radius": "Sug\u00e1r" }, - "title": "T\u00f6ltse ki a sz\u0171r\u0151 adatait." + "title": "T\u00f6ltsd ki a sz\u0171r\u0151 adatait." } } } diff --git a/homeassistant/components/geonetnz_quakes/.translations/pl.json b/homeassistant/components/geonetnz_quakes/.translations/pl.json index 427c753f6c1..fd82bba43b5 100644 --- a/homeassistant/components/geonetnz_quakes/.translations/pl.json +++ b/homeassistant/components/geonetnz_quakes/.translations/pl.json @@ -9,7 +9,7 @@ "mmi": "MMI", "radius": "Promie\u0144" }, - "title": "Wype\u0142nij szczeg\u00f3\u0142y dotycz\u0105ce filtra." + "title": "Wprowad\u017a szczeg\u00f3\u0142owe dane filtra." } }, "title": "GeoNet NZ Quakes" diff --git a/homeassistant/components/geonetnz_quakes/.translations/ru.json b/homeassistant/components/geonetnz_quakes/.translations/ru.json index d6763d17e2d..dddb5c47bb9 100644 --- a/homeassistant/components/geonetnz_quakes/.translations/ru.json +++ b/homeassistant/components/geonetnz_quakes/.translations/ru.json @@ -9,9 +9,9 @@ "mmi": "MMI", "radius": "\u0420\u0430\u0434\u0438\u0443\u0441" }, - "title": "GeoNet" + "title": "GeoNet NZ Quakes" } }, - "title": "\u0417\u0435\u043c\u043b\u0435\u0442\u0440\u044f\u0441\u0435\u043d\u0438\u044f \u0432 \u041d\u043e\u0432\u043e\u0439 \u0417\u0435\u043b\u0430\u043d\u0434\u0438\u0438 (GeoNet)" + "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 59b4abf259a..487ac9ea8c0 100644 --- a/homeassistant/components/geonetnz_quakes/.translations/zh-Hant.json +++ b/homeassistant/components/geonetnz_quakes/.translations/zh-Hant.json @@ -12,6 +12,6 @@ "title": "\u586b\u5beb\u904e\u6ffe\u5668\u8cc7\u8a0a\u3002" } }, - "title": "GeoNet NZ Quakes" + "title": "\u7d10\u897f\u862d GeoNet \u5730\u9707\u9810\u8b66" } } \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/__init__.py b/homeassistant/components/geonetnz_quakes/__init__.py index 069c9ab7daa..141d0506847 100644 --- a/homeassistant/components/geonetnz_quakes/__init__.py +++ b/homeassistant/components/geonetnz_quakes/__init__.py @@ -1,30 +1,29 @@ """The GeoNet NZ Quakes integration.""" import asyncio -import logging from datetime import timedelta +import logging -import voluptuous as vol from aio_geojson_geonetnz_quakes import GeonetnzQuakesFeedManager +import voluptuous as vol -from homeassistant.core import callback -from homeassistant.util.unit_system import METRIC_SYSTEM from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS, CONF_SCAN_INTERVAL, - CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM, + CONF_UNIT_SYSTEM_IMPERIAL, LENGTH_MILES, ) -from homeassistant.helpers import config_validation as cv, aiohttp_client +from homeassistant.core import callback +from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval +from homeassistant.util.unit_system import METRIC_SYSTEM from .config_flow import configured_instances from .const import ( - PLATFORMS, CONF_MINIMUM_MAGNITUDE, CONF_MMI, DEFAULT_FILTER_TIME_INTERVAL, @@ -34,6 +33,7 @@ from .const import ( DEFAULT_SCAN_INTERVAL, DOMAIN, FEED, + PLATFORMS, SIGNAL_DELETE_ENTITY, SIGNAL_NEW_GEOLOCATION, SIGNAL_STATUS, diff --git a/homeassistant/components/geonetnz_quakes/config_flow.py b/homeassistant/components/geonetnz_quakes/config_flow.py index bd93f08c72b..cc40f31f1fb 100644 --- a/homeassistant/components/geonetnz_quakes/config_flow.py +++ b/homeassistant/components/geonetnz_quakes/config_flow.py @@ -17,13 +17,13 @@ from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from .const import ( + CONF_MINIMUM_MAGNITUDE, CONF_MMI, + DEFAULT_MINIMUM_MAGNITUDE, DEFAULT_MMI, DEFAULT_RADIUS, DEFAULT_SCAN_INTERVAL, DOMAIN, - DEFAULT_MINIMUM_MAGNITUDE, - CONF_MINIMUM_MAGNITUDE, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/geonetnz_quakes/geo_location.py b/homeassistant/components/geonetnz_quakes/geo_location.py index 1ee7c287c61..ae8b8fef48d 100644 --- a/homeassistant/components/geonetnz_quakes/geo_location.py +++ b/homeassistant/components/geonetnz_quakes/geo_location.py @@ -5,10 +5,10 @@ from typing import Optional from homeassistant.components.geo_location import GeolocationEvent from homeassistant.const import ( ATTR_ATTRIBUTION, + ATTR_TIME, CONF_UNIT_SYSTEM_IMPERIAL, LENGTH_KILOMETERS, LENGTH_MILES, - ATTR_TIME, ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect diff --git a/homeassistant/components/geonetnz_volcano/.translations/bg.json b/homeassistant/components/geonetnz_volcano/.translations/bg.json new file mode 100644 index 00000000000..f895d282902 --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/.translations/bg.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "identifier_exists": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "step": { + "user": { + "data": { + "radius": "\u0420\u0430\u0434\u0438\u0443\u0441" + }, + "title": "\u041f\u043e\u043f\u044a\u043b\u043d\u0435\u0442\u0435 \u0434\u0430\u043d\u043d\u0438\u0442\u0435 \u0437\u0430 \u0444\u0438\u043b\u0442\u044a\u0440\u0430 \u0441\u0438." + } + }, + "title": "GeoNet NZ Volcano" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/.translations/ca.json b/homeassistant/components/geonetnz_volcano/.translations/ca.json new file mode 100644 index 00000000000..2e595b73040 --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/.translations/ca.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "identifier_exists": "Ubicaci\u00f3 ja registrada" + }, + "step": { + "user": { + "data": { + "radius": "Radi" + }, + "title": "Introdueix els detalls del filtre." + } + }, + "title": "GeoNet NZ Volcano" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/.translations/da.json b/homeassistant/components/geonetnz_volcano/.translations/da.json new file mode 100644 index 00000000000..a8c238a60b0 --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/.translations/da.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "identifier_exists": "Lokalitet allerede registreret" + }, + "step": { + "user": { + "data": { + "radius": "Radius" + }, + "title": "Udfyld dine filteroplysninger." + } + }, + "title": "GeoNet NZ vulkan" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/.translations/de.json b/homeassistant/components/geonetnz_volcano/.translations/de.json new file mode 100644 index 00000000000..1a51f1fb490 --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/.translations/de.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "identifier_exists": "Standort bereits registriert" + }, + "step": { + "user": { + "data": { + "radius": "Radius" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/.translations/en.json b/homeassistant/components/geonetnz_volcano/.translations/en.json new file mode 100644 index 00000000000..1175597908e --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/.translations/en.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "identifier_exists": "Location already registered" + }, + "step": { + "user": { + "data": { + "radius": "Radius" + }, + "title": "Fill in your filter details." + } + }, + "title": "GeoNet NZ Volcano" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/.translations/es.json b/homeassistant/components/geonetnz_volcano/.translations/es.json new file mode 100644 index 00000000000..c6b92e83089 --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/.translations/es.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "identifier_exists": "Lugar ya registrado" + }, + "step": { + "user": { + "data": { + "radius": "Radio" + }, + "title": "Complete los detalles de su filtro." + } + }, + "title": "GeoNet NZ Volc\u00e1n" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/.translations/fr.json b/homeassistant/components/geonetnz_volcano/.translations/fr.json new file mode 100644 index 00000000000..c93ae906a46 --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/.translations/fr.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "identifier_exists": "Emplacement d\u00e9j\u00e0 enregistr\u00e9" + }, + "step": { + "user": { + "data": { + "radius": "Rayon" + }, + "title": "Remplissez les d\u00e9tails de votre filtre." + } + }, + "title": "GeoNet NZ Volcano" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/.translations/it.json b/homeassistant/components/geonetnz_volcano/.translations/it.json new file mode 100644 index 00000000000..85bfc7297ee --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/.translations/it.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "identifier_exists": "Localit\u00e0 gi\u00e0 registrata" + }, + "step": { + "user": { + "data": { + "radius": "Raggio" + }, + "title": "Inserisci i tuoi dettagli del filtro." + } + }, + "title": "GeoNet NZ Vulcano" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/.translations/ko.json b/homeassistant/components/geonetnz_volcano/.translations/ko.json new file mode 100644 index 00000000000..5d393fef4c4 --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/.translations/ko.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "identifier_exists": "\uc704\uce58\uac00 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "radius": "\ubc18\uacbd" + }, + "title": "\ud544\ud130 \uc138\ubd80 \uc0ac\ud56d\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694" + } + }, + "title": "GeoNet NZ Volcano" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/.translations/lb.json b/homeassistant/components/geonetnz_volcano/.translations/lb.json new file mode 100644 index 00000000000..a7ad17e6bd5 --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/.translations/lb.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "identifier_exists": "Standuert ass scho registr\u00e9iert" + }, + "step": { + "user": { + "data": { + "radius": "Radius" + }, + "title": "F\u00ebllt \u00e4r Filter D\u00e9tailer aus." + } + }, + "title": "GeoNet NZ Vulkan" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/.translations/nl.json b/homeassistant/components/geonetnz_volcano/.translations/nl.json new file mode 100644 index 00000000000..44d814b9db2 --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/.translations/nl.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "identifier_exists": "Locatie al geregistreerd" + }, + "step": { + "user": { + "data": { + "radius": "Straal" + }, + "title": "Vul uw filtergegevens in." + } + }, + "title": "GeoNet NZ Volcano" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/.translations/no.json b/homeassistant/components/geonetnz_volcano/.translations/no.json new file mode 100644 index 00000000000..d66e0eb6d7d --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/.translations/no.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "identifier_exists": "Beliggenhet er allerede registrert" + }, + "step": { + "user": { + "data": { + "radius": "Radius" + }, + "title": "Fyll ut filterdetaljene." + } + }, + "title": "GeoNet NZ Volcano" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/.translations/pl.json b/homeassistant/components/geonetnz_volcano/.translations/pl.json new file mode 100644 index 00000000000..7d329815f3f --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/.translations/pl.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "identifier_exists": "Lokalizacja ju\u017c zarejestrowana" + }, + "step": { + "user": { + "data": { + "radius": "Promie\u0144" + }, + "title": "Wprowad\u017a szczeg\u00f3\u0142owe dane filtra." + } + }, + "title": "GeoNet NZ Volcano" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/.translations/pt-BR.json b/homeassistant/components/geonetnz_volcano/.translations/pt-BR.json new file mode 100644 index 00000000000..b1629599926 --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/.translations/pt-BR.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "identifier_exists": "Local j\u00e1 registrado" + }, + "step": { + "user": { + "data": { + "radius": "Raio" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/.translations/ro.json b/homeassistant/components/geonetnz_volcano/.translations/ro.json new file mode 100644 index 00000000000..4c0cd317d48 --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/.translations/ro.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "radius": "Raz\u0103" + }, + "title": "Completa\u021bi detaliile filtrului." + } + }, + "title": "Vulcanul GeoNet NZ" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/.translations/ru.json b/homeassistant/components/geonetnz_volcano/.translations/ru.json new file mode 100644 index 00000000000..6e7411f28b9 --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/.translations/ru.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "identifier_exists": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043e." + }, + "step": { + "user": { + "data": { + "radius": "\u0420\u0430\u0434\u0438\u0443\u0441" + }, + "title": "GeoNet NZ Volcano" + } + }, + "title": "GeoNet NZ Volcano" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/.translations/sl.json b/homeassistant/components/geonetnz_volcano/.translations/sl.json new file mode 100644 index 00000000000..e31f473c26f --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/.translations/sl.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "identifier_exists": "Lokacija je \u017ee registrirana" + }, + "step": { + "user": { + "data": { + "radius": "Radij" + }, + "title": "Izpolnite podrobnosti filtra." + } + }, + "title": "GeoNet NZ vulkan" + } +} \ 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 new file mode 100644 index 00000000000..0f74841fd7b --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/.translations/zh-Hant.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "identifier_exists": "\u5ea7\u6a19\u5df2\u8a3b\u518a" + }, + "step": { + "user": { + "data": { + "radius": "\u534a\u5f91" + }, + "title": "\u586b\u5beb\u904e\u6ffe\u5668\u8cc7\u8a0a\u3002" + } + }, + "title": "\u7d10\u897f\u862d GeoNet \u706b\u5c71\u9810\u8b66" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/__init__.py b/homeassistant/components/geonetnz_volcano/__init__.py new file mode 100644 index 00000000000..e24de7fdc5d --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/__init__.py @@ -0,0 +1,205 @@ +"""The GeoNet NZ Volcano integration.""" +import asyncio +from datetime import datetime, timedelta +import logging +from typing import Optional + +from aio_geojson_geonetnz_volcano import GeonetnzVolcanoFeedManager +import voluptuous as vol + +from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.const import ( + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_RADIUS, + CONF_SCAN_INTERVAL, + CONF_UNIT_SYSTEM, + CONF_UNIT_SYSTEM_IMPERIAL, + LENGTH_MILES, +) +from homeassistant.core import callback +from homeassistant.helpers import aiohttp_client, config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.util.unit_system import METRIC_SYSTEM + +from .config_flow import configured_instances +from .const import ( + DEFAULT_RADIUS, + DEFAULT_SCAN_INTERVAL, + DOMAIN, + FEED, + SIGNAL_NEW_SENSOR, + SIGNAL_UPDATE_ENTITY, +) + +_LOGGER = logging.getLogger(__name__) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Optional(CONF_LATITUDE): cv.latitude, + vol.Optional(CONF_LONGITUDE): cv.longitude, + vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS): vol.Coerce(float), + vol.Optional( + CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL + ): cv.time_period, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass, config): + """Set up the GeoNet NZ Volcano component.""" + if DOMAIN not in config: + return True + + conf = config[DOMAIN] + + latitude = conf.get(CONF_LATITUDE, hass.config.latitude) + longitude = conf.get(CONF_LONGITUDE, hass.config.longitude) + scan_interval = conf[CONF_SCAN_INTERVAL] + + identifier = f"{latitude}, {longitude}" + if identifier in configured_instances(hass): + return True + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + CONF_LATITUDE: latitude, + CONF_LONGITUDE: longitude, + CONF_RADIUS: conf[CONF_RADIUS], + CONF_SCAN_INTERVAL: scan_interval, + }, + ) + ) + + return True + + +async def async_setup_entry(hass, config_entry): + """Set up the GeoNet NZ Volcano component as config entry.""" + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN].setdefault(FEED, {}) + + radius = config_entry.data[CONF_RADIUS] + unit_system = config_entry.data[CONF_UNIT_SYSTEM] + if unit_system == CONF_UNIT_SYSTEM_IMPERIAL: + radius = METRIC_SYSTEM.length(radius, LENGTH_MILES) + # Create feed entity manager for all platforms. + manager = GeonetnzVolcanoFeedEntityManager(hass, config_entry, radius, unit_system) + hass.data[DOMAIN][FEED][config_entry.entry_id] = manager + _LOGGER.debug("Feed entity manager added for %s", config_entry.entry_id) + await manager.async_init() + return True + + +async def async_unload_entry(hass, config_entry): + """Unload an GeoNet NZ Volcano component config entry.""" + manager = hass.data[DOMAIN][FEED].pop(config_entry.entry_id) + await manager.async_stop() + await asyncio.wait( + [hass.config_entries.async_forward_entry_unload(config_entry, "sensor")] + ) + return True + + +class GeonetnzVolcanoFeedEntityManager: + """Feed Entity Manager for GeoNet NZ Volcano feed.""" + + def __init__(self, hass, config_entry, radius_in_km, unit_system): + """Initialize the Feed Entity Manager.""" + self._hass = hass + self._config_entry = config_entry + coordinates = ( + config_entry.data[CONF_LATITUDE], + config_entry.data[CONF_LONGITUDE], + ) + websession = aiohttp_client.async_get_clientsession(hass) + self._feed_manager = GeonetnzVolcanoFeedManager( + websession, + self._generate_entity, + self._update_entity, + self._remove_entity, + coordinates, + filter_radius=radius_in_km, + ) + self._config_entry_id = config_entry.entry_id + self._scan_interval = timedelta(seconds=config_entry.data[CONF_SCAN_INTERVAL]) + self._unit_system = unit_system + self._track_time_remove_callback = None + self.listeners = [] + + async def async_init(self): + """Schedule initial and regular updates based on configured time interval.""" + + self._hass.async_create_task( + self._hass.config_entries.async_forward_entry_setup( + self._config_entry, "sensor" + ) + ) + + async def update(event_time): + """Update.""" + await self.async_update() + + # Trigger updates at regular intervals. + self._track_time_remove_callback = async_track_time_interval( + self._hass, update, self._scan_interval + ) + + _LOGGER.debug("Feed entity manager initialized") + + async def async_update(self): + """Refresh data.""" + await self._feed_manager.update() + _LOGGER.debug("Feed entity manager updated") + + async def async_stop(self): + """Stop this feed entity manager from refreshing.""" + for unsub_dispatcher in self.listeners: + unsub_dispatcher() + self.listeners = [] + if self._track_time_remove_callback: + self._track_time_remove_callback() + _LOGGER.debug("Feed entity manager stopped") + + @callback + def async_event_new_entity(self): + """Return manager specific event to signal new entity.""" + return SIGNAL_NEW_SENSOR.format(self._config_entry_id) + + 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]: + """Return the last update of this feed.""" + return self._feed_manager.last_update + + def last_update_successful(self) -> Optional[datetime]: + """Return the last successful update of this feed.""" + return self._feed_manager.last_update_successful + + async def _generate_entity(self, external_id): + """Generate new entity.""" + async_dispatcher_send( + self._hass, + self.async_event_new_entity(), + self, + external_id, + self._unit_system, + ) + + async def _update_entity(self, external_id): + """Update entity.""" + async_dispatcher_send(self._hass, SIGNAL_UPDATE_ENTITY.format(external_id)) + + async def _remove_entity(self, external_id): + """Ignore removing entity.""" diff --git a/homeassistant/components/geonetnz_volcano/config_flow.py b/homeassistant/components/geonetnz_volcano/config_flow.py new file mode 100644 index 00000000000..7c079c432dd --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/config_flow.py @@ -0,0 +1,74 @@ +"""Config flow to configure the GeoNet NZ Volcano integration.""" +import logging + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import ( + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_RADIUS, + CONF_SCAN_INTERVAL, + CONF_UNIT_SYSTEM, + CONF_UNIT_SYSTEM_IMPERIAL, + CONF_UNIT_SYSTEM_METRIC, +) +from homeassistant.core import callback +from homeassistant.helpers import config_validation as cv + +from .const import DEFAULT_RADIUS, DEFAULT_SCAN_INTERVAL, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +@callback +def configured_instances(hass): + """Return a set of configured GeoNet NZ Volcano instances.""" + return set( + f"{entry.data[CONF_LATITUDE]}, {entry.data[CONF_LONGITUDE]}" + for entry in hass.config_entries.async_entries(DOMAIN) + ) + + +class GeonetnzVolcanoFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a GeoNet NZ Volcano config flow.""" + + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + async def _show_form(self, errors=None): + """Show the form to the user.""" + data_schema = vol.Schema( + {vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS): cv.positive_int} + ) + + return self.async_show_form( + step_id="user", data_schema=data_schema, errors=errors or {} + ) + + 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.""" + if not user_input: + return await self._show_form() + + latitude = user_input.get(CONF_LATITUDE, self.hass.config.latitude) + user_input[CONF_LATITUDE] = latitude + longitude = user_input.get(CONF_LONGITUDE, self.hass.config.longitude) + user_input[CONF_LONGITUDE] = longitude + + identifier = f"{user_input[CONF_LATITUDE]}, {user_input[CONF_LONGITUDE]}" + if identifier in configured_instances(self.hass): + return await self._show_form({"base": "identifier_exists"}) + + if self.hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL: + user_input[CONF_UNIT_SYSTEM] = CONF_UNIT_SYSTEM_IMPERIAL + else: + user_input[CONF_UNIT_SYSTEM] = CONF_UNIT_SYSTEM_METRIC + + scan_interval = user_input.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) + user_input[CONF_SCAN_INTERVAL] = scan_interval.seconds + + return self.async_create_entry(title=identifier, data=user_input) diff --git a/homeassistant/components/geonetnz_volcano/const.py b/homeassistant/components/geonetnz_volcano/const.py new file mode 100644 index 00000000000..7bc15d3a6a1 --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/const.py @@ -0,0 +1,19 @@ +"""Define constants for the GeoNet NZ Volcano integration.""" +from datetime import timedelta + +DOMAIN = "geonetnz_volcano" + +FEED = "feed" + +ATTR_ACTIVITY = "activity" +ATTR_DISTANCE = "distance" +ATTR_EXTERNAL_ID = "external_id" +ATTR_HAZARDS = "hazards" + +# Icon alias "mdi:mountain" not working. +DEFAULT_ICON = "mdi:image-filter-hdr" +DEFAULT_RADIUS = 50.0 +DEFAULT_SCAN_INTERVAL = timedelta(minutes=5) + +SIGNAL_NEW_SENSOR = "geonetnz_volcano_new_sensor_{}" +SIGNAL_UPDATE_ENTITY = "geonetnz_volcano_update_{}" diff --git a/homeassistant/components/geonetnz_volcano/manifest.json b/homeassistant/components/geonetnz_volcano/manifest.json new file mode 100644 index 00000000000..a80ebdcff65 --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/manifest.json @@ -0,0 +1,13 @@ +{ + "domain": "geonetnz_volcano", + "name": "GeoNet NZ Volcano", + "config_flow": true, + "documentation": "https://www.home-assistant.io/components/geonetnz_volcano", + "requirements": [ + "aio_geojson_geonetnz_volcano==0.5" + ], + "dependencies": [], + "codeowners": [ + "@exxamalte" + ] +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/sensor.py b/homeassistant/components/geonetnz_volcano/sensor.py new file mode 100644 index 00000000000..f87ea88fc1c --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/sensor.py @@ -0,0 +1,169 @@ +"""Feed Entity Manager Sensor support for GeoNet NZ Volcano Feeds.""" +import logging +from typing import Optional + +from homeassistant.const import ( + ATTR_ATTRIBUTION, + ATTR_LATITUDE, + ATTR_LONGITUDE, + CONF_UNIT_SYSTEM_IMPERIAL, + LENGTH_KILOMETERS, +) +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 + +from .const import ( + ATTR_ACTIVITY, + ATTR_DISTANCE, + ATTR_EXTERNAL_ID, + ATTR_HAZARDS, + DEFAULT_ICON, + DOMAIN, + FEED, + SIGNAL_UPDATE_ENTITY, +) + +_LOGGER = logging.getLogger(__name__) + +ATTR_LAST_UPDATE = "feed_last_update" +ATTR_LAST_UPDATE_SUCCESSFUL = "feed_last_update_successful" + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the GeoNet NZ Volcano Feed platform.""" + manager = hass.data[DOMAIN][FEED][entry.entry_id] + + @callback + def async_add_sensor(feed_manager, external_id, unit_system): + """Add sensor entity from feed.""" + new_entity = GeonetnzVolcanoSensor( + entry.entry_id, feed_manager, external_id, unit_system + ) + _LOGGER.debug("Adding sensor %s", new_entity) + async_add_entities([new_entity], True) + + manager.listeners.append( + async_dispatcher_connect( + hass, manager.async_event_new_entity(), async_add_sensor + ) + ) + hass.async_create_task(manager.async_update()) + _LOGGER.debug("Sensor setup done") + + +class GeonetnzVolcanoSensor(Entity): + """This represents an external event with GeoNet NZ Volcano feed data.""" + + def __init__(self, config_entry_id, feed_manager, external_id, unit_system): + """Initialize entity with data from feed entry.""" + self._config_entry_id = config_entry_id + self._feed_manager = feed_manager + self._external_id = external_id + self._unit_system = unit_system + self._title = None + self._distance = None + self._latitude = None + self._longitude = None + self._attribution = None + self._alert_level = None + self._activity = None + self._hazards = None + self._feed_last_update = None + self._feed_last_update_successful = None + self._remove_signal_update = None + + async def async_added_to_hass(self): + """Call when entity is added to hass.""" + self._remove_signal_update = async_dispatcher_connect( + self.hass, + SIGNAL_UPDATE_ENTITY.format(self._external_id), + self._update_callback, + ) + + async def async_will_remove_from_hass(self) -> None: + """Call when entity will be removed from hass.""" + if self._remove_signal_update: + self._remove_signal_update() + + @callback + def _update_callback(self): + """Call update method.""" + self.async_schedule_update_ha_state(True) + + @property + def should_poll(self): + """No polling needed for GeoNet NZ Volcano feed location events.""" + return False + + async def async_update(self): + """Update this entity from the data held in the feed manager.""" + _LOGGER.debug("Updating %s", self._external_id) + feed_entry = self._feed_manager.get_entry(self._external_id) + last_update = self._feed_manager.last_update() + last_update_successful = self._feed_manager.last_update_successful() + if feed_entry: + self._update_from_feed(feed_entry, last_update, last_update_successful) + + def _update_from_feed(self, feed_entry, last_update, last_update_successful): + """Update the internal state from the provided feed entry.""" + self._title = feed_entry.title + # Convert distance if not metric system. + if self._unit_system == CONF_UNIT_SYSTEM_IMPERIAL: + self._distance = round( + IMPERIAL_SYSTEM.length(feed_entry.distance_to_home, LENGTH_KILOMETERS), + 1, + ) + else: + self._distance = round(feed_entry.distance_to_home, 1) + self._latitude = round(feed_entry.coordinates[0], 5) + self._longitude = round(feed_entry.coordinates[1], 5) + self._attribution = feed_entry.attribution + self._alert_level = feed_entry.alert_level + self._activity = feed_entry.activity + self._hazards = feed_entry.hazards + self._feed_last_update = dt.as_utc(last_update) if last_update else None + self._feed_last_update_successful = ( + dt.as_utc(last_update_successful) if last_update_successful else None + ) + + @property + def state(self): + """Return the state of the sensor.""" + return self._alert_level + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return DEFAULT_ICON + + @property + def name(self) -> Optional[str]: + """Return the name of the entity.""" + return f"Volcano {self._title}" + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return "alert level" + + @property + def device_state_attributes(self): + """Return the device state attributes.""" + attributes = {} + for key, value in ( + (ATTR_EXTERNAL_ID, self._external_id), + (ATTR_ATTRIBUTION, self._attribution), + (ATTR_ACTIVITY, self._activity), + (ATTR_HAZARDS, self._hazards), + (ATTR_LONGITUDE, self._longitude), + (ATTR_LATITUDE, self._latitude), + (ATTR_DISTANCE, self._distance), + (ATTR_LAST_UPDATE, self._feed_last_update), + (ATTR_LAST_UPDATE_SUCCESSFUL, self._feed_last_update_successful), + ): + if value or isinstance(value, bool): + attributes[key] = value + return attributes diff --git a/homeassistant/components/geonetnz_volcano/strings.json b/homeassistant/components/geonetnz_volcano/strings.json new file mode 100644 index 00000000000..93ec8603d03 --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/strings.json @@ -0,0 +1,16 @@ +{ + "config": { + "title": "GeoNet NZ Volcano", + "step": { + "user": { + "title": "Fill in your filter details.", + "data": { + "radius": "Radius" + } + } + }, + "error": { + "identifier_exists": "Location already registered" + } + } +} diff --git a/homeassistant/components/gios/.translations/ca.json b/homeassistant/components/gios/.translations/ca.json new file mode 100644 index 00000000000..80fedcafdd9 --- /dev/null +++ b/homeassistant/components/gios/.translations/ca.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "cannot_connect": "No s'ha pogut connectar al servidor de GIO\u015a.", + "invalid_sensors_data": "Les dades dels sensors d'aquesta estaci\u00f3 de mesura s\u00f3n inv\u00e0lides.", + "wrong_station_id": "L'ID de l'estaci\u00f3 de mesura \u00e9s incorrecte." + }, + "step": { + "user": { + "data": { + "name": "Nom de la integraci\u00f3", + "station_id": "ID de l'estaci\u00f3 de mesura" + }, + "description": "Integraci\u00f3 de mesura de qualitat de l\u2019aire GIO\u015a (Polish Chief Inspectorate Of Environmental Protection). Si necessites ajuda amb la configuraci\u00f3, fes un cop d'ull a: https://www.home-assistant.io/integrations/gios", + "title": "GIO\u015a (Polish Chief Inspectorate Of Environmental Protection)" + } + }, + "title": "GIO\u015a" + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/.translations/da.json b/homeassistant/components/gios/.translations/da.json new file mode 100644 index 00000000000..b4855da7951 --- /dev/null +++ b/homeassistant/components/gios/.translations/da.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "cannot_connect": "Kan ikke oprette forbindelse til GIO\u015a-serveren.", + "invalid_sensors_data": "Ugyldige sensordata for denne m\u00e5lestation.", + "wrong_station_id": "M\u00e5lestationens ID er ikke korrekt." + }, + "step": { + "user": { + "data": { + "name": "Navn p\u00e5 integrationen", + "station_id": "ID for m\u00e5lestationen" + }, + "description": "Ops\u00e6t GIO\u015a (polsk inspektorat for milj\u00f8beskyttelse) luftkvalitet-integration. Hvis du har brug for hj\u00e6lp med konfigurationen, kig her: https://www.home-assistant.io/integrations/gios", + "title": "GIO\u015a (Polish Chief Inspectorate Of Environmental Protection)" + } + }, + "title": "GIO\u015a" + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/.translations/en.json b/homeassistant/components/gios/.translations/en.json new file mode 100644 index 00000000000..2ff0d8c60f3 --- /dev/null +++ b/homeassistant/components/gios/.translations/en.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "cannot_connect": "Cannot connect to the GIO\u015a server.", + "invalid_sensors_data": "Invalid sensors' data for this measuring station.", + "wrong_station_id": "ID of the measuring station is not correct." + }, + "step": { + "user": { + "data": { + "name": "Name of the integration", + "station_id": "ID of the measuring station" + }, + "description": "Set up GIO\u015a (Polish Chief Inspectorate Of Environmental Protection) air quality integration. If you need help with the configuration have a look here: https://www.home-assistant.io/integrations/gios", + "title": "GIO\u015a (Polish Chief Inspectorate Of Environmental Protection)" + } + }, + "title": "GIO\u015a" + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/.translations/ko.json b/homeassistant/components/gios/.translations/ko.json new file mode 100644 index 00000000000..6fb37205502 --- /dev/null +++ b/homeassistant/components/gios/.translations/ko.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "cannot_connect": "GIO\u015a \uc11c\ubc84\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\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", + "station_id": "\uce21\uc815 \uc2a4\ud14c\uc774\uc158\uc758 ID" + }, + "description": "GIO\u015a (\ud3f4\ub780\ub4dc \ud658\uacbd \ubcf4\ud638\uccad) \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)" + } + }, + "title": "GIO\u015a" + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/.translations/ru.json b/homeassistant/components/gios/.translations/ru.json new file mode 100644 index 00000000000..f45ad965550 --- /dev/null +++ b/homeassistant/components/gios/.translations/ru.json @@ -0,0 +1,20 @@ +{ + "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 \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 GIO\u015a.", + "invalid_sensors_data": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u043e\u0432 \u0434\u043b\u044f \u044d\u0442\u043e\u0439 \u0438\u0437\u043c\u0435\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u0441\u0442\u0430\u043d\u0446\u0438\u0438.", + "wrong_station_id": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 ID \u0438\u0437\u043c\u0435\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u0441\u0442\u0430\u043d\u0446\u0438\u0438." + }, + "step": { + "user": { + "data": { + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "station_id": "ID \u0438\u0437\u043c\u0435\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u0441\u0442\u0430\u043d\u0446\u0438\u0438" + }, + "description": "\u0418\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u043e \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0432\u043e\u0437\u0434\u0443\u0445\u0430 \u043e\u0442 \u041f\u043e\u043b\u044c\u0441\u043a\u043e\u0439 \u0438\u043d\u0441\u043f\u0435\u043a\u0446\u0438\u0438 \u043f\u043e \u043e\u0445\u0440\u0430\u043d\u0435 \u043e\u043a\u0440\u0443\u0436\u0430\u044e\u0449\u0435\u0439 \u0441\u0440\u0435\u0434\u044b (GIO\u015a). \u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u0435\u0439 \u043f\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438: https://www.home-assistant.io/integrations/gios.", + "title": "GIO\u015a (\u041f\u043e\u043b\u044c\u0441\u043a\u0430\u044f \u0438\u043d\u0441\u043f\u0435\u043a\u0446\u0438\u044f \u043f\u043e \u043e\u0445\u0440\u0430\u043d\u0435 \u043e\u043a\u0440\u0443\u0436\u0430\u044e\u0449\u0435\u0439 \u0441\u0440\u0435\u0434\u044b)" + } + }, + "title": "GIO\u015a" + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/__init__.py b/homeassistant/components/gios/__init__.py new file mode 100644 index 00000000000..981de6395de --- /dev/null +++ b/homeassistant/components/gios/__init__.py @@ -0,0 +1,78 @@ +"""The GIOS component.""" +import asyncio +import logging + +from aiohttp.client_exceptions import ClientConnectorError +from async_timeout import timeout +from gios import ApiError, Gios, NoStationError + +from homeassistant.core import Config, HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.util import Throttle + +from .const import CONF_STATION_ID, DATA_CLIENT, DEFAULT_SCAN_INTERVAL, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistant, config: Config) -> bool: + """Set up configured GIOS.""" + hass.data[DOMAIN] = {} + hass.data[DOMAIN][DATA_CLIENT] = {} + return True + + +async def async_setup_entry(hass, config_entry): + """Set up GIOS as config entry.""" + station_id = config_entry.data[CONF_STATION_ID] + _LOGGER.debug("Using station_id: %s", station_id) + + websession = async_get_clientsession(hass) + + gios = GiosData(websession, station_id) + + await gios.async_update() + + hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = gios + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, "air_quality") + ) + return True + + +async def async_unload_entry(hass, config_entry): + """Unload a config entry.""" + hass.data[DOMAIN][DATA_CLIENT].pop(config_entry.entry_id) + await hass.config_entries.async_forward_entry_unload(config_entry, "air_quality") + return True + + +class GiosData: + """Define an object to hold GIOS data.""" + + def __init__(self, session, station_id): + """Initialize.""" + self._gios = Gios(station_id, session) + self.station_id = station_id + self.sensors = {} + self.latitude = None + self.longitude = None + self.station_name = None + self.available = True + + @Throttle(DEFAULT_SCAN_INTERVAL) + async def async_update(self): + """Update GIOS data.""" + try: + with timeout(30): + await self._gios.update() + except asyncio.TimeoutError: + _LOGGER.error("Asyncio Timeout Error") + except (ApiError, NoStationError, ClientConnectorError) as error: + _LOGGER.error("GIOS data update failed: %s", error) + self.available = self._gios.available + self.latitude = self._gios.latitude + self.longitude = self._gios.longitude + self.station_name = self._gios.station_name + self.sensors = self._gios.data diff --git a/homeassistant/components/gios/air_quality.py b/homeassistant/components/gios/air_quality.py new file mode 100644 index 00000000000..f7285c8cc5a --- /dev/null +++ b/homeassistant/components/gios/air_quality.py @@ -0,0 +1,158 @@ +"""Support for the GIOS service.""" +from homeassistant.components.air_quality import ( + ATTR_CO, + ATTR_NO2, + ATTR_OZONE, + ATTR_PM_2_5, + ATTR_PM_10, + ATTR_SO2, + AirQualityEntity, +) +from homeassistant.const import CONF_NAME + +from .const import ATTR_STATION, DATA_CLIENT, DEFAULT_SCAN_INTERVAL, DOMAIN, ICONS_MAP + +ATTRIBUTION = "Data provided by GIOŚ" +SCAN_INTERVAL = DEFAULT_SCAN_INTERVAL + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Add a GIOS entities from a config_entry.""" + name = config_entry.data[CONF_NAME] + + data = hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] + + async_add_entities([GiosAirQuality(data, name)], True) + + +def round_state(func): + """Round state.""" + + def _decorator(self): + res = func(self) + if isinstance(res, float): + return round(res) + return res + + return _decorator + + +class GiosAirQuality(AirQualityEntity): + """Define an GIOS sensor.""" + + def __init__(self, gios, name): + """Initialize.""" + self.gios = gios + self._name = name + self._aqi = None + self._co = None + self._no2 = None + self._o3 = None + self._pm_2_5 = None + self._pm_10 = None + self._so2 = None + self._attrs = {} + + @property + def name(self): + """Return the name.""" + return self._name + + @property + def icon(self): + """Return the icon.""" + if self._aqi in ICONS_MAP: + return ICONS_MAP[self._aqi] + return "mdi:blur" + + @property + def air_quality_index(self): + """Return the air quality index.""" + return self._aqi + + @property + @round_state + def particulate_matter_2_5(self): + """Return the particulate matter 2.5 level.""" + return self._pm_2_5 + + @property + @round_state + def particulate_matter_10(self): + """Return the particulate matter 10 level.""" + return self._pm_10 + + @property + @round_state + def ozone(self): + """Return the O3 (ozone) level.""" + return self._o3 + + @property + @round_state + def carbon_monoxide(self): + """Return the CO (carbon monoxide) level.""" + return self._co + + @property + @round_state + def sulphur_dioxide(self): + """Return the SO2 (sulphur dioxide) level.""" + return self._so2 + + @property + @round_state + def nitrogen_dioxide(self): + """Return the NO2 (nitrogen dioxide) level.""" + return self._no2 + + @property + def attribution(self): + """Return the attribution.""" + return ATTRIBUTION + + @property + def unique_id(self): + """Return a unique_id for this entity.""" + return self.gios.station_id + + @property + def available(self): + """Return True if entity is available.""" + return self.gios.available + + @property + def device_state_attributes(self): + """Return the state attributes.""" + self._attrs[ATTR_STATION] = self.gios.station_name + return self._attrs + + async def async_update(self): + """Get the data from GIOS.""" + await self.gios.async_update() + + if self.gios.available: + # Different measuring stations have different sets of sensors. We don't know + # what data we will get. + if "AQI" in self.gios.sensors: + self._aqi = self.gios.sensors["AQI"]["value"] + if "CO" in self.gios.sensors: + self._co = self.gios.sensors["CO"]["value"] + self._attrs[f"{ATTR_CO}_index"] = self.gios.sensors["CO"]["index"] + if "NO2" in self.gios.sensors: + self._no2 = self.gios.sensors["NO2"]["value"] + self._attrs[f"{ATTR_NO2}_index"] = self.gios.sensors["NO2"]["index"] + if "O3" in self.gios.sensors: + self._o3 = self.gios.sensors["O3"]["value"] + self._attrs[f"{ATTR_OZONE}_index"] = self.gios.sensors["O3"]["index"] + if "PM2.5" in self.gios.sensors: + self._pm_2_5 = self.gios.sensors["PM2.5"]["value"] + self._attrs[f"{ATTR_PM_2_5}_index"] = self.gios.sensors["PM2.5"][ + "index" + ] + if "PM10" in self.gios.sensors: + self._pm_10 = self.gios.sensors["PM10"]["value"] + self._attrs[f"{ATTR_PM_10}_index"] = self.gios.sensors["PM10"]["index"] + if "SO2" in self.gios.sensors: + self._so2 = self.gios.sensors["SO2"]["value"] + self._attrs[f"{ATTR_SO2}_index"] = self.gios.sensors["SO2"]["index"] diff --git a/homeassistant/components/gios/config_flow.py b/homeassistant/components/gios/config_flow.py new file mode 100644 index 00000000000..368d610c226 --- /dev/null +++ b/homeassistant/components/gios/config_flow.py @@ -0,0 +1,65 @@ +"""Adds config flow for GIOS.""" +import asyncio + +from aiohttp.client_exceptions import ClientConnectorError +from async_timeout import timeout +from gios import ApiError, Gios, NoStationError +import voluptuous as vol + +from homeassistant import config_entries, exceptions +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 + +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_STATION_ID): int, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): str, + } +) + + +class GiosFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Config flow for GIOS.""" + + 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: + try: + await self.async_set_unique_id( + user_input[CONF_STATION_ID], raise_on_progress=False + ) + self._abort_if_unique_id_configured() + + websession = async_get_clientsession(self.hass) + + with timeout(30): + gios = Gios(user_input[CONF_STATION_ID], websession) + await gios.update() + + if not gios.available: + raise InvalidSensorsData() + + return self.async_create_entry( + title=user_input[CONF_STATION_ID], data=user_input, + ) + except (ApiError, ClientConnectorError, asyncio.TimeoutError): + errors["base"] = "cannot_connect" + except NoStationError: + errors[CONF_STATION_ID] = "wrong_station_id" + except InvalidSensorsData: + errors[CONF_STATION_ID] = "invalid_sensors_data" + + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) + + +class InvalidSensorsData(exceptions.HomeAssistantError): + """Error to indicate invalid sensors data.""" diff --git a/homeassistant/components/gios/const.py b/homeassistant/components/gios/const.py new file mode 100644 index 00000000000..3588b5e8dfc --- /dev/null +++ b/homeassistant/components/gios/const.py @@ -0,0 +1,25 @@ +"""Constants for GIOS integration.""" +from datetime import timedelta + +ATTR_NAME = "name" +ATTR_STATION = "station" +CONF_STATION_ID = "station_id" +DATA_CLIENT = "client" +DEFAULT_NAME = "GIOŚ" +# Term of service GIOŚ allow downloading data no more than twice an hour. +DEFAULT_SCAN_INTERVAL = timedelta(minutes=30) +DOMAIN = "gios" + +AQI_GOOD = "dobry" +AQI_MODERATE = "umiarkowany" +AQI_POOR = "dostateczny" +AQI_VERY_GOOD = "bardzo dobry" +AQI_VERY_POOR = "zły" + +ICONS_MAP = { + AQI_VERY_GOOD: "mdi:emoticon-excited", + AQI_GOOD: "mdi:emoticon-happy", + AQI_MODERATE: "mdi:emoticon-neutral", + AQI_POOR: "mdi:emoticon-sad", + AQI_VERY_POOR: "mdi:emoticon-dead", +} diff --git a/homeassistant/components/gios/manifest.json b/homeassistant/components/gios/manifest.json new file mode 100644 index 00000000000..b3d125d8ab6 --- /dev/null +++ b/homeassistant/components/gios/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "gios", + "name": "GIOŚ", + "documentation": "https://www.home-assistant.io/integrations/gios", + "dependencies": [], + "codeowners": ["@bieniu"], + "requirements": ["gios==0.0.3"], + "config_flow": true +} diff --git a/homeassistant/components/gios/strings.json b/homeassistant/components/gios/strings.json new file mode 100644 index 00000000000..cc05a471b4a --- /dev/null +++ b/homeassistant/components/gios/strings.json @@ -0,0 +1,20 @@ +{ + "config": { + "title": "GIOŚ", + "step": { + "user": { + "title": "GIOŚ (Polish Chief Inspectorate Of Environmental Protection)", + "description": "Set up GIOŚ (Polish Chief Inspectorate Of Environmental Protection) air quality integration. If you need help with the configuration have a look here: https://www.home-assistant.io/integrations/gios", + "data": { + "name": "Name of the integration", + "station_id": "ID of the measuring station" + } + } + }, + "error": { + "wrong_station_id": "ID of the measuring station is not correct.", + "invalid_sensors_data": "Invalid sensors' data for this measuring station.", + "cannot_connect": "Cannot connect to the GIOŚ server." + } + } +} diff --git a/homeassistant/components/github/sensor.py b/homeassistant/components/github/sensor.py index 5e8200b41ab..c77cf7930b8 100644 --- a/homeassistant/components/github/sensor.py +++ b/homeassistant/components/github/sensor.py @@ -1,6 +1,7 @@ """Support for GitHub.""" from datetime import timedelta import logging + import github import voluptuous as vol diff --git a/homeassistant/components/gitter/sensor.py b/homeassistant/components/gitter/sensor.py index f124849a193..4f1eeca7d71 100644 --- a/homeassistant/components/gitter/sensor.py +++ b/homeassistant/components/gitter/sensor.py @@ -1,6 +1,8 @@ """Support for displaying details about a Gitter.im chat room.""" import logging +from gitterpy.client import GitterClient +from gitterpy.errors import GitterRoomError, GitterTokenError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -30,8 +32,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Gitter sensor.""" - from gitterpy.client import GitterClient - from gitterpy.errors import GitterTokenError name = config.get(CONF_NAME) api_key = config.get(CONF_API_KEY) @@ -91,7 +91,6 @@ class GitterSensor(Entity): def update(self): """Get the latest data and updates the state.""" - from gitterpy.errors import GitterRoomError try: data = self._data.user.unread_items(self._room) diff --git a/homeassistant/components/glances/.translations/nn.json b/homeassistant/components/glances/.translations/nn.json new file mode 100644 index 00000000000..2c9acc227bd --- /dev/null +++ b/homeassistant/components/glances/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Glances" + } +} \ No newline at end of file diff --git a/homeassistant/components/gogogate2/cover.py b/homeassistant/components/gogogate2/cover.py index 26aecee2504..fcb0182ec0e 100644 --- a/homeassistant/components/gogogate2/cover.py +++ b/homeassistant/components/gogogate2/cover.py @@ -1,15 +1,16 @@ """Support for Gogogate2 garage Doors.""" import logging +from pygogogate2 import Gogogate2API as pygogogate2 import voluptuous as vol -from homeassistant.components.cover import CoverDevice, SUPPORT_OPEN, SUPPORT_CLOSE +from homeassistant.components.cover import SUPPORT_CLOSE, SUPPORT_OPEN, CoverDevice from homeassistant.const import ( - CONF_USERNAME, - CONF_PASSWORD, - STATE_CLOSED, CONF_IP_ADDRESS, CONF_NAME, + CONF_PASSWORD, + CONF_USERNAME, + STATE_CLOSED, ) import homeassistant.helpers.config_validation as cv @@ -32,7 +33,6 @@ COVER_SCHEMA = vol.Schema( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Gogogate2 component.""" - from pygogogate2 import Gogogate2API as pygogogate2 ip_address = config.get(CONF_IP_ADDRESS) name = config.get(CONF_NAME) diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index 9cb9be0fa4f..0e7ccd33b33 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -1,23 +1,22 @@ """Support for Google - Calendar Event Devices.""" -from datetime import timedelta, datetime +from datetime import datetime, timedelta import logging import os -import yaml +from googleapiclient import discovery as google_discovery import httplib2 from oauth2client.client import ( - OAuth2WebServerFlow, - OAuth2DeviceCodeError, FlowExchangeError, + OAuth2DeviceCodeError, + OAuth2WebServerFlow, ) from oauth2client.file import Storage -from googleapiclient import discovery as google_discovery - import voluptuous as vol from voluptuous.error import Error as VoluptuousError +import yaml -import homeassistant.helpers.config_validation as cv from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.event import track_time_change from homeassistant.util import convert, dt diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py index ebf906b6f2a..107003db583 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -1,44 +1,38 @@ """Support for Actions on Google Assistant Smart Home Control.""" -import asyncio import logging -from typing import Dict, Any - -import aiohttp -import async_timeout +from typing import Any, Dict import voluptuous as vol # Typing imports -from homeassistant.core import HomeAssistant, ServiceCall - from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ( - DOMAIN, - CONF_PROJECT_ID, - CONF_EXPOSE_BY_DEFAULT, - DEFAULT_EXPOSE_BY_DEFAULT, - CONF_EXPOSED_DOMAINS, - DEFAULT_EXPOSED_DOMAINS, + CONF_ALIASES, + CONF_ALLOW_UNLOCK, CONF_API_KEY, - SERVICE_REQUEST_SYNC, - REQUEST_SYNC_BASE_URL, + CONF_CLIENT_EMAIL, CONF_ENTITY_CONFIG, CONF_EXPOSE, - CONF_ALIASES, + CONF_EXPOSE_BY_DEFAULT, + CONF_EXPOSED_DOMAINS, + CONF_PRIVATE_KEY, + CONF_PROJECT_ID, CONF_REPORT_STATE, CONF_ROOM_HINT, - CONF_ALLOW_UNLOCK, CONF_SECURE_DEVICES_PIN, CONF_SERVICE_ACCOUNT, - CONF_CLIENT_EMAIL, - CONF_PRIVATE_KEY, + DEFAULT_EXPOSE_BY_DEFAULT, + DEFAULT_EXPOSED_DOMAINS, + DOMAIN, + SERVICE_REQUEST_SYNC, ) -from .const import EVENT_COMMAND_RECEIVED, EVENT_SYNC_RECEIVED # noqa: F401 from .const import EVENT_QUERY_RECEIVED # noqa: F401 -from .http import async_register_http +from .http import GoogleAssistantView, GoogleConfig + +from .const import EVENT_COMMAND_RECEIVED, EVENT_SYNC_RECEIVED # noqa: F401, isort:skip _LOGGER = logging.getLogger(__name__) @@ -99,37 +93,29 @@ CONFIG_SCHEMA = vol.Schema({DOMAIN: GOOGLE_ASSISTANT_SCHEMA}, extra=vol.ALLOW_EX async def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): """Activate Google Actions component.""" config = yaml_config.get(DOMAIN, {}) - api_key = config.get(CONF_API_KEY) - async_register_http(hass, config) + + google_config = GoogleConfig(hass, config) + await google_config.async_initialize() + + hass.http.register_view(GoogleAssistantView(google_config)) + + if google_config.should_report_state: + google_config.async_enable_report_state() async def request_sync_service_handler(call: ServiceCall): """Handle request sync service calls.""" - websession = async_get_clientsession(hass) - try: - with async_timeout.timeout(15): - agent_user_id = call.data.get("agent_user_id") or call.context.user_id + agent_user_id = call.data.get("agent_user_id") or call.context.user_id - if agent_user_id is None: - _LOGGER.warning( - "No agent_user_id supplied for request_sync. Call as a user or pass in user id as agent_user_id." - ) - return + if agent_user_id is None: + _LOGGER.warning( + "No agent_user_id supplied for request_sync. Call as a user or pass in user id as agent_user_id." + ) + return - res = await websession.post( - REQUEST_SYNC_BASE_URL, - params={"key": api_key}, - json={"agent_user_id": agent_user_id}, - ) - _LOGGER.info("Submitted request_sync request to Google") - res.raise_for_status() - except aiohttp.ClientResponseError: - body = await res.read() - _LOGGER.error("request_sync request failed: %d %s", res.status, body) - except (asyncio.TimeoutError, aiohttp.ClientError): - _LOGGER.error("Could not contact Google for request_sync") + await google_config.async_sync_entities(agent_user_id) - # Register service only if api key is provided - if api_key is not None: + # Register service only if key is provided + if CONF_API_KEY in config or CONF_SERVICE_ACCOUNT in config: hass.services.async_register( DOMAIN, SERVICE_REQUEST_SYNC, request_sync_service_handler ) diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index 03253e244fe..dcb87d1d93d 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -1,5 +1,6 @@ """Constants for Google Assistant.""" from homeassistant.components import ( + alarm_control_panel, binary_sensor, camera, climate, @@ -15,7 +16,6 @@ from homeassistant.components import ( sensor, switch, vacuum, - alarm_control_panel, ) DOMAIN = "google_assistant" @@ -135,8 +135,11 @@ DEVICE_CLASS_TO_GOOGLE_TYPES = { (media_player.DOMAIN, media_player.DEVICE_CLASS_TV): TYPE_TV, (media_player.DOMAIN, media_player.DEVICE_CLASS_SPEAKER): TYPE_SPEAKER, (sensor.DOMAIN, sensor.DEVICE_CLASS_TEMPERATURE): TYPE_SENSOR, + (sensor.DOMAIN, sensor.DEVICE_CLASS_HUMIDITY): TYPE_SENSOR, } CHALLENGE_ACK_NEEDED = "ackNeeded" CHALLENGE_PIN_NEEDED = "pinNeeded" CHALLENGE_FAILED_PIN_NEEDED = "challengeFailedPinNeeded" + +STORE_AGENT_USER_IDS = "agent_user_ids" diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 96b9b93d70a..8a847eca705 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -7,25 +7,27 @@ from typing import List, Optional from aiohttp.web import json_response -from homeassistant.core import Context, callback, HomeAssistant, State -from homeassistant.helpers.event import async_call_later from homeassistant.components import webhook from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_SUPPORTED_FEATURES, + CLOUD_NEVER_EXPOSED_ENTITIES, CONF_NAME, STATE_UNAVAILABLE, - ATTR_SUPPORTED_FEATURES, - ATTR_DEVICE_CLASS, - CLOUD_NEVER_EXPOSED_ENTITIES, ) +from homeassistant.core import Context, HomeAssistant, State, callback +from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.storage import Store from . import trait from .const import ( + CONF_ALIASES, + CONF_ROOM_HINT, + DEVICE_CLASS_TO_GOOGLE_TYPES, DOMAIN, DOMAIN_TO_GOOGLE_TYPES, - CONF_ALIASES, ERR_FUNCTION_NOT_SUPPORTED, - DEVICE_CLASS_TO_GOOGLE_TYPES, - CONF_ROOM_HINT, + STORE_AGENT_USER_IDS, ) from .error import SmartHomeError @@ -41,19 +43,20 @@ class AbstractConfig: def __init__(self, hass): """Initialize abstract config.""" self.hass = hass - self._google_sync_unsub = None + self._store = None + self._google_sync_unsub = {} self._local_sdk_active = False + async def async_initialize(self): + """Perform async initialization of config.""" + self._store = GoogleConfigStore(self.hass) + await self._store.async_load() + @property def enabled(self): """Return if Google is enabled.""" return False - @property - def agent_user_id(self): - """Return Agent User Id to use for query responses.""" - return None - @property def entity_config(self): """Return entity config.""" @@ -77,7 +80,6 @@ class AbstractConfig: @property def should_report_state(self): """Return if states should be proactively reported.""" - # pylint: disable=no-self-use return False @property @@ -102,13 +104,22 @@ class AbstractConfig: # pylint: disable=no-self-use return True - async def async_report_state(self, message): + async def async_report_state(self, message, agent_user_id: str): """Send a state report to Google.""" raise NotImplementedError + async def async_report_state_all(self, message): + """Send a state report to Google for all previously synced users.""" + jobs = [ + self.async_report_state(message, agent_user_id) + for agent_user_id in self._store.agent_user_ids + ] + await gather(*jobs) + def async_enable_report_state(self): """Enable proactive mode.""" # Circular dep + # pylint: disable=import-outside-toplevel from .report_state import async_enable_report_state if self._unsub_report_state is None: @@ -120,42 +131,63 @@ class AbstractConfig: self._unsub_report_state() self._unsub_report_state = None - async def async_sync_entities(self): + async def async_sync_entities(self, agent_user_id: str): """Sync all entities to Google.""" # Remove any pending sync - if self._google_sync_unsub: - self._google_sync_unsub() - self._google_sync_unsub = None + self._google_sync_unsub.pop(agent_user_id, lambda: None)() + return await self._async_request_sync_devices(agent_user_id) - return await self._async_request_sync_devices() - - async def _schedule_callback(self, _now): - """Handle a scheduled sync callback.""" - self._google_sync_unsub = None - await self.async_sync_entities() + async def async_sync_entities_all(self): + """Sync all entities to Google for all registered agents.""" + res = await gather( + *[ + self.async_sync_entities(agent_user_id) + for agent_user_id in self._store.agent_user_ids + ] + ) + return max(res, default=204) @callback - def async_schedule_google_sync(self): + def async_schedule_google_sync(self, agent_user_id: str): """Schedule a sync.""" - if self._google_sync_unsub: - self._google_sync_unsub() - self._google_sync_unsub = async_call_later( - self.hass, SYNC_DELAY, self._schedule_callback + async def _schedule_callback(_now): + """Handle a scheduled sync callback.""" + self._google_sync_unsub.pop(agent_user_id, None) + await self.async_sync_entities(agent_user_id) + + self._google_sync_unsub.pop(agent_user_id, lambda: None)() + + self._google_sync_unsub[agent_user_id] = async_call_later( + self.hass, SYNC_DELAY, _schedule_callback ) - async def _async_request_sync_devices(self) -> int: + @callback + def async_schedule_google_sync_all(self): + """Schedule a sync for all registered agents.""" + for agent_user_id in self._store.agent_user_ids: + self.async_schedule_google_sync(agent_user_id) + + async def _async_request_sync_devices(self, agent_user_id: str) -> int: """Trigger a sync with Google. Return value is the HTTP status code of the sync request. """ raise NotImplementedError - async def async_deactivate_report_state(self): + async def async_connect_agent_user(self, agent_user_id: str): + """Add an synced and known agent_user_id. + + Called when a completed sync response have been sent to Google. + """ + self._store.add_agent_user_id(agent_user_id) + + async def async_disconnect_agent_user(self, agent_user_id: str): """Turn off report state and disable further state reporting. Called when the user disconnects their account from Google. """ + self._store.pop_agent_user_id(agent_user_id) @callback def async_enable_local_sdk(self): @@ -166,7 +198,7 @@ class AbstractConfig: return webhook.async_register( - self.hass, DOMAIN, "Local Support", webhook_id, self._handle_local_webhook + self.hass, DOMAIN, "Local Support", webhook_id, self._handle_local_webhook, ) self._local_sdk_active = True @@ -182,6 +214,8 @@ class AbstractConfig: async def _handle_local_webhook(self, hass, webhook_id, request): """Handle an incoming local SDK message.""" + # Circular dep + # pylint: disable=import-outside-toplevel from . import smart_home payload = await request.json() @@ -202,6 +236,44 @@ class AbstractConfig: return json_response(result) +class GoogleConfigStore: + """A configuration store for google assistant.""" + + _STORAGE_VERSION = 1 + _STORAGE_KEY = DOMAIN + + def __init__(self, hass): + """Initialize a configuration store.""" + self._hass = hass + self._store = Store(hass, self._STORAGE_VERSION, self._STORAGE_KEY) + self._data = {STORE_AGENT_USER_IDS: {}} + + @property + def agent_user_ids(self): + """Return a list of connected agent user_ids.""" + return self._data[STORE_AGENT_USER_IDS] + + @callback + def add_agent_user_id(self, agent_user_id): + """Add an agent user id to store.""" + if agent_user_id not in self._data[STORE_AGENT_USER_IDS]: + self._data[STORE_AGENT_USER_IDS][agent_user_id] = {} + self._store.async_delay_save(lambda: self._data, 1.0) + + @callback + def pop_agent_user_id(self, agent_user_id): + """Remove agent user id from store.""" + if agent_user_id in self._data[STORE_AGENT_USER_IDS]: + self._data[STORE_AGENT_USER_IDS].pop(agent_user_id, None) + self._store.async_delay_save(lambda: self._data, 1.0) + + async def async_load(self): + """Store current configuration to disk.""" + data = await self._store.async_load() + if data: + self._data = data + + class RequestData: """Hold data associated with a particular request.""" @@ -267,7 +339,7 @@ class GoogleEntity: @callback def is_supported(self) -> bool: """Return if the entity is supported by Google.""" - return self.state.state != STATE_UNAVAILABLE and bool(self.traits()) + return bool(self.traits()) @callback def might_2fa(self) -> bool: @@ -281,7 +353,7 @@ class GoogleEntity: trait.might_2fa(domain, features, device_class) for trait in self.traits() ) - async def sync_serialize(self): + async def sync_serialize(self, agent_user_id): """Serialize entity for a SYNC response. https://developers.google.com/actions/smarthome/create-app#actiondevicessync @@ -317,7 +389,7 @@ class GoogleEntity: "webhookId": self.config.local_sdk_webhook_id, "httpPort": self.hass.config.api.port, "httpSSL": self.hass.config.api.use_ssl, - "proxyDeviceId": self.config.agent_user_id, + "proxyDeviceId": agent_user_id, } for trt in traits: diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index 90fa1ced157..233923e97a9 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -3,39 +3,36 @@ import asyncio from datetime import timedelta import logging from uuid import uuid4 -import jwt -from aiohttp import ClientResponseError, ClientError +from aiohttp import ClientError, ClientResponseError from aiohttp.web import Request, Response +import jwt # Typing imports from homeassistant.components.http import HomeAssistantView -from homeassistant.core import callback, ServiceCall from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util import dt as dt_util from .const import ( - GOOGLE_ASSISTANT_API_ENDPOINT, CONF_API_KEY, - CONF_EXPOSE_BY_DEFAULT, - CONF_EXPOSED_DOMAINS, + CONF_CLIENT_EMAIL, CONF_ENTITY_CONFIG, CONF_EXPOSE, + CONF_EXPOSE_BY_DEFAULT, + CONF_EXPOSED_DOMAINS, + CONF_PRIVATE_KEY, CONF_REPORT_STATE, CONF_SECURE_DEVICES_PIN, CONF_SERVICE_ACCOUNT, - CONF_CLIENT_EMAIL, - CONF_PRIVATE_KEY, - DOMAIN, - HOMEGRAPH_TOKEN_URL, + GOOGLE_ASSISTANT_API_ENDPOINT, HOMEGRAPH_SCOPE, + HOMEGRAPH_TOKEN_URL, REPORT_STATE_BASE_URL, REQUEST_SYNC_BASE_URL, - SERVICE_REQUEST_SYNC, ) -from .smart_home import async_handle_message from .helpers import AbstractConfig +from .smart_home import async_handle_message _LOGGER = logging.getLogger(__name__) @@ -84,11 +81,6 @@ class GoogleConfig(AbstractConfig): """Return if Google is enabled.""" return True - @property - def agent_user_id(self): - """Return Agent User Id to use for query responses.""" - return None - @property def entity_config(self): """Return entity config.""" @@ -102,7 +94,6 @@ class GoogleConfig(AbstractConfig): @property def should_report_state(self): """Return if states should be proactively reported.""" - # pylint: disable=no-self-use return self._config.get(CONF_REPORT_STATE) def should_expose(self, state) -> bool: @@ -134,6 +125,18 @@ class GoogleConfig(AbstractConfig): """If an entity should have 2FA checked.""" return True + async def _async_request_sync_devices(self, agent_user_id: str): + if CONF_API_KEY in self._config: + await self.async_call_homegraph_api_key( + REQUEST_SYNC_BASE_URL, {"agentUserId": agent_user_id} + ) + elif CONF_SERVICE_ACCOUNT in self._config: + await self.async_call_homegraph_api( + REQUEST_SYNC_BASE_URL, {"agentUserId": agent_user_id} + ) + else: + _LOGGER.error("No configuration for request_sync available") + async def _async_update_token(self, force=False): if CONF_SERVICE_ACCOUNT not in self._config: _LOGGER.error("Trying to get homegraph api token without service account") @@ -152,6 +155,25 @@ class GoogleConfig(AbstractConfig): self._access_token = token["access_token"] self._access_token_renew = now + timedelta(seconds=token["expires_in"]) + async def async_call_homegraph_api_key(self, url, data): + """Call a homegraph api with api key authentication.""" + websession = async_get_clientsession(self.hass) + try: + res = await websession.post( + url, params={"key": self._config.get(CONF_API_KEY)}, json=data + ) + _LOGGER.debug( + "Response on %s with data %s was %s", url, data, await res.text() + ) + res.raise_for_status() + return res.status + except ClientResponseError as error: + _LOGGER.error("Request for %s failed: %d", url, error.status) + return error.status + except (asyncio.TimeoutError, ClientError): + _LOGGER.error("Could not contact %s", url) + return 500 + async def async_call_homegraph_api(self, url, data): """Call a homegraph api with authenticaiton.""" session = async_get_clientsession(self.hass) @@ -166,63 +188,37 @@ class GoogleConfig(AbstractConfig): "Response on %s with data %s was %s", url, data, await res.text() ) res.raise_for_status() + return res.status try: await self._async_update_token() try: - await _call() + return await _call() except ClientResponseError as error: if error.status == 401: _LOGGER.warning( "Request for %s unauthorized, renewing token and retrying", url ) await self._async_update_token(True) - await _call() - else: - raise + return await _call() + raise except ClientResponseError as error: _LOGGER.error("Request for %s failed: %d", url, error.status) + return error.status except (asyncio.TimeoutError, ClientError): _LOGGER.error("Could not contact %s", url) + return 500 - async def async_report_state(self, message): + async def async_report_state(self, message, agent_user_id: str): """Send a state report to Google.""" data = { "requestId": uuid4().hex, - "agentUserId": (await self.hass.auth.async_get_owner()).id, + "agentUserId": agent_user_id, "payload": message, } await self.async_call_homegraph_api(REPORT_STATE_BASE_URL, data) -@callback -def async_register_http(hass, cfg): - """Register HTTP views for Google Assistant.""" - config = GoogleConfig(hass, cfg) - hass.http.register_view(GoogleAssistantView(config)) - if config.should_report_state: - config.async_enable_report_state() - - async def request_sync_service_handler(call: ServiceCall): - """Handle request sync service calls.""" - agent_user_id = call.data.get("agent_user_id") or call.context.user_id - - if agent_user_id is None: - _LOGGER.warning( - "No agent_user_id supplied for request_sync. Call as a user or pass in user id as agent_user_id." - ) - return - await config.async_call_homegraph_api( - REQUEST_SYNC_BASE_URL, {"agentUserId": agent_user_id} - ) - - # Register service only if api key is provided - if CONF_API_KEY not in cfg and CONF_SERVICE_ACCOUNT in cfg: - hass.services.async_register( - DOMAIN, SERVICE_REQUEST_SYNC, request_sync_service_handler - ) - - class GoogleAssistantView(HomeAssistantView): """Handle Google Assistant requests.""" diff --git a/homeassistant/components/google_assistant/manifest.json b/homeassistant/components/google_assistant/manifest.json index f97977a7400..94dd3b7f079 100644 --- a/homeassistant/components/google_assistant/manifest.json +++ b/homeassistant/components/google_assistant/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/google_assistant", "requirements": [], "dependencies": ["http"], + "after_dependencies": ["camera"], "codeowners": ["@home-assistant/cloud"] } diff --git a/homeassistant/components/google_assistant/report_state.py b/homeassistant/components/google_assistant/report_state.py index aacb90e9d2b..1e8b6c020de 100644 --- a/homeassistant/components/google_assistant/report_state.py +++ b/homeassistant/components/google_assistant/report_state.py @@ -1,12 +1,12 @@ """Google Report State implementation.""" import logging -from homeassistant.core import HomeAssistant, callback from homeassistant.const import MATCH_ALL +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.event import async_call_later -from .helpers import AbstractConfig, GoogleEntity, async_get_entities from .error import SmartHomeError +from .helpers import AbstractConfig, GoogleEntity, async_get_entities # Time to wait until the homegraph updates # https://github.com/actions-on-google/smart-home-nodejs/issues/196#issuecomment-439156639 @@ -45,7 +45,7 @@ def async_enable_report_state(hass: HomeAssistant, google_config: AbstractConfig if entity_data == old_entity.query_serialize(): return - await google_config.async_report_state( + await google_config.async_report_state_all( {"devices": {"states": {changed_entity: entity_data}}} ) @@ -62,7 +62,7 @@ def async_enable_report_state(hass: HomeAssistant, google_config: AbstractConfig except SmartHomeError: continue - await google_config.async_report_state({"devices": {"states": entities}}) + await google_config.async_report_state_all({"devices": {"states": entities}}) async_call_later(hass, INITIAL_REPORT_DELAY, inital_report) diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index 0944c9532ef..b111e6dc942 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -3,20 +3,19 @@ import asyncio from itertools import product import logging +from homeassistant.const import ATTR_ENTITY_ID, __version__ from homeassistant.util.decorator import Registry -from homeassistant.const import ATTR_ENTITY_ID, __version__ - from .const import ( - ERR_PROTOCOL_ERROR, ERR_DEVICE_OFFLINE, + ERR_PROTOCOL_ERROR, ERR_UNKNOWN_ERROR, EVENT_COMMAND_RECEIVED, - EVENT_SYNC_RECEIVED, EVENT_QUERY_RECEIVED, + EVENT_SYNC_RECEIVED, ) -from .helpers import RequestData, GoogleEntity, async_get_entities from .error import SmartHomeError +from .helpers import GoogleEntity, RequestData, async_get_entities HANDLERS = Registry() _LOGGER = logging.getLogger(__name__) @@ -79,18 +78,19 @@ async def async_devices_sync(hass, data, payload): EVENT_SYNC_RECEIVED, {"request_id": data.request_id}, context=data.context ) + agent_user_id = data.context.user_id + devices = await asyncio.gather( *( - entity.sync_serialize() + entity.sync_serialize(agent_user_id) for entity in async_get_entities(hass, data.config) if entity.should_expose() ) ) - response = { - "agentUserId": data.config.agent_user_id or data.context.user_id, - "devices": devices, - } + response = {"agentUserId": agent_user_id, "devices": devices} + + await data.config.async_connect_agent_user(agent_user_id) return response @@ -197,7 +197,7 @@ async def async_devices_disconnect(hass, data: RequestData, payload): https://developers.google.com/assistant/smarthome/develop/process-intents#DISCONNECT """ - await data.config.async_deactivate_report_state() + await data.config.async_disconnect_agent_user(data.context.user_id) return None @@ -209,7 +209,7 @@ async def async_devices_identify(hass, data: RequestData, payload): """ return { "device": { - "id": data.config.agent_user_id, + "id": data.context.user_id, "isLocalOnly": True, "isProxy": True, "deviceInfo": { diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 6b5530ab2ce..d49632755cd 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -2,66 +2,68 @@ import logging from homeassistant.components import ( + alarm_control_panel, binary_sensor, camera, cover, - group, fan, + group, input_boolean, - media_player, light, lock, + media_player, scene, script, sensor, switch, vacuum, - alarm_control_panel, ) from homeassistant.components.climate import const as climate from homeassistant.const import ( - ATTR_ENTITY_ID, + ATTR_ASSUMED_STATE, + ATTR_CODE, ATTR_DEVICE_CLASS, + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + ATTR_TEMPERATURE, + SERVICE_ALARM_ARM_AWAY, + SERVICE_ALARM_ARM_CUSTOM_BYPASS, + SERVICE_ALARM_ARM_HOME, + SERVICE_ALARM_ARM_NIGHT, + SERVICE_ALARM_DISARM, + SERVICE_ALARM_TRIGGER, SERVICE_TURN_OFF, SERVICE_TURN_ON, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_CUSTOM_BYPASS, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, + STATE_ALARM_PENDING, + STATE_ALARM_TRIGGERED, STATE_LOCKED, STATE_OFF, STATE_ON, + STATE_UNAVAILABLE, + STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT, - ATTR_SUPPORTED_FEATURES, - ATTR_TEMPERATURE, - ATTR_ASSUMED_STATE, - SERVICE_ALARM_DISARM, - SERVICE_ALARM_ARM_HOME, - SERVICE_ALARM_ARM_AWAY, - SERVICE_ALARM_ARM_NIGHT, - SERVICE_ALARM_ARM_CUSTOM_BYPASS, - SERVICE_ALARM_TRIGGER, - STATE_ALARM_ARMED_HOME, - STATE_ALARM_ARMED_AWAY, - STATE_ALARM_ARMED_NIGHT, - STATE_ALARM_ARMED_CUSTOM_BYPASS, - STATE_ALARM_DISARMED, - STATE_ALARM_TRIGGERED, - STATE_ALARM_PENDING, - ATTR_CODE, - STATE_UNKNOWN, ) from homeassistant.core import DOMAIN as HA_DOMAIN from homeassistant.util import color as color_util, temperature as temp_util + from .const import ( - ERR_VALUE_OUT_OF_RANGE, - ERR_NOT_SUPPORTED, - ERR_FUNCTION_NOT_SUPPORTED, - ERR_CHALLENGE_NOT_SETUP, CHALLENGE_ACK_NEEDED, - CHALLENGE_PIN_NEEDED, CHALLENGE_FAILED_PIN_NEEDED, - ERR_ALREADY_DISARMED, + CHALLENGE_PIN_NEEDED, ERR_ALREADY_ARMED, + ERR_ALREADY_DISARMED, + ERR_CHALLENGE_NOT_SETUP, + ERR_FUNCTION_NOT_SUPPORTED, + ERR_NOT_SUPPORTED, + ERR_VALUE_OUT_OF_RANGE, ) -from .error import SmartHomeError, ChallengeNeeded +from .error import ChallengeNeeded, SmartHomeError _LOGGER = logging.getLogger(__name__) @@ -80,6 +82,7 @@ TRAIT_MODES = PREFIX_TRAITS + "Modes" TRAIT_OPENCLOSE = PREFIX_TRAITS + "OpenClose" TRAIT_VOLUME = PREFIX_TRAITS + "Volume" TRAIT_ARMDISARM = PREFIX_TRAITS + "ArmDisarm" +TRAIT_HUMIDITY_SETTING = PREFIX_TRAITS + "HumiditySetting" PREFIX_COMMANDS = "action.devices.commands." COMMAND_ONOFF = PREFIX_COMMANDS + "OnOff" @@ -664,7 +667,7 @@ class TemperatureSettingTrait(_Trait): device_class = attrs.get(ATTR_DEVICE_CLASS) if device_class == sensor.DEVICE_CLASS_TEMPERATURE: current_temp = self.state.state - if current_temp is not None: + if current_temp not in (STATE_UNKNOWN, STATE_UNAVAILABLE): response["thermostatTemperatureAmbient"] = round( temp_util.convert(float(current_temp), unit, TEMP_CELSIUS), 1 ) @@ -849,6 +852,56 @@ class TemperatureSettingTrait(_Trait): ) +@register_trait +class HumiditySettingTrait(_Trait): + """Trait to offer humidity setting functionality. + + https://developers.google.com/actions/smarthome/traits/humiditysetting + """ + + name = TRAIT_HUMIDITY_SETTING + commands = [] + + @staticmethod + def supported(domain, features, device_class): + """Test if state is supported.""" + return domain == sensor.DOMAIN and device_class == sensor.DEVICE_CLASS_HUMIDITY + + def sync_attributes(self): + """Return humidity attributes for a sync request.""" + response = {} + attrs = self.state.attributes + domain = self.state.domain + if domain == sensor.DOMAIN: + device_class = attrs.get(ATTR_DEVICE_CLASS) + if device_class == sensor.DEVICE_CLASS_HUMIDITY: + response["queryOnlyHumiditySetting"] = True + + return response + + def query_attributes(self): + """Return humidity query attributes.""" + response = {} + attrs = self.state.attributes + domain = self.state.domain + if domain == sensor.DOMAIN: + device_class = attrs.get(ATTR_DEVICE_CLASS) + if device_class == sensor.DEVICE_CLASS_HUMIDITY: + current_humidity = self.state.state + if current_humidity not in (STATE_UNKNOWN, STATE_UNAVAILABLE): + response["humidityAmbientPercent"] = round(float(current_humidity)) + + return response + + async def execute(self, command, data, params, challenge): + """Execute a humidity command.""" + domain = self.state.domain + if domain == sensor.DOMAIN: + raise SmartHomeError( + ERR_NOT_SUPPORTED, "Execute is not supported by sensor" + ) + + @register_trait class LockUnlockTrait(_Trait): """Trait to lock or unlock a lock. diff --git a/homeassistant/components/google_cloud/tts.py b/homeassistant/components/google_cloud/tts.py index 942ee0a4e48..6721520d130 100644 --- a/homeassistant/components/google_cloud/tts.py +++ b/homeassistant/components/google_cloud/tts.py @@ -1,11 +1,11 @@ """Support for the Google Cloud TTS service.""" +import asyncio import logging import os -import asyncio import async_timeout -import voluptuous as vol from google.cloud import texttospeech +import voluptuous as vol from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/google_domains/__init__.py b/homeassistant/components/google_domains/__init__.py index 8f975db6fd8..d440567d9ad 100644 --- a/homeassistant/components/google_domains/__init__.py +++ b/homeassistant/components/google_domains/__init__.py @@ -7,8 +7,8 @@ import aiohttp import async_timeout import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_DOMAIN, CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/google_maps/device_tracker.py b/homeassistant/components/google_maps/device_tracker.py index 75f370e502e..9e33ff5f715 100644 --- a/homeassistant/components/google_maps/device_tracker.py +++ b/homeassistant/components/google_maps/device_tracker.py @@ -53,6 +53,7 @@ class GoogleMapsScanner: self.username = config[CONF_USERNAME] self.max_gps_accuracy = config[CONF_MAX_GPS_ACCURACY] self.scan_interval = config.get(CONF_SCAN_INTERVAL) or timedelta(seconds=60) + self._prev_seen = {} credfile = "{}.{}".format( hass.config.path(CREDENTIALS_FILE), slugify(self.username) @@ -92,11 +93,22 @@ class GoogleMapsScanner: ) continue + last_seen = dt_util.as_utc(person.datetime) + if last_seen < self._prev_seen.get(dev_id, last_seen): + _LOGGER.warning( + "Ignoring %s update because timestamp " + "is older than last timestamp", + person.nickname, + ) + _LOGGER.debug("%s < %s", last_seen, self._prev_seen[dev_id]) + continue + self._prev_seen[dev_id] = last_seen + attrs = { ATTR_ADDRESS: person.address, ATTR_FULL_NAME: person.full_name, ATTR_ID: person.id, - ATTR_LAST_SEEN: dt_util.as_utc(person.datetime), + ATTR_LAST_SEEN: last_seen, ATTR_NICKNAME: person.nickname, ATTR_BATTERY_CHARGING: person.charging, ATTR_BATTERY_LEVEL: person.battery_level, diff --git a/homeassistant/components/google_pubsub/__init__.py b/homeassistant/components/google_pubsub/__init__.py index e108d249797..bc7811a7a8f 100644 --- a/homeassistant/components/google_pubsub/__init__.py +++ b/homeassistant/components/google_pubsub/__init__.py @@ -5,6 +5,7 @@ import logging import os from typing import Any, Dict +from google.cloud import pubsub_v1 import voluptuous as vol from homeassistant.const import EVENT_STATE_CHANGED, STATE_UNAVAILABLE, STATE_UNKNOWN @@ -38,7 +39,6 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): """Activate Google Pub/Sub component.""" - from google.cloud import pubsub_v1 config = yaml_config[DOMAIN] project_id = config[CONF_PROJECT_ID] @@ -57,7 +57,9 @@ def setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): service_principal_path ) - topic_path = publisher.topic_path(project_id, topic_name) # pylint: disable=E1101 + topic_path = publisher.topic_path( # pylint: disable=no-member + project_id, topic_name + ) encoder = DateTimeJSONEncoder() @@ -87,7 +89,7 @@ class DateTimeJSONEncoder(json.JSONEncoder): Additionally add encoding for datetime objects as isoformat. """ - def default(self, o): # pylint: disable=E0202 + def default(self, o): # pylint: disable=method-hidden """Implement encoding logic.""" if isinstance(o, datetime.datetime): return o.isoformat() diff --git a/homeassistant/components/google_translate/tts.py b/homeassistant/components/google_translate/tts.py index 3add45b8cb8..e35a229ab98 100644 --- a/homeassistant/components/google_translate/tts.py +++ b/homeassistant/components/google_translate/tts.py @@ -6,6 +6,7 @@ import re import aiohttp from aiohttp.hdrs import REFERER, USER_AGENT import async_timeout +from gtts_token import gtts_token import voluptuous as vol import yarl @@ -115,7 +116,6 @@ class GoogleProvider(Provider): async def async_get_tts_audio(self, message, language, options=None): """Load TTS from google.""" - from gtts_token import gtts_token token = gtts_token.Token() websession = async_get_clientsession(self.hass) diff --git a/homeassistant/components/google_wifi/sensor.py b/homeassistant/components/google_wifi/sensor.py index 1d4ed8d84f8..9d6f3ea3d58 100644 --- a/homeassistant/components/google_wifi/sensor.py +++ b/homeassistant/components/google_wifi/sensor.py @@ -1,21 +1,20 @@ """Support for retrieving status info from Google Wifi/OnHub routers.""" -import logging from datetime import timedelta +import logging -import voluptuous as vol import requests +import voluptuous as vol -from homeassistant.util import dt -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, CONF_HOST, CONF_MONITORED_CONDITIONS, + CONF_NAME, STATE_UNKNOWN, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.util import Throttle +from homeassistant.util import Throttle, dt _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/gpmdp/media_player.py b/homeassistant/components/gpmdp/media_player.py index e6df8b0fe8b..e7b18aacc15 100644 --- a/homeassistant/components/gpmdp/media_player.py +++ b/homeassistant/components/gpmdp/media_player.py @@ -5,8 +5,9 @@ import socket import time import voluptuous as vol +from websocket import _exceptions, create_connection -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, @@ -65,8 +66,6 @@ def request_configuration(hass, config, url, add_entities_callback): ) return - from websocket import create_connection - websocket = create_connection((url), timeout=1) websocket.send( json.dumps( @@ -81,7 +80,6 @@ def request_configuration(hass, config, url, add_entities_callback): def gpmdp_configuration_callback(callback_data): """Handle configuration changes.""" while True: - from websocket import _exceptions try: msg = json.loads(websocket.recv()) @@ -174,7 +172,6 @@ class GPMDP(MediaPlayerDevice): def __init__(self, name, url, code): """Initialize the media player.""" - from websocket import create_connection self._connection = create_connection self._url = url @@ -210,7 +207,6 @@ class GPMDP(MediaPlayerDevice): def send_gpmdp_msg(self, namespace, method, with_id=True): """Send ws messages to GPMDP and verify request id in response.""" - from websocket import _exceptions try: websocket = self.get_ws() diff --git a/homeassistant/components/gpslogger/.translations/da.json b/homeassistant/components/gpslogger/.translations/da.json index 6d5c2185718..b118783cd3c 100644 --- a/homeassistant/components/gpslogger/.translations/da.json +++ b/homeassistant/components/gpslogger/.translations/da.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "not_internet_accessible": "Dit Home Assistant system skal v\u00e6re tilg\u00e6ngeligt fra internettet for at modtage GPSLogger meddelelser.", + "not_internet_accessible": "Din Home Assistant-instans skal v\u00e6re tilg\u00e6ngelig fra internettet for at modtage GPSLogger-meddelelser.", "one_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning" }, "create_entry": { - "default": "For at sende begivenheder til Home Assistant skal du konfigurere webhook funktionen i GPSLogger.\n\n Udfyld f\u00f8lgende oplysninger: \n\n - URL: `{webhook_url}`\n - Metode: POST\n \n Se [dokumentationen]({docs_url}) for yderligere oplysninger." + "default": "For at sende h\u00e6ndelser til Home Assistant skal du konfigurere webhook-funktionen i GPSLogger.\n\n Udfyld f\u00f8lgende oplysninger: \n\n - Webadresse: `{webhook_url}`\n - Metode: POST\n \nSe [dokumentationen]({docs_url}) for yderligere oplysninger." }, "step": { "user": { diff --git a/homeassistant/components/gpslogger/.translations/ko.json b/homeassistant/components/gpslogger/.translations/ko.json index 786a67b0b19..19bfc36e424 100644 --- a/homeassistant/components/gpslogger/.translations/ko.json +++ b/homeassistant/components/gpslogger/.translations/ko.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "GPSLogger Webhook \uc744 \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "GPSLogger Webhook \uc744 \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "GPSLogger Webhook \uc124\uc815" } }, diff --git a/homeassistant/components/gpslogger/__init__.py b/homeassistant/components/gpslogger/__init__.py index 3ac09457d81..aa95d17cbfc 100644 --- a/homeassistant/components/gpslogger/__init__.py +++ b/homeassistant/components/gpslogger/__init__.py @@ -1,30 +1,33 @@ """Support for GPSLogger.""" import logging -import voluptuous as vol from aiohttp import web +import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.components.device_tracker import ATTR_BATTERY +from homeassistant.components.device_tracker import ( + ATTR_BATTERY, + DOMAIN as DEVICE_TRACKER, +) from homeassistant.const import ( - HTTP_UNPROCESSABLE_ENTITY, - HTTP_OK, ATTR_LATITUDE, ATTR_LONGITUDE, 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 -from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER + from .const import ( - DOMAIN, - ATTR_ALTITUDE, ATTR_ACCURACY, ATTR_ACTIVITY, + ATTR_ALTITUDE, ATTR_DEVICE, ATTR_DIRECTION, ATTR_PROVIDER, ATTR_SPEED, + DOMAIN, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/gpslogger/config_flow.py b/homeassistant/components/gpslogger/config_flow.py index 5173c02e7ff..ef90a8d1607 100644 --- a/homeassistant/components/gpslogger/config_flow.py +++ b/homeassistant/components/gpslogger/config_flow.py @@ -1,7 +1,7 @@ """Config flow for GPSLogger.""" from homeassistant.helpers import config_entry_flow -from .const import DOMAIN +from .const import DOMAIN config_entry_flow.register_webhook_flow( DOMAIN, diff --git a/homeassistant/components/gpslogger/device_tracker.py b/homeassistant/components/gpslogger/device_tracker.py index c9dbbfee026..d8afc377d40 100644 --- a/homeassistant/components/gpslogger/device_tracker.py +++ b/homeassistant/components/gpslogger/device_tracker.py @@ -1,15 +1,15 @@ """Support for the GPSLogger device tracking.""" import logging -from homeassistant.core import callback +from homeassistant.components.device_tracker import SOURCE_TYPE_GPS +from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, ) -from homeassistant.components.device_tracker import SOURCE_TYPE_GPS -from homeassistant.components.device_tracker.config_entry import TrackerEntity +from homeassistant.core import callback from homeassistant.helpers import device_registry from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.restore_state import RestoreEntity diff --git a/homeassistant/components/graphite/__init__.py b/homeassistant/components/graphite/__init__.py index 3809249bea6..bf34bc3ddea 100644 --- a/homeassistant/components/graphite/__init__.py +++ b/homeassistant/components/graphite/__init__.py @@ -7,7 +7,6 @@ import time import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( CONF_HOST, CONF_PORT, @@ -17,6 +16,7 @@ from homeassistant.const import ( EVENT_STATE_CHANGED, ) from homeassistant.helpers import state +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/greeneye_monitor/__init__.py b/homeassistant/components/greeneye_monitor/__init__.py index cb67ac7faa4..4f5899f6a4a 100644 --- a/homeassistant/components/greeneye_monitor/__init__.py +++ b/homeassistant/components/greeneye_monitor/__init__.py @@ -1,6 +1,7 @@ """Support for monitoring a GreenEye Monitor energy monitor.""" import logging +from greeneye import Monitors import voluptuous as vol from homeassistant.const import ( @@ -110,7 +111,6 @@ CONFIG_SCHEMA = vol.Schema({DOMAIN: COMPONENT_SCHEMA}, extra=vol.ALLOW_EXTRA) async def async_setup(hass, config): """Set up the GreenEye Monitor component.""" - from greeneye import Monitors monitors = Monitors() hass.data[DATA_GREENEYE_MONITOR] = monitors diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index 29126c82d44..16094ed4832 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -7,34 +7,33 @@ import voluptuous as vol from homeassistant import core as ha from homeassistant.const import ( + ATTR_ASSUMED_STATE, ATTR_ENTITY_ID, + ATTR_ICON, + ATTR_NAME, CONF_ICON, CONF_NAME, + SERVICE_RELOAD, STATE_CLOSED, STATE_HOME, + STATE_LOCKED, STATE_NOT_HOME, STATE_OFF, + STATE_OK, STATE_ON, STATE_OPEN, - STATE_LOCKED, - STATE_UNLOCKED, - STATE_OK, STATE_PROBLEM, STATE_UNKNOWN, - ATTR_ASSUMED_STATE, - SERVICE_RELOAD, - ATTR_NAME, - ATTR_ICON, + STATE_UNLOCKED, ) from homeassistant.core import callback -from homeassistant.loader import bind_hass +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.config_validation import make_entity_service_schema from homeassistant.helpers.entity import Entity, async_generate_entity_id from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import async_track_state_change -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA from homeassistant.helpers.typing import HomeAssistantType - +from homeassistant.loader import bind_hass # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs @@ -63,28 +62,6 @@ SERVICE_REMOVE = "remove" CONTROL_TYPES = vol.In(["hidden", None]) -SET_VISIBILITY_SERVICE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_VISIBLE): cv.boolean} -) - -RELOAD_SERVICE_SCHEMA = vol.Schema({}) - -SET_SERVICE_SCHEMA = vol.Schema( - { - vol.Required(ATTR_OBJECT_ID): cv.slug, - vol.Optional(ATTR_NAME): cv.string, - vol.Optional(ATTR_VIEW): cv.boolean, - vol.Optional(ATTR_ICON): cv.string, - vol.Optional(ATTR_CONTROL): CONTROL_TYPES, - vol.Optional(ATTR_VISIBLE): cv.boolean, - vol.Optional(ATTR_ALL): cv.boolean, - vol.Exclusive(ATTR_ENTITIES, "entities"): cv.entity_ids, - vol.Exclusive(ATTR_ADD_ENTITIES, "entities"): cv.entity_ids, - } -) - -REMOVE_SERVICE_SCHEMA = vol.Schema({vol.Required(ATTR_OBJECT_ID): cv.slug}) - _LOGGER = logging.getLogger(__name__) @@ -227,7 +204,7 @@ async def async_setup(hass, config): await component.async_add_entities(auto) hass.services.async_register( - DOMAIN, SERVICE_RELOAD, reload_service_handler, schema=RELOAD_SERVICE_SCHEMA + DOMAIN, SERVICE_RELOAD, reload_service_handler, schema=vol.Schema({}) ) service_lock = asyncio.Lock() @@ -319,11 +296,29 @@ async def async_setup(hass, config): await component.async_remove_entity(entity_id) hass.services.async_register( - DOMAIN, SERVICE_SET, locked_service_handler, schema=SET_SERVICE_SCHEMA + DOMAIN, + SERVICE_SET, + locked_service_handler, + schema=vol.Schema( + { + vol.Required(ATTR_OBJECT_ID): cv.slug, + vol.Optional(ATTR_NAME): cv.string, + vol.Optional(ATTR_VIEW): cv.boolean, + vol.Optional(ATTR_ICON): cv.string, + vol.Optional(ATTR_CONTROL): CONTROL_TYPES, + vol.Optional(ATTR_VISIBLE): cv.boolean, + vol.Optional(ATTR_ALL): cv.boolean, + vol.Exclusive(ATTR_ENTITIES, "entities"): cv.entity_ids, + vol.Exclusive(ATTR_ADD_ENTITIES, "entities"): cv.entity_ids, + } + ), ) hass.services.async_register( - DOMAIN, SERVICE_REMOVE, groups_service_handler, schema=REMOVE_SERVICE_SCHEMA + DOMAIN, + SERVICE_REMOVE, + groups_service_handler, + schema=vol.Schema({vol.Required(ATTR_OBJECT_ID): cv.slug}), ) async def visibility_service_handler(service): @@ -344,7 +339,7 @@ async def async_setup(hass, config): DOMAIN, SERVICE_SET_VISIBILITY, visibility_service_handler, - schema=SET_VISIBILITY_SERVICE_SCHEMA, + schema=make_entity_service_schema({vol.Required(ATTR_VISIBLE): cv.boolean}), ) return True @@ -656,7 +651,6 @@ class Group(Entity): if gr_on is None: return - # pylint: disable=too-many-boolean-expressions if tr_state is None or ( (gr_state == gr_on and tr_state.state == gr_off) or (gr_state == gr_off and tr_state.state == gr_on) diff --git a/homeassistant/components/group/cover.py b/homeassistant/components/group/cover.py index f7a9643e5c8..d9efdfa53c6 100644 --- a/homeassistant/components/group/cover.py +++ b/homeassistant/components/group/cover.py @@ -4,18 +4,6 @@ from typing import Dict, Optional, Set import voluptuous as vol -from homeassistant.const import ( - ATTR_ASSUMED_STATE, - ATTR_ENTITY_ID, - ATTR_SUPPORTED_FEATURES, - CONF_ENTITIES, - CONF_NAME, - STATE_CLOSED, -) -from homeassistant.core import callback, State -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import async_track_state_change - from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, ATTR_CURRENT_TILT_POSITION, @@ -41,7 +29,17 @@ from homeassistant.components.cover import ( SUPPORT_STOP_TILT, CoverDevice, ) - +from homeassistant.const import ( + ATTR_ASSUMED_STATE, + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + CONF_ENTITIES, + CONF_NAME, + STATE_CLOSED, +) +from homeassistant.core import State, callback +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import async_track_state_change # mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs # mypy: no-check-untyped-defs diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index 2cd65028131..f0c81696469 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -8,20 +8,6 @@ from typing import Any, Callable, Iterator, List, Optional, Tuple, cast import voluptuous as vol from homeassistant.components import light -from homeassistant.const import ( - ATTR_ENTITY_ID, - ATTR_SUPPORTED_FEATURES, - CONF_ENTITIES, - CONF_NAME, - STATE_ON, - STATE_UNAVAILABLE, -) -from homeassistant.core import CALLBACK_TYPE, State, callback -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import async_track_state_change -from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from homeassistant.util import color as color_util - from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, @@ -42,7 +28,19 @@ from homeassistant.components.light import ( SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE, ) - +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + CONF_ENTITIES, + CONF_NAME, + STATE_ON, + STATE_UNAVAILABLE, +) +from homeassistant.core import CALLBACK_TYPE, State, callback +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import async_track_state_change +from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.util import color as color_util # mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs # mypy: no-check-untyped-defs diff --git a/homeassistant/components/group/notify.py b/homeassistant/components/group/notify.py index e17990690fa..2209e0e2333 100644 --- a/homeassistant/components/group/notify.py +++ b/homeassistant/components/group/notify.py @@ -6,9 +6,6 @@ import logging import voluptuous as vol -from homeassistant.const import ATTR_SERVICE -import homeassistant.helpers.config_validation as cv - from homeassistant.components.notify import ( ATTR_DATA, ATTR_MESSAGE, @@ -16,7 +13,8 @@ from homeassistant.components.notify import ( PLATFORM_SCHEMA, BaseNotificationService, ) - +from homeassistant.const import ATTR_SERVICE +import homeassistant.helpers.config_validation as cv # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs diff --git a/homeassistant/components/group/reproduce_state.py b/homeassistant/components/group/reproduce_state.py index 827e9bb1dcb..78790701934 100644 --- a/homeassistant/components/group/reproduce_state.py +++ b/homeassistant/components/group/reproduce_state.py @@ -2,15 +2,16 @@ from typing import Iterable, Optional from homeassistant.core import Context, 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, states: Iterable[State], context: Optional[Context] = None ) -> None: """Reproduce component states.""" - from . import get_entity_ids - from homeassistant.helpers.state import async_reproduce_state states_copy = [] for state in states: diff --git a/homeassistant/components/growatt_server/sensor.py b/homeassistant/components/growatt_server/sensor.py index 3b7109222a4..2816b86be84 100644 --- a/homeassistant/components/growatt_server/sensor.py +++ b/homeassistant/components/growatt_server/sensor.py @@ -1,17 +1,17 @@ """Read status of growatt inverters.""" -import re +import datetime import json import logging -import datetime +import re import growattServer import voluptuous as vol -from homeassistant.util import Throttle -from homeassistant.helpers.entity import Entity -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, CONF_USERNAME, CONF_PASSWORD +from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/gstreamer/media_player.py b/homeassistant/components/gstreamer/media_player.py index a213587bc0e..9b371bfffca 100644 --- a/homeassistant/components/gstreamer/media_player.py +++ b/homeassistant/components/gstreamer/media_player.py @@ -1,9 +1,10 @@ """Play media via gstreamer.""" import logging +from gsp import GstreamerPlayer import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, @@ -36,7 +37,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Gstreamer platform.""" - from gsp import GstreamerPlayer name = config.get(CONF_NAME) pipeline = config.get(CONF_PIPELINE) diff --git a/homeassistant/components/habitica/__init__.py b/homeassistant/components/habitica/__init__.py index f94a09e4da5..52326555aab 100644 --- a/homeassistant/components/habitica/__init__.py +++ b/homeassistant/components/habitica/__init__.py @@ -2,6 +2,7 @@ from collections import namedtuple import logging +from habitipy.aio import HabitipyAsync import voluptuous as vol from homeassistant.const import ( @@ -92,7 +93,6 @@ SERVICE_API_CALL_SCHEMA = vol.Schema( async def async_setup(hass, config): """Set up the Habitica service.""" - from habitipy.aio import HabitipyAsync conf = config[DOMAIN] data = hass.data[DOMAIN] = {} diff --git a/homeassistant/components/hangouts/.translations/ca.json b/homeassistant/components/hangouts/.translations/ca.json index ea43c804f2d..0dcc0f029c2 100644 --- a/homeassistant/components/hangouts/.translations/ca.json +++ b/homeassistant/components/hangouts/.translations/ca.json @@ -14,6 +14,7 @@ "data": { "2fa": "Pin 2FA" }, + "description": "buit", "title": "Verificaci\u00f3 en dos passos" }, "user": { @@ -22,6 +23,7 @@ "email": "Correu electr\u00f2nic", "password": "Contrasenya" }, + "description": "buit", "title": "Inici de sessi\u00f3 de Google Hangouts" } }, diff --git a/homeassistant/components/hangouts/.translations/da.json b/homeassistant/components/hangouts/.translations/da.json index 4155da38f8f..2ceb78ddde8 100644 --- a/homeassistant/components/hangouts/.translations/da.json +++ b/homeassistant/components/hangouts/.translations/da.json @@ -5,7 +5,7 @@ "unknown": "Ukendt fejl opstod" }, "error": { - "invalid_2fa": "Ugyldig 2-faktor godkendelse, pr\u00f8v venligst igen.", + "invalid_2fa": "Ugyldig tofaktor-godkendelse, pr\u00f8v igen.", "invalid_2fa_method": "Ugyldig 2FA-metode (Bekr\u00e6ft p\u00e5 telefon).", "invalid_login": "Ugyldig login, pr\u00f8v venligst igen." }, @@ -14,12 +14,12 @@ "data": { "2fa": "2FA pin" }, - "title": "To-faktor autentificering" + "title": "Tofaktor-godkendelse" }, "user": { "data": { - "authorization_code": "Autorisationskode (kr\u00e6ves til manuel godkendelse)", - "email": "Email adresse", + "authorization_code": "Godkendelseskode (kr\u00e6vet til manuel godkendelse)", + "email": "Emailadresse", "password": "Adgangskode" }, "title": "Google Hangouts login" diff --git a/homeassistant/components/hangouts/.translations/ru.json b/homeassistant/components/hangouts/.translations/ru.json index 15d90a672de..5bb98effb9f 100644 --- a/homeassistant/components/hangouts/.translations/ru.json +++ b/homeassistant/components/hangouts/.translations/ru.json @@ -14,6 +14,7 @@ "data": { "2fa": "\u041f\u0438\u043d-\u043a\u043e\u0434 \u0434\u043b\u044f \u0434\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u043e\u0439 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" }, + "description": "\u043f\u0443\u0441\u0442\u043e", "title": "\u0414\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" }, "user": { @@ -22,6 +23,7 @@ "email": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b", "password": "\u041f\u0430\u0440\u043e\u043b\u044c" }, + "description": "\u043f\u0443\u0441\u0442\u043e", "title": "Google Hangouts" } }, diff --git a/homeassistant/components/hangouts/__init__.py b/homeassistant/components/hangouts/__init__.py index 953994d6ac0..d4892c66890 100644 --- a/homeassistant/components/hangouts/__init__.py +++ b/homeassistant/components/hangouts/__init__.py @@ -1,16 +1,16 @@ """Support for Hangouts.""" import logging +from hangups.auth import GoogleAuthError import voluptuous as vol from homeassistant import config_entries +from homeassistant.components.conversation.util import create_matcher from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.helpers import dispatcher, intent import homeassistant.helpers.config_validation as cv -from homeassistant.components.conversation.util import create_matcher # We need an import from .config_flow, without it .config_flow is never loaded. -from .intents import HelpIntent from .config_flow import HangoutsFlowHandler # noqa: F401 from .const import ( CONF_BOT, @@ -32,6 +32,8 @@ from .const import ( SERVICE_UPDATE, TARGETS_SCHEMA, ) +from .hangouts_bot import HangoutsBot +from .intents import HelpIntent _LOGGER = logging.getLogger(__name__) @@ -96,11 +98,7 @@ async def async_setup(hass, config): async def async_setup_entry(hass, config): """Set up a config entry.""" - from hangups.auth import GoogleAuthError - try: - from .hangouts_bot import HangoutsBot - bot = HangoutsBot( hass, config.data.get(CONF_REFRESH_TOKEN), diff --git a/homeassistant/components/hangouts/config_flow.py b/homeassistant/components/hangouts/config_flow.py index 8e262d8b40f..f253df49341 100644 --- a/homeassistant/components/hangouts/config_flow.py +++ b/homeassistant/components/hangouts/config_flow.py @@ -1,7 +1,8 @@ """Config flow to configure Google Hangouts.""" import functools -import voluptuous as vol +from hangups import get_auth +import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_EMAIL, CONF_PASSWORD @@ -9,10 +10,16 @@ from homeassistant.core import callback from .const import ( CONF_2FA, - CONF_REFRESH_TOKEN, CONF_AUTH_CODE, + CONF_REFRESH_TOKEN, DOMAIN as HANGOUTS_DOMAIN, ) +from .hangups_utils import ( + Google2FAError, + GoogleAuthError, + HangoutsCredentials, + HangoutsRefreshToken, +) @callback @@ -44,14 +51,6 @@ class HangoutsFlowHandler(config_entries.ConfigFlow): return self.async_abort(reason="already_configured") if user_input is not None: - from hangups import get_auth - from .hangups_utils import ( - HangoutsCredentials, - HangoutsRefreshToken, - GoogleAuthError, - Google2FAError, - ) - user_email = user_input[CONF_EMAIL] user_password = user_input[CONF_PASSWORD] user_auth_code = user_input.get(CONF_AUTH_CODE) @@ -99,9 +98,6 @@ class HangoutsFlowHandler(config_entries.ConfigFlow): errors = {} if user_input is not None: - from hangups import get_auth - from .hangups_utils import GoogleAuthError - self._credentials.set_verification_code(user_input[CONF_2FA]) try: await self.hass.async_add_executor_job( diff --git a/homeassistant/components/hangouts/hangouts_bot.py b/homeassistant/components/hangouts/hangouts_bot.py index 9eee2ca2be3..8575a547a9c 100644 --- a/homeassistant/components/hangouts/hangouts_bot.py +++ b/homeassistant/components/hangouts/hangouts_bot.py @@ -4,6 +4,8 @@ import io import logging import aiohttp +import hangups +from hangups import ChatMessageEvent, ChatMessageSegment, Client, get_auth, hangouts_pb2 from homeassistant.helpers import dispatcher, intent from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -24,6 +26,7 @@ from .const import ( EVENT_HANGOUTS_MESSAGE_RECEIVED, INTENT_HELP, ) +from .hangups_utils import HangoutsCredentials, HangoutsRefreshToken _LOGGER = logging.getLogger(__name__) @@ -126,8 +129,6 @@ class HangoutsBot: ) async def _async_handle_conversation_event(self, event): - from hangups import ChatMessageEvent - if isinstance(event, ChatMessageEvent): dispatcher.async_dispatcher_send( self.hass, @@ -196,11 +197,6 @@ class HangoutsBot: async def async_connect(self): """Login to the Google Hangouts.""" - from .hangups_utils import HangoutsRefreshToken, HangoutsCredentials - - from hangups import Client - from hangups import get_auth - session = await self.hass.async_add_executor_job( get_auth, HangoutsCredentials(None, None, None), @@ -252,8 +248,6 @@ class HangoutsBot: if not conversations: return False - from hangups import ChatMessageSegment, hangouts_pb2 - messages = [] for segment in message: if messages: @@ -306,8 +300,6 @@ class HangoutsBot: await conv.send_message(messages, image_file) async def _async_list_conversations(self): - import hangups - ( self._user_list, self._conversation_list, diff --git a/homeassistant/components/harmony/const.py b/homeassistant/components/harmony/const.py new file mode 100644 index 00000000000..12e71050665 --- /dev/null +++ b/homeassistant/components/harmony/const.py @@ -0,0 +1,4 @@ +"""Constants for the Harmony component.""" +DOMAIN = "harmony" +SERVICE_SYNC = "sync" +SERVICE_CHANGE_CHANNEL = "change_channel" diff --git a/homeassistant/components/harmony/remote.py b/homeassistant/components/harmony/remote.py index 118af7fe34a..c48d5fb00b0 100644 --- a/homeassistant/components/harmony/remote.py +++ b/homeassistant/components/harmony/remote.py @@ -19,7 +19,6 @@ from homeassistant.components.remote import ( ATTR_HOLD_SECS, ATTR_NUM_REPEATS, DEFAULT_DELAY_SECS, - DOMAIN, PLATFORM_SCHEMA, ) from homeassistant.const import ( @@ -33,6 +32,8 @@ from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.util import slugify +from .const import DOMAIN, SERVICE_CHANGE_CHANNEL, SERVICE_SYNC + _LOGGER = logging.getLogger(__name__) ATTR_CHANNEL = "channel" @@ -42,9 +43,6 @@ DEFAULT_PORT = 8088 DEVICES = [] CONF_DEVICE_CACHE = "harmony_device_cache" -SERVICE_SYNC = "harmony_sync" -SERVICE_CHANGE_CHANNEL = "harmony_change_channel" - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional(ATTR_ACTIVITY): cv.string, @@ -316,7 +314,6 @@ class HarmonyRemote(remote.RemoteDevice): except aioexc.TimeOut: _LOGGER.error("%s: Powering off timed-out", self.name) - # pylint: disable=arguments-differ async def async_send_command(self, command, **kwargs): """Send a list of commands to one device.""" _LOGGER.debug("%s: Send Command", self.name) @@ -372,7 +369,7 @@ class HarmonyRemote(remote.RemoteDevice): for result in result_list: _LOGGER.error( - "Sending command %s to device %s failed with code " "%s: %s", + "Sending command %s to device %s failed with code %s: %s", result.command.command, result.command.device, result.code, diff --git a/homeassistant/components/harmony/services.yaml b/homeassistant/components/harmony/services.yaml index e69de29bb2d..1b9ae225c7f 100644 --- a/homeassistant/components/harmony/services.yaml +++ b/homeassistant/components/harmony/services.yaml @@ -0,0 +1,16 @@ +sync: + description: Syncs the remote's configuration. + fields: + entity_id: + description: Name(s) of entities to sync. + example: 'remote.family_room' + +change_channel: + description: Sends change channel command to the Harmony HUB + fields: + entity_id: + description: Name(s) of Harmony remote entities to send change channel command to + example: 'remote.family_room' + channel: + description: Channel number to change to + example: '200' \ No newline at end of file diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index e0c0a57375a..dab0fadd922 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -10,9 +10,9 @@ from homeassistant.components.homeassistant import SERVICE_CHECK_CONFIG import homeassistant.config as conf_util from homeassistant.const import ( ATTR_NAME, + EVENT_CORE_CONFIG_UPDATE, SERVICE_HOMEASSISTANT_RESTART, SERVICE_HOMEASSISTANT_STOP, - EVENT_CORE_CONFIG_UPDATE, ) from homeassistant.core import DOMAIN as HASS_DOMAIN, callback from homeassistant.exceptions import HomeAssistantError @@ -20,8 +20,8 @@ import homeassistant.helpers.config_validation as cv from homeassistant.loader import bind_hass from homeassistant.util.dt import utcnow -from .auth import async_setup_auth_view from .addon_panel import async_setup_addon_panel +from .auth import async_setup_auth_view from .discovery import async_setup_discovery_view from .handler import HassIO, HassioAPIError from .http import HassIOView diff --git a/homeassistant/components/hassio/addon_panel.py b/homeassistant/components/hassio/addon_panel.py index b60c864a893..cb509cb19a1 100644 --- a/homeassistant/components/hassio/addon_panel.py +++ b/homeassistant/components/hassio/addon_panel.py @@ -7,7 +7,7 @@ from aiohttp import web from homeassistant.components.http import HomeAssistantView from homeassistant.helpers.typing import HomeAssistantType -from .const import ATTR_PANELS, ATTR_TITLE, ATTR_ICON, ATTR_ADMIN, ATTR_ENABLE +from .const import ATTR_ADMIN, ATTR_ENABLE, ATTR_ICON, ATTR_PANELS, ATTR_TITLE from .handler import HassioAPIError _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/hassio/auth.py b/homeassistant/components/hassio/auth.py index 19e4c63b5a5..800801b4350 100644 --- a/homeassistant/components/hassio/auth.py +++ b/homeassistant/components/hassio/auth.py @@ -1,18 +1,18 @@ """Implement the auth feature from Hass.io for Add-ons.""" +from ipaddress import ip_address import logging import os -from ipaddress import ip_address -import voluptuous as vol from aiohttp import web from aiohttp.web_exceptions import HTTPForbidden, HTTPNotFound +import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.const import KEY_REAL_IP from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import HomeAssistantType from .const import ATTR_ADDON, ATTR_PASSWORD, ATTR_USERNAME diff --git a/homeassistant/components/hassio/discovery.py b/homeassistant/components/hassio/discovery.py index 55336735133..fc6efbe0e58 100644 --- a/homeassistant/components/hassio/discovery.py +++ b/homeassistant/components/hassio/discovery.py @@ -5,9 +5,9 @@ import logging from aiohttp import web from aiohttp.web_exceptions import HTTPServiceUnavailable +from homeassistant.components.http import HomeAssistantView from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.core import callback -from homeassistant.components.http import HomeAssistantView from .const import ( ATTR_ADDON, diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index 3b1b8374510..ddb9269219b 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -7,7 +7,7 @@ from typing import Dict, Union import aiohttp from aiohttp import web -from aiohttp.hdrs import CONTENT_TYPE, CONTENT_LENGTH +from aiohttp.hdrs import CONTENT_LENGTH, CONTENT_TYPE from aiohttp.web_exceptions import HTTPBadGateway import async_timeout diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py index 53235f80dca..c69d2078468 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -1,8 +1,8 @@ """Hass.io Add-on ingress service.""" import asyncio +from ipaddress import ip_address import logging import os -from ipaddress import ip_address from typing import Dict, Union import aiohttp diff --git a/homeassistant/components/haveibeenpwned/sensor.py b/homeassistant/components/haveibeenpwned/sensor.py index 7fa3f422300..99f94499478 100644 --- a/homeassistant/components/haveibeenpwned/sensor.py +++ b/homeassistant/components/haveibeenpwned/sensor.py @@ -7,7 +7,7 @@ import requests import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_EMAIL, CONF_API_KEY, ATTR_ATTRIBUTION +from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_EMAIL import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import track_point_in_time @@ -178,7 +178,7 @@ class HaveIBeenPwnedData: else: _LOGGER.error( - "Failed fetching data for %s" "(HTTP Status_code = %d)", + "Failed fetching data for %s (HTTP Status_code = %d)", self._email, req.status_code, ) diff --git a/homeassistant/components/hddtemp/sensor.py b/homeassistant/components/hddtemp/sensor.py index d0dd5018dca..a1052b0440a 100644 --- a/homeassistant/components/hddtemp/sensor.py +++ b/homeassistant/components/hddtemp/sensor.py @@ -1,21 +1,21 @@ """Support for getting the disk temperature of a host.""" -import logging from datetime import timedelta -from telnetlib import Telnet +import logging import socket +from telnetlib import Telnet import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, + CONF_DISKS, CONF_HOST, + CONF_NAME, CONF_PORT, TEMP_CELSIUS, TEMP_FAHRENHEIT, - CONF_DISKS, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/hdmi_cec/__init__.py b/homeassistant/components/hdmi_cec/__init__.py index d1637f96d95..b460020546f 100644 --- a/homeassistant/components/hdmi_cec/__init__.py +++ b/homeassistant/components/hdmi_cec/__init__.py @@ -4,6 +4,25 @@ from functools import reduce import logging import multiprocessing +from pycec.cec import CecAdapter +from pycec.commands import CecCommand, KeyPressCommand, KeyReleaseCommand +from pycec.const import ( + ADDR_AUDIOSYSTEM, + ADDR_BROADCAST, + ADDR_UNREGISTERED, + KEY_MUTE_OFF, + KEY_MUTE_ON, + KEY_MUTE_TOGGLE, + KEY_VOLUME_DOWN, + KEY_VOLUME_UP, + POWER_OFF, + POWER_ON, + STATUS_PLAY, + STATUS_STILL, + STATUS_STOP, +) +from pycec.network import HDMINetwork, PhysicalAddress +from pycec.tcp import TcpAdapter import voluptuous as vol from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER @@ -155,8 +174,6 @@ def parse_mapping(mapping, parents=None): parents = [] for addr, val in mapping.items(): if isinstance(addr, (str,)) and isinstance(val, (str,)): - from pycec.network import PhysicalAddress - yield (addr, PhysicalAddress(val)) else: cur = parents + [addr] @@ -168,20 +185,6 @@ def parse_mapping(mapping, parents=None): def setup(hass: HomeAssistant, base_config): """Set up the CEC capability.""" - from pycec.network import HDMINetwork - from pycec.commands import CecCommand, KeyReleaseCommand, KeyPressCommand - from pycec.const import ( - KEY_VOLUME_UP, - KEY_VOLUME_DOWN, - KEY_MUTE_ON, - KEY_MUTE_OFF, - KEY_MUTE_TOGGLE, - ADDR_AUDIOSYSTEM, - ADDR_BROADCAST, - ADDR_UNREGISTERED, - ) - from pycec.cec import CecAdapter - from pycec.tcp import TcpAdapter # Parse configuration into a dict of device name to physical address # represented as a list of four elements. @@ -278,8 +281,6 @@ def setup(hass: HomeAssistant, base_config): def _select_device(call): """Select the active device.""" - from pycec.network import PhysicalAddress - addr = call.data[ATTR_DEVICE] if not addr: _LOGGER.error("Device not found: %s", call.data[ATTR_DEVICE]) @@ -366,14 +367,6 @@ class CecDevice(Entity): def update(self): """Update device status.""" device = self._device - from pycec.const import ( - STATUS_PLAY, - STATUS_STOP, - STATUS_STILL, - POWER_OFF, - POWER_ON, - ) - if device.power_status in [POWER_OFF, 3]: self._state = STATE_OFF elif device.status == STATUS_PLAY: diff --git a/homeassistant/components/hdmi_cec/media_player.py b/homeassistant/components/hdmi_cec/media_player.py index 379105430bc..42c5f0b456c 100644 --- a/homeassistant/components/hdmi_cec/media_player.py +++ b/homeassistant/components/hdmi_cec/media_player.py @@ -1,6 +1,27 @@ """Support for HDMI CEC devices as media players.""" import logging +from pycec.commands import CecCommand, KeyPressCommand, KeyReleaseCommand +from pycec.const import ( + KEY_BACKWARD, + KEY_FORWARD, + KEY_MUTE_TOGGLE, + KEY_PAUSE, + KEY_PLAY, + KEY_STOP, + KEY_VOLUME_DOWN, + KEY_VOLUME_UP, + POWER_OFF, + POWER_ON, + STATUS_PLAY, + STATUS_STILL, + STATUS_STOP, + TYPE_AUDIO, + TYPE_PLAYBACK, + TYPE_RECORDER, + TYPE_TUNER, +) + from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( DOMAIN, @@ -50,8 +71,6 @@ class CecPlayerDevice(CecDevice, MediaPlayerDevice): def send_keypress(self, key): """Send keypress to CEC adapter.""" - from pycec.commands import KeyPressCommand, KeyReleaseCommand - _LOGGER.debug( "Sending keypress %s to device %s", hex(key), hex(self._logical_address) ) @@ -60,20 +79,14 @@ class CecPlayerDevice(CecDevice, MediaPlayerDevice): def send_playback(self, key): """Send playback status to CEC adapter.""" - from pycec.commands import CecCommand - self._device.async_send_command(CecCommand(key, dst=self._logical_address)) def mute_volume(self, mute): """Mute volume.""" - from pycec.const import KEY_MUTE_TOGGLE - self.send_keypress(KEY_MUTE_TOGGLE) def media_previous_track(self): """Go to previous track.""" - from pycec.const import KEY_BACKWARD - self.send_keypress(KEY_BACKWARD) def turn_on(self): @@ -92,8 +105,6 @@ class CecPlayerDevice(CecDevice, MediaPlayerDevice): def media_stop(self): """Stop playback.""" - from pycec.const import KEY_STOP - self.send_keypress(KEY_STOP) self._state = STATE_IDLE @@ -103,8 +114,6 @@ class CecPlayerDevice(CecDevice, MediaPlayerDevice): def media_next_track(self): """Skip to next track.""" - from pycec.const import KEY_FORWARD - self.send_keypress(KEY_FORWARD) def media_seek(self, position): @@ -117,8 +126,6 @@ class CecPlayerDevice(CecDevice, MediaPlayerDevice): def media_pause(self): """Pause playback.""" - from pycec.const import KEY_PAUSE - self.send_keypress(KEY_PAUSE) self._state = STATE_PAUSED @@ -128,22 +135,16 @@ class CecPlayerDevice(CecDevice, MediaPlayerDevice): def media_play(self): """Start playback.""" - from pycec.const import KEY_PLAY - self.send_keypress(KEY_PLAY) self._state = STATE_PLAYING def volume_up(self): """Increase volume.""" - from pycec.const import KEY_VOLUME_UP - _LOGGER.debug("%s: volume up", self._logical_address) self.send_keypress(KEY_VOLUME_UP) def volume_down(self): """Decrease volume.""" - from pycec.const import KEY_VOLUME_DOWN - _LOGGER.debug("%s: volume down", self._logical_address) self.send_keypress(KEY_VOLUME_DOWN) @@ -155,14 +156,6 @@ class CecPlayerDevice(CecDevice, MediaPlayerDevice): def update(self): """Update device status.""" device = self._device - from pycec.const import ( - STATUS_PLAY, - STATUS_STOP, - STATUS_STILL, - POWER_OFF, - POWER_ON, - ) - if device.power_status in [POWER_OFF, 3]: self._state = STATE_OFF elif not self.support_pause: @@ -180,8 +173,6 @@ class CecPlayerDevice(CecDevice, MediaPlayerDevice): @property def supported_features(self): """Flag media player features that are supported.""" - from pycec.const import TYPE_RECORDER, TYPE_PLAYBACK, TYPE_TUNER, TYPE_AUDIO - if self.type_id == TYPE_RECORDER or self.type == TYPE_PLAYBACK: return ( SUPPORT_TURN_ON diff --git a/homeassistant/components/heatmiser/climate.py b/homeassistant/components/heatmiser/climate.py index c9bed1e9d34..553ae8f4bc3 100644 --- a/homeassistant/components/heatmiser/climate.py +++ b/homeassistant/components/heatmiser/climate.py @@ -1,54 +1,64 @@ """Support for the PRT Heatmiser themostats using the V3 protocol.""" import logging +from typing import List +from heatmiserV3 import connection, heatmiser import voluptuous as vol -from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate import ( + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + PLATFORM_SCHEMA, + ClimateDevice, +) from homeassistant.components.climate.const import SUPPORT_TARGET_TEMPERATURE from homeassistant.const import ( - TEMP_CELSIUS, ATTR_TEMPERATURE, - CONF_PORT, - CONF_NAME, + CONF_HOST, CONF_ID, + CONF_NAME, + CONF_PORT, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, ) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -CONF_IPADDRESS = "ipaddress" -CONF_TSTATS = "tstats" +CONF_THERMOSTATS = "tstats" TSTATS_SCHEMA = vol.Schema( - {vol.Required(CONF_ID): cv.string, vol.Required(CONF_NAME): cv.string} + vol.All( + cv.ensure_list, + [{vol.Required(CONF_ID): cv.positive_int, vol.Required(CONF_NAME): cv.string}], + ) ) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { - vol.Required(CONF_IPADDRESS): cv.string, - vol.Required(CONF_PORT): cv.port, - vol.Required(CONF_TSTATS, default={}): vol.Schema({cv.string: TSTATS_SCHEMA}), + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PORT): cv.string, + vol.Optional(CONF_THERMOSTATS, default=[]): TSTATS_SCHEMA, } ) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the heatmiser thermostat.""" - from heatmiserV3 import heatmiser, connection - ipaddress = config.get(CONF_IPADDRESS) - port = str(config.get(CONF_PORT)) - tstats = config.get(CONF_TSTATS) + heatmiser_v3_thermostat = heatmiser.HeatmiserThermostat - serport = connection.connection(ipaddress, port) - serport.open() + host = config[CONF_HOST] + port = config[CONF_PORT] + + thermostats = config[CONF_THERMOSTATS] + + uh1_hub = connection.HeatmiserUH1(host, port) add_entities( [ - HeatmiserV3Thermostat( - heatmiser, tstat.get(CONF_ID), tstat.get(CONF_NAME), serport - ) - for tstat in tstats.values() + HeatmiserV3Thermostat(heatmiser_v3_thermostat, thermostat, uh1_hub) + for thermostat in thermostats ], True, ) @@ -57,15 +67,17 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class HeatmiserV3Thermostat(ClimateDevice): """Representation of a HeatmiserV3 thermostat.""" - def __init__(self, heatmiser, device, name, serport): + def __init__(self, therm, device, uh1): """Initialize the thermostat.""" - self.heatmiser = heatmiser - self.serport = serport + self.therm = therm(device[CONF_ID], "prt", uh1) + self.uh1 = uh1 + self._name = device[CONF_NAME] self._current_temperature = None self._target_temperature = None - self._name = name self._id = device self.dcb = None + self._hvac_mode = HVAC_MODE_HEAT + self._temperature_unit = None @property def supported_features(self): @@ -80,7 +92,23 @@ class HeatmiserV3Thermostat(ClimateDevice): @property def temperature_unit(self): """Return the unit of measurement which this thermostat uses.""" - return TEMP_CELSIUS + return self._temperature_unit + + @property + def hvac_mode(self) -> str: + """Return hvac operation ie. heat, cool mode. + + Need to be one of HVAC_MODE_*. + """ + return self._hvac_mode + + @property + def hvac_modes(self) -> List[str]: + """Return the list of available hvac operation modes. + + Need to be a subset of HVAC_MODES. + """ + return [HVAC_MODE_HEAT, HVAC_MODE_OFF] @property def current_temperature(self): @@ -95,12 +123,25 @@ class HeatmiserV3Thermostat(ClimateDevice): def set_temperature(self, **kwargs): """Set new target temperature.""" temperature = kwargs.get(ATTR_TEMPERATURE) - self.heatmiser.hmSendAddress(self._id, 18, temperature, 1, self.serport) + self._target_temperature = int(temperature) + self.therm.set_target_temp(self._target_temperature) def update(self): """Get the latest data.""" - self.dcb = self.heatmiser.hmReadAddress(self._id, "prt", self.serport) - low = self.dcb.get("floortemplow ") - high = self.dcb.get("floortemphigh") - self._current_temperature = (high * 256 + low) / 10.0 - self._target_temperature = int(self.dcb.get("roomset")) + self.uh1.reopen() + if not self.uh1.status: + _LOGGER.error("Failed to update device %s", self._name) + return + self.dcb = self.therm.read_dcb() + self._temperature_unit = ( + TEMP_CELSIUS + if (self.therm.get_temperature_format() == "C") + else TEMP_FAHRENHEIT + ) + self._current_temperature = int(self.therm.get_floor_temp()) + self._target_temperature = int(self.therm.get_target_temp()) + self._hvac_mode = ( + HVAC_MODE_OFF + if (int(self.therm.get_current_state()) == 0) + else HVAC_MODE_HEAT + ) diff --git a/homeassistant/components/heatmiser/manifest.json b/homeassistant/components/heatmiser/manifest.json index b3882c63c51..89bcec08125 100644 --- a/homeassistant/components/heatmiser/manifest.json +++ b/homeassistant/components/heatmiser/manifest.json @@ -3,8 +3,10 @@ "name": "Heatmiser", "documentation": "https://www.home-assistant.io/integrations/heatmiser", "requirements": [ - "heatmiserV3==0.9.1" + "heatmiserV3==1.1.18" ], "dependencies": [], - "codeowners": [] -} + "codeowners": [ + "@andylockran" + ] +} \ No newline at end of file diff --git a/homeassistant/components/heos/config_flow.py b/homeassistant/components/heos/config_flow.py index 4380cb4d8ba..7e7fe067874 100644 --- a/homeassistant/components/heos/config_flow.py +++ b/homeassistant/components/heos/config_flow.py @@ -1,9 +1,12 @@ """Config flow to configure Heos.""" +from urllib.parse import urlparse + from pyheos import Heos, HeosError import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_HOST, CONF_NAME +from homeassistant.components import ssdp +from homeassistant.const import CONF_HOST from .const import DATA_DISCOVERED_HOSTS, DOMAIN @@ -23,11 +26,12 @@ class HeosFlowHandler(config_entries.ConfigFlow): async def async_step_ssdp(self, discovery_info): """Handle a discovered Heos device.""" # Store discovered host + hostname = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]).hostname friendly_name = "{} ({})".format( - discovery_info[CONF_NAME], discovery_info[CONF_HOST] + discovery_info[ssdp.ATTR_UPNP_FRIENDLY_NAME], hostname ) self.hass.data.setdefault(DATA_DISCOVERED_HOSTS, {}) - self.hass.data[DATA_DISCOVERED_HOSTS][friendly_name] = discovery_info[CONF_HOST] + self.hass.data[DATA_DISCOVERED_HOSTS][friendly_name] = hostname # Abort if other flows in progress or an entry already exists if self._async_in_progress() or self._async_current_entries(): return self.async_abort(reason="already_setup") diff --git a/homeassistant/components/here_travel_time/__init__.py b/homeassistant/components/here_travel_time/__init__.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/here_travel_time/manifest.json b/homeassistant/components/here_travel_time/manifest.json old mode 100755 new mode 100644 index 0f2bde253de..da2c03b1ac8 --- a/homeassistant/components/here_travel_time/manifest.json +++ b/homeassistant/components/here_travel_time/manifest.json @@ -3,7 +3,7 @@ "name": "HERE travel time", "documentation": "https://www.home-assistant.io/integrations/here_travel_time", "requirements": [ - "herepy==0.6.3.1" + "herepy==2.0.0" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/here_travel_time/sensor.py b/homeassistant/components/here_travel_time/sensor.py old mode 100755 new mode 100644 index 0b688a770c5..e482943eff3 --- a/homeassistant/components/here_travel_time/sensor.py +++ b/homeassistant/components/here_travel_time/sensor.py @@ -32,8 +32,7 @@ CONF_DESTINATION_ENTITY_ID = "destination_entity_id" CONF_ORIGIN_LATITUDE = "origin_latitude" CONF_ORIGIN_LONGITUDE = "origin_longitude" CONF_ORIGIN_ENTITY_ID = "origin_entity_id" -CONF_APP_ID = "app_id" -CONF_APP_CODE = "app_code" +CONF_API_KEY = "api_key" CONF_TRAFFIC_MODE = "traffic_mode" CONF_ROUTE_MODE = "route_mode" @@ -97,8 +96,7 @@ PLATFORM_SCHEMA = vol.All( cv.has_at_least_one_key(CONF_ORIGIN_LATITUDE, CONF_ORIGIN_ENTITY_ID), PLATFORM_SCHEMA.extend( { - vol.Required(CONF_APP_ID): cv.string, - vol.Required(CONF_APP_CODE): cv.string, + vol.Required(CONF_API_KEY): cv.string, vol.Inclusive( CONF_DESTINATION_LATITUDE, "destination_coordinates" ): cv.latitude, @@ -131,9 +129,8 @@ async def async_setup_platform( ) -> None: """Set up the HERE travel time platform.""" - app_id = config[CONF_APP_ID] - app_code = config[CONF_APP_CODE] - here_client = herepy.RoutingApi(app_id, app_code) + api_key = config[CONF_API_KEY] + here_client = herepy.RoutingApi(api_key) if not await hass.async_add_executor_job( _are_valid_client_credentials, here_client diff --git a/homeassistant/components/hikvision/binary_sensor.py b/homeassistant/components/hikvision/binary_sensor.py index b898f5d860c..9db91217300 100644 --- a/homeassistant/components/hikvision/binary_sensor.py +++ b/homeassistant/components/hikvision/binary_sensor.py @@ -1,24 +1,26 @@ """Support for Hikvision event stream events represented as binary sensors.""" -import logging from datetime import timedelta +import logging + +from pyhik.hikvision import HikCamera import voluptuous as vol -from homeassistant.helpers.event import track_point_in_utc_time -from homeassistant.util.dt import utcnow -from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA -import homeassistant.helpers.config_validation as cv +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice from homeassistant.const import ( - CONF_HOST, - CONF_PORT, - CONF_NAME, - CONF_USERNAME, - CONF_PASSWORD, - CONF_SSL, - EVENT_HOMEASSISTANT_STOP, - EVENT_HOMEASSISTANT_START, ATTR_LAST_TRIP_TIME, CONF_CUSTOMIZE, + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_SSL, + CONF_USERNAME, + EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP, ) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import track_point_in_utc_time +from homeassistant.util.dt import utcnow _LOGGER = logging.getLogger(__name__) @@ -135,7 +137,6 @@ class HikvisionData: def __init__(self, hass, url, port, name, username, password): """Initialize the data object.""" - from pyhik.hikvision import HikCamera self._url = url self._port = port diff --git a/homeassistant/components/hikvisioncam/switch.py b/homeassistant/components/hikvisioncam/switch.py index 020b894c0f7..f86853a5468 100644 --- a/homeassistant/components/hikvisioncam/switch.py +++ b/homeassistant/components/hikvisioncam/switch.py @@ -5,7 +5,7 @@ import hikvision.api from hikvision.error import HikvisionError, MissingParamError import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -16,7 +16,6 @@ from homeassistant.const import ( STATE_ON, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import ToggleEntity # This is the last working version, please test before updating @@ -60,7 +59,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([HikvisionMotionSwitch(name, hikvision_cam)]) -class HikvisionMotionSwitch(ToggleEntity): +class HikvisionMotionSwitch(SwitchDevice): """Representation of a switch to toggle on/off motion detection.""" def __init__(self, name, hikvision_cam): diff --git a/homeassistant/components/hisense_aehw4a1/.translations/bg.json b/homeassistant/components/hisense_aehw4a1/.translations/bg.json new file mode 100644 index 00000000000..c758e9cc20d --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/.translations/bg.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u0412 \u043c\u0440\u0435\u0436\u0430\u0442\u0430 \u043d\u0435 \u0441\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Hisense AEH-W4A1.", + "single_instance_allowed": "\u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0430 Hisense AEH-W4A1." + }, + "step": { + "confirm": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 Hisense AEH-W4A1?", + "title": "Hisense AEH-W4A1" + } + }, + "title": "Hisense AEH-W4A1" + } +} \ No newline at end of file diff --git a/homeassistant/components/hisense_aehw4a1/.translations/da.json b/homeassistant/components/hisense_aehw4a1/.translations/da.json new file mode 100644 index 00000000000..3d479543231 --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/.translations/da.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Ingen Hisense AEH-W4A1-enheder fundet p\u00e5 netv\u00e6rket.", + "single_instance_allowed": "Kun en enkelt konfiguration af Hisense AEH-W4A1 er mulig." + }, + "step": { + "confirm": { + "description": "Vil du konfigurere Hisense AEH-W4A1?", + "title": "Hisense AEH-W4A1" + } + }, + "title": "Hisense AEH-W4A1" + } +} \ No newline at end of file diff --git a/homeassistant/components/hisense_aehw4a1/.translations/de.json b/homeassistant/components/hisense_aehw4a1/.translations/de.json new file mode 100644 index 00000000000..8b474ea0418 --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/.translations/de.json @@ -0,0 +1,15 @@ +{ + "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." + }, + "step": { + "confirm": { + "description": "M\u00f6chten Sie Hisense AEH-W4A1 einrichten?", + "title": "Hisense AEH-W4A1" + } + }, + "title": "Hisense AEH-W4A1" + } +} \ No newline at end of file diff --git a/homeassistant/components/hisense_aehw4a1/.translations/es.json b/homeassistant/components/hisense_aehw4a1/.translations/es.json new file mode 100644 index 00000000000..69f071bf5d8 --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/.translations/es.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "No se encontraron dispositivos Hisense AEH-W4A1 en la red.", + "single_instance_allowed": "Solo es posible una \u00fanica configuraci\u00f3n de Hisense AEH-W4A1." + }, + "step": { + "confirm": { + "description": "\u00bfDesea configurar Hisense AEH-W4A1?", + "title": "Hisense AEH-W4A1" + } + }, + "title": "Hisense AEH-W4A1" + } +} \ No newline at end of file diff --git a/homeassistant/components/hisense_aehw4a1/.translations/fr.json b/homeassistant/components/hisense_aehw4a1/.translations/fr.json new file mode 100644 index 00000000000..50c753538c7 --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/.translations/fr.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Aucun p\u00e9riph\u00e9rique AEH-W4A1 trouv\u00e9 sur le r\u00e9seau.", + "single_instance_allowed": "Une seule configuration de AEH-W4A1 est possible." + }, + "step": { + "confirm": { + "description": "Voulez-vous configurer AEH-W4A1?", + "title": "Hisense AEH-W4A1" + } + }, + "title": "Hisense AEH-W4A1" + } +} \ No newline at end of file diff --git a/homeassistant/components/hisense_aehw4a1/.translations/it.json b/homeassistant/components/hisense_aehw4a1/.translations/it.json new file mode 100644 index 00000000000..b584d18e8bf --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/.translations/it.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nessun dispositivo Hisense AEH-W4A1 trovato sulla rete.", + "single_instance_allowed": "\u00c8 consentita solo una configurazione di Hisense AEH-W4A1" + }, + "step": { + "confirm": { + "description": "Voui configurare Hisense AEH-W4A1", + "title": "Hisense AEH-W4A1" + } + }, + "title": "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 new file mode 100644 index 00000000000..6d8b6b4b44c --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/.translations/ko.json @@ -0,0 +1,15 @@ +{ + "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." + }, + "step": { + "confirm": { + "description": "Hisense AEH-W4A1 \uc744 \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "Hisense AEH-W4A1" + } + }, + "title": "Hisense AEH-W4A1" + } +} \ No newline at end of file diff --git a/homeassistant/components/hisense_aehw4a1/.translations/lb.json b/homeassistant/components/hisense_aehw4a1/.translations/lb.json new file mode 100644 index 00000000000..33b93348300 --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/.translations/lb.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Keng Hisense AEH-W4A1 Apparater am Netzwierk fonnt.", + "single_instance_allowed": "N\u00ebmmen eng eenzeg Konfiguratioun vun Hisense AEH-W4A1 ass m\u00e9iglech." + }, + "step": { + "confirm": { + "description": "Soll Hisense AEH-W4A1 konfigur\u00e9iert ginn?", + "title": "Hisense AEH-W4A1" + } + }, + "title": "Hisense AEH-W4A1" + } +} \ No newline at end of file diff --git a/homeassistant/components/hisense_aehw4a1/.translations/nl.json b/homeassistant/components/hisense_aehw4a1/.translations/nl.json new file mode 100644 index 00000000000..7360908a11d --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/.translations/nl.json @@ -0,0 +1,15 @@ +{ + "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." + }, + "step": { + "confirm": { + "description": "Wilt u Hisense AEH-W4A1 instellen?", + "title": "Hisense AEH-W4A1" + } + }, + "title": "Hisense AEH-W4A1" + } +} \ No newline at end of file diff --git a/homeassistant/components/hisense_aehw4a1/.translations/no.json b/homeassistant/components/hisense_aehw4a1/.translations/no.json new file mode 100644 index 00000000000..e44e818ea60 --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/.translations/no.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Ingen Hisense AEH-W4A1-enheter funnet p\u00e5 nettverket.", + "single_instance_allowed": "Bare en enkelt konfigurasjon av Hisense AEH-W4A1 er mulig." + }, + "step": { + "confirm": { + "description": "Vil du konfigurere Hisense AEH-W4A1?", + "title": "Hisense AEH-W4A1" + } + }, + "title": "Hisense AEH-W4A1" + } +} \ No newline at end of file diff --git a/homeassistant/components/hisense_aehw4a1/.translations/pl.json b/homeassistant/components/hisense_aehw4a1/.translations/pl.json new file mode 100644 index 00000000000..e0ab5cddbda --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/.translations/pl.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nie znaleziono w sieci urz\u0105dze\u0144 Hisense AEH-W4A1.", + "single_instance_allowed": "Dozwolona jest tylko jedna konfiguracja Hisense AEH-W4A1." + }, + "step": { + "confirm": { + "description": "Chcesz skonfigurowa\u0107 AEH-W4A1?", + "title": "Hisense AEH-W4A1" + } + }, + "title": "Hisense AEH-W4A1" + } +} \ No newline at end of file diff --git a/homeassistant/components/hisense_aehw4a1/.translations/sl.json b/homeassistant/components/hisense_aehw4a1/.translations/sl.json new file mode 100644 index 00000000000..3c15eecf6e1 --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/.translations/sl.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "V omre\u017eju ni bilo najdenih naprav Hisense AEH-W4A1.", + "single_instance_allowed": "Mo\u017ena je samo ena konfiguracija Hisense AEH-W4A1." + }, + "step": { + "confirm": { + "description": "Ali \u017eelite nastaviti Hisense AEH-W4A1?", + "title": "Hisense AEH-W4A1" + } + }, + "title": "Hisense AEH-W4A1" + } +} \ No newline at end of file diff --git a/homeassistant/components/hisense_aehw4a1/.translations/zh-Hant.json b/homeassistant/components/hisense_aehw4a1/.translations/zh-Hant.json new file mode 100644 index 00000000000..d4f87905da9 --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/.translations/zh-Hant.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u6d77\u4fe1 AEH-W4A1 \u8a2d\u5099\u3002", + "single_instance_allowed": "\u50c5\u5141\u8a31\u8a2d\u5b9a\u4e00\u7d44\u6d77\u4fe1 AEH-W4A1\u3002" + }, + "step": { + "confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a\u6d77\u4fe1 AEH-W4A1\uff1f", + "title": "\u6d77\u4fe1 AEH-W4A1" + } + }, + "title": "\u6d77\u4fe1 AEH-W4A1" + } +} \ No newline at end of file diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index 65607d0f8bf..7fcbf519bf3 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -5,22 +5,23 @@ from itertools import groupby import logging import time +from sqlalchemy import and_, func import voluptuous as vol +from homeassistant.components import recorder +from homeassistant.components.http import HomeAssistantView +from homeassistant.components.recorder.models import States +from homeassistant.components.recorder.util import execute, session_scope from homeassistant.const import ( - HTTP_BAD_REQUEST, + ATTR_HIDDEN, CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, CONF_INCLUDE, + HTTP_BAD_REQUEST, ) -import homeassistant.util.dt as dt_util -from homeassistant.components import recorder, script -from homeassistant.components.http import HomeAssistantView -from homeassistant.const import ATTR_HIDDEN -from homeassistant.components.recorder.util import session_scope, execute import homeassistant.helpers.config_validation as cv - +import homeassistant.util.dt as dt_util # mypy: allow-untyped-defs, no-check-untyped-defs @@ -58,7 +59,6 @@ def get_significant_states( thermostat so that we get current temperature in our graphs). """ timer_start = time.perf_counter() - from homeassistant.components.recorder.models import States with session_scope(hass=hass) as session: query = session.query(States).filter( @@ -94,7 +94,6 @@ def get_significant_states( def state_changes_during_period(hass, start_time, end_time=None, entity_id=None): """Return states changes during UTC period start_time - end_time.""" - from homeassistant.components.recorder.models import States with session_scope(hass=hass) as session: query = session.query(States).filter( @@ -117,7 +116,6 @@ def state_changes_during_period(hass, start_time, end_time=None, entity_id=None) def get_last_state_changes(hass, number_of_states, entity_id): """Return the last number_of_states.""" - from homeassistant.components.recorder.models import States start_time = dt_util.utcnow() @@ -142,7 +140,6 @@ def get_last_state_changes(hass, number_of_states, entity_id): def get_states(hass, utc_point_in_time, entity_ids=None, run=None, filters=None): """Return the states at a specific point in time.""" - from homeassistant.components.recorder.models import States if run is None: run = recorder.run_information(hass, utc_point_in_time) @@ -151,8 +148,6 @@ def get_states(hass, utc_point_in_time, entity_ids=None, run=None, filters=None) if run is None: return [] - from sqlalchemy import and_, func - with session_scope(hass=hass) as session: query = session.query(States) @@ -386,7 +381,6 @@ class Filters: * if include and exclude is defined - select the entities specified in the include and filter out the ones from the exclude list. """ - from homeassistant.components.recorder.models import States # specific entities requested - do not in/exclude anything if entity_ids is not None: @@ -436,4 +430,4 @@ def _is_significant(state): Will only test for things that are not filtered out in SQL. """ # scripts that are not cancellable will never change state - return state.domain != "script" or state.attributes.get(script.ATTR_CAN_CANCEL) + return state.domain != "script" or state.attributes.get("can_cancel") diff --git a/homeassistant/components/history_graph/__init__.py b/homeassistant/components/history_graph/__init__.py index ad8398c75f5..2b89556818f 100644 --- a/homeassistant/components/history_graph/__init__.py +++ b/homeassistant/components/history_graph/__init__.py @@ -3,8 +3,8 @@ import logging import voluptuous as vol +from homeassistant.const import ATTR_ENTITY_ID, CONF_ENTITIES, CONF_NAME import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_ENTITIES, CONF_NAME, ATTR_ENTITY_ID from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent diff --git a/homeassistant/components/history_stats/sensor.py b/homeassistant/components/history_stats/sensor.py index 5c59b5f8e97..3eb604b3957 100644 --- a/homeassistant/components/history_stats/sensor.py +++ b/homeassistant/components/history_stats/sensor.py @@ -5,21 +5,21 @@ import math import voluptuous as vol -from homeassistant.core import callback from homeassistant.components import history -import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt_util from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, CONF_ENTITY_ID, + CONF_NAME, CONF_STATE, CONF_TYPE, EVENT_HOMEASSISTANT_START, ) +from homeassistant.core import 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 +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -45,7 +45,7 @@ def exactly_two_period_keys(conf): """Ensure exactly 2 of CONF_PERIOD_KEYS are provided.""" if sum(param in conf for param in CONF_PERIOD_KEYS) != 2: raise vol.Invalid( - "You must provide exactly 2 of the following:" " start, end, duration" + "You must provide exactly 2 of the following: start, end, duration" ) return conf @@ -262,7 +262,7 @@ class HistoryStatsSensor(Entity): ) except ValueError: _LOGGER.error( - "Parsing error: start must be a datetime" "or a timestamp" + "Parsing error: start must be a datetime or a timestamp" ) return @@ -281,7 +281,7 @@ class HistoryStatsSensor(Entity): ) except ValueError: _LOGGER.error( - "Parsing error: end must be a datetime " "or a timestamp" + "Parsing error: end must be a datetime or a timestamp" ) return diff --git a/homeassistant/components/hitron_coda/device_tracker.py b/homeassistant/components/hitron_coda/device_tracker.py index 2f3526d45b6..12b03acbcc5 100644 --- a/homeassistant/components/hitron_coda/device_tracker.py +++ b/homeassistant/components/hitron_coda/device_tracker.py @@ -1,17 +1,17 @@ """Support for the Hitron CODA-4582U, provided by Rogers.""" -import logging from collections import namedtuple +import logging import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner, ) -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_TYPE +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_TYPE, CONF_USERNAME +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/hive/binary_sensor.py b/homeassistant/components/hive/binary_sensor.py index ce7e53b77a5..fa91d6862a2 100644 --- a/homeassistant/components/hive/binary_sensor.py +++ b/homeassistant/components/hive/binary_sensor.py @@ -1,7 +1,7 @@ """Support for the Hive binary sensors.""" from homeassistant.components.binary_sensor import BinarySensorDevice -from . import DOMAIN, DATA_HIVE, HiveEntity +from . import DATA_HIVE, DOMAIN, HiveEntity DEVICETYPE_DEVICE_CLASS = {"motionsensor": "motion", "contactsensor": "opening"} diff --git a/homeassistant/components/hive/climate.py b/homeassistant/components/hive/climate.py index ed13e3019ce..202cea7bf8e 100644 --- a/homeassistant/components/hive/climate.py +++ b/homeassistant/components/hive/climate.py @@ -1,6 +1,9 @@ """Support for the Hive climate devices.""" from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + CURRENT_HVAC_OFF, HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_OFF, @@ -8,14 +11,10 @@ from homeassistant.components.climate.const import ( PRESET_NONE, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, - CURRENT_HVAC_IDLE, - CURRENT_HVAC_OFF, - CURRENT_HVAC_HEAT, ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS - -from . import DOMAIN, DATA_HIVE, HiveEntity, refresh_system +from . import DATA_HIVE, DOMAIN, HiveEntity, refresh_system HIVE_TO_HASS_STATE = { "SCHEDULE": HVAC_MODE_AUTO, diff --git a/homeassistant/components/hive/light.py b/homeassistant/components/hive/light.py index 41fc286d13b..33175de543d 100644 --- a/homeassistant/components/hive/light.py +++ b/homeassistant/components/hive/light.py @@ -10,7 +10,7 @@ from homeassistant.components.light import ( ) import homeassistant.util.color as color_util -from . import DOMAIN, DATA_HIVE, HiveEntity, refresh_system +from . import DATA_HIVE, DOMAIN, HiveEntity, refresh_system def setup_platform(hass, config, add_entities, discovery_info=None): diff --git a/homeassistant/components/hive/sensor.py b/homeassistant/components/hive/sensor.py index ccd635015de..360fb61bfbe 100644 --- a/homeassistant/components/hive/sensor.py +++ b/homeassistant/components/hive/sensor.py @@ -2,7 +2,7 @@ from homeassistant.const import TEMP_CELSIUS from homeassistant.helpers.entity import Entity -from . import DOMAIN, DATA_HIVE, HiveEntity +from . import DATA_HIVE, DOMAIN, HiveEntity FRIENDLY_NAMES = { "Hub_OnlineStatus": "Hive Hub Status", diff --git a/homeassistant/components/hive/switch.py b/homeassistant/components/hive/switch.py index 1447f5483a4..53e1ec6a069 100644 --- a/homeassistant/components/hive/switch.py +++ b/homeassistant/components/hive/switch.py @@ -1,7 +1,7 @@ """Support for the Hive switches.""" from homeassistant.components.switch import SwitchDevice -from . import DOMAIN, DATA_HIVE, HiveEntity, refresh_system +from . import DATA_HIVE, DOMAIN, HiveEntity, refresh_system def setup_platform(hass, config, add_entities, discovery_info=None): diff --git a/homeassistant/components/hive/water_heater.py b/homeassistant/components/hive/water_heater.py index c60a9ec01d1..d7d98426df5 100644 --- a/homeassistant/components/hive/water_heater.py +++ b/homeassistant/components/hive/water_heater.py @@ -7,7 +7,8 @@ from homeassistant.components.water_heater import ( WaterHeaterDevice, ) from homeassistant.const import TEMP_CELSIUS -from . import DOMAIN, DATA_HIVE, HiveEntity, refresh_system + +from . import DATA_HIVE, DOMAIN, HiveEntity, refresh_system SUPPORT_FLAGS_HEATER = SUPPORT_OPERATION_MODE diff --git a/homeassistant/components/hlk_sw16/__init__.py b/homeassistant/components/hlk_sw16/__init__.py index f174b00613b..e7264c4e0dd 100644 --- a/homeassistant/components/hlk_sw16/__init__.py +++ b/homeassistant/components/hlk_sw16/__init__.py @@ -1,23 +1,24 @@ """Support for HLK-SW16 relay switches.""" import logging +from hlk_sw16 import create_hlk_sw16_connection import voluptuous as vol from homeassistant.const import ( CONF_HOST, - CONF_PORT, - EVENT_HOMEASSISTANT_STOP, - CONF_SWITCHES, CONF_NAME, + CONF_PORT, + CONF_SWITCHES, + EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import ( - async_dispatcher_send, async_dispatcher_connect, + async_dispatcher_send, ) +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -59,7 +60,6 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass, config): """Set up the HLK-SW16 switch.""" # Allow platform to specify function to register new unknown devices - from hlk_sw16 import create_hlk_sw16_connection hass.data[DATA_DEVICE_REGISTER] = {} diff --git a/homeassistant/components/homeassistant/__init__.py b/homeassistant/components/homeassistant/__init__.py index d2d6abdadb5..8aa1d7e020a 100644 --- a/homeassistant/components/homeassistant/__init__.py +++ b/homeassistant/components/homeassistant/__init__.py @@ -6,23 +6,22 @@ from typing import Awaitable import voluptuous as vol -import homeassistant.core as ha import homeassistant.config as conf_util -from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.service import async_extract_entity_ids -from homeassistant.helpers import intent from homeassistant.const import ( ATTR_ENTITY_ID, - SERVICE_TURN_ON, - SERVICE_TURN_OFF, - SERVICE_TOGGLE, - SERVICE_HOMEASSISTANT_STOP, - SERVICE_HOMEASSISTANT_RESTART, - RESTART_EXIT_CODE, ATTR_LATITUDE, ATTR_LONGITUDE, + RESTART_EXIT_CODE, + SERVICE_HOMEASSISTANT_RESTART, + SERVICE_HOMEASSISTANT_STOP, + SERVICE_TOGGLE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, ) -from homeassistant.helpers import config_validation as cv +import homeassistant.core as ha +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import config_validation as cv, intent +from homeassistant.helpers.service import async_extract_entity_ids _LOGGER = logging.getLogger(__name__) DOMAIN = ha.DOMAIN diff --git a/homeassistant/components/homeassistant/scene.py b/homeassistant/components/homeassistant/scene.py index c505d1534de..af271a069fe 100644 --- a/homeassistant/components/homeassistant/scene.py +++ b/homeassistant/components/homeassistant/scene.py @@ -4,6 +4,8 @@ import logging import voluptuous as vol +from homeassistant import config as conf_util +from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN, STATES, Scene from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_STATE, @@ -11,21 +13,19 @@ from homeassistant.const import ( CONF_ID, CONF_NAME, CONF_PLATFORM, + SERVICE_RELOAD, STATE_OFF, STATE_ON, - SERVICE_RELOAD, ) -from homeassistant.core import State, DOMAIN as HA_DOMAIN -from homeassistant import config as conf_util +from homeassistant.core import DOMAIN as HA_DOMAIN, State from homeassistant.exceptions import HomeAssistantError -from homeassistant.loader import async_get_integration from homeassistant.helpers import ( config_per_platform, config_validation as cv, entity_platform, ) from homeassistant.helpers.state import async_reproduce_state -from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN, STATES, Scene +from homeassistant.loader import async_get_integration def _convert_states(states): @@ -55,7 +55,22 @@ def _convert_states(states): return result +def _ensure_no_intersection(value): + """Validate that entities and snapshot_entities do not overlap.""" + if ( + CONF_SNAPSHOT not in value + or CONF_ENTITIES not in value + or not any( + entity_id in value[CONF_SNAPSHOT] for entity_id in value[CONF_ENTITIES] + ) + ): + return value + + raise vol.Invalid("entities and snapshot_entities must not overlap") + + CONF_SCENE_ID = "scene_id" +CONF_SNAPSHOT = "snapshot_entities" STATES_SCHEMA = vol.All(dict, _convert_states) @@ -75,8 +90,16 @@ PLATFORM_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -CREATE_SCENE_SCHEMA = vol.Schema( - {vol.Required(CONF_SCENE_ID): cv.slug, vol.Required(CONF_ENTITIES): STATES_SCHEMA} +CREATE_SCENE_SCHEMA = vol.All( + cv.has_at_least_one_key(CONF_ENTITIES, CONF_SNAPSHOT), + _ensure_no_intersection, + vol.Schema( + { + vol.Required(CONF_SCENE_ID): cv.slug, + vol.Optional(CONF_ENTITIES, default={}): STATES_SCHEMA, + vol.Optional(CONF_SNAPSHOT, default=[]): cv.entity_ids, + } + ), ) SERVICE_APPLY = "apply" @@ -139,7 +162,24 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def create_service(call): """Create a scene.""" - scene_config = SCENECONFIG(call.data[CONF_SCENE_ID], call.data[CONF_ENTITIES]) + snapshot = call.data[CONF_SNAPSHOT] + entities = call.data[CONF_ENTITIES] + + for entity_id in snapshot: + state = hass.states.get(entity_id) + if state is None: + _LOGGER.warning( + "Entity %s does not exist and therefore cannot be snapshotted", + entity_id, + ) + continue + entities[entity_id] = State(entity_id, state.state, state.attributes) + + if not entities: + _LOGGER.warning("Empty scenes are not allowed") + return + + scene_config = SCENECONFIG(call.data[CONF_SCENE_ID], entities) entity_id = f"{SCENE_DOMAIN}.{scene_config.name}" old = platform.entities.get(entity_id) if old is not None: diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index bb525271cec..ca5a601068a 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -45,8 +45,8 @@ from .const import ( DEVICE_CLASS_PM25, DOMAIN, HOMEKIT_FILE, - SERVICE_HOMEKIT_START, SERVICE_HOMEKIT_RESET_ACCESSORY, + SERVICE_HOMEKIT_START, TYPE_FAUCET, TYPE_OUTLET, TYPE_SHOWER, @@ -54,7 +54,6 @@ from .const import ( TYPE_SWITCH, TYPE_VALVE, ) - from .util import ( show_setup_message, validate_entity_config, @@ -303,6 +302,7 @@ class HomeKit: def setup(self): """Set up bridge and accessory driver.""" + # pylint: disable=import-outside-toplevel from .accessories import HomeBridge, HomeDriver self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.stop) @@ -328,7 +328,7 @@ class HomeKit: aid = generate_aid(entity_id) if aid not in self.bridge.accessories: _LOGGER.warning( - "Could not reset accessory. entity_id " "not found %s", entity_id + "Could not reset accessory. entity_id not found %s", entity_id ) continue acc = self.remove_bridge_accessory(aid) @@ -362,8 +362,7 @@ class HomeKit: return self.status = STATUS_WAIT - # pylint: disable=unused-import - from . import ( # noqa: F401 + from . import ( # noqa: F401 pylint: disable=unused-import, import-outside-toplevel type_covers, type_fans, type_lights, diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 84f0b7894c4..ddcc795d262 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -105,8 +105,18 @@ class HomeAccessory(Accessory): battery_found = self.hass.states.get(self.entity_id).attributes.get( ATTR_BATTERY_LEVEL ) + if self.linked_battery_sensor: - battery_found = self.hass.states.get(self.linked_battery_sensor).state + state = self.hass.states.get(self.linked_battery_sensor) + if state is not None: + battery_found = state.state + else: + self.linked_battery_sensor = None + _LOGGER.warning( + "%s: Battery sensor state missing: %s", + self.entity_id, + self.linked_battery_sensor, + ) if battery_found is None: return diff --git a/homeassistant/components/homekit/type_fans.py b/homeassistant/components/homekit/type_fans.py index e3fa6c42c58..e6d128d1e28 100644 --- a/homeassistant/components/homekit/type_fans.py +++ b/homeassistant/components/homekit/type_fans.py @@ -88,8 +88,11 @@ class Fan(HomeAccessory): ) if CHAR_ROTATION_SPEED in chars: + # Initial value is set to 100 because 0 is a special value (off). 100 is + # an arbitrary non-zero value. It is updated immediately by update_state + # to set to the correct initial value. self.char_speed = serv_fan.configure_char( - CHAR_ROTATION_SPEED, value=0, setter_callback=self.set_speed + CHAR_ROTATION_SPEED, value=100, setter_callback=self.set_speed ) if CHAR_SWING_MODE in chars: @@ -156,7 +159,22 @@ class Fan(HomeAccessory): speed = new_state.attributes.get(ATTR_SPEED) hk_speed_value = self.speed_mapping.speed_to_homekit(speed) if hk_speed_value is not None and self.char_speed.value != hk_speed_value: - self.char_speed.set_value(hk_speed_value) + # If the homeassistant component reports its speed as the first entry + # in its speed list but is not off, the hk_speed_value is 0. But 0 + # is a special value in homekit. When you turn on a homekit accessory + # it will try to restore the last rotation speed state which will be + # the last value saved by char_speed.set_value. But if it is set to + # 0, HomeKit will update the rotation speed to 100 as it thinks 0 is + # off. + # + # Therefore, if the hk_speed_value is 0 and the device is still on, + # the rotation speed is mapped to 1 otherwise the update is ignored + # in order to avoid this incorrect behavior. + if hk_speed_value == 0: + if state == STATE_ON: + self.char_speed.set_value(1) + else: + self.char_speed.set_value(hk_speed_value) # Handle Oscillating if self.char_swing is not None: diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index 8e1b07fbbff..7f195b276d6 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -82,8 +82,11 @@ class Light(HomeAccessory): ) if CHAR_BRIGHTNESS in self.chars: + # Initial value is set to 100 because 0 is a special value (off). 100 is + # an arbitrary non-zero value. It is updated immediately by update_state + # to set to the correct initial value. self.char_brightness = serv_light.configure_char( - CHAR_BRIGHTNESS, value=0, setter_callback=self.set_brightness + CHAR_BRIGHTNESS, value=100, setter_callback=self.set_brightness ) if CHAR_COLOR_TEMPERATURE in self.chars: min_mireds = self.hass.states.get(self.entity_id).attributes.get( @@ -183,7 +186,21 @@ class Light(HomeAccessory): if not self._flag[CHAR_BRIGHTNESS] and isinstance(brightness, int): brightness = round(brightness / 255 * 100, 0) if self.char_brightness.value != brightness: - self.char_brightness.set_value(brightness) + # The homeassistant component might report its brightness as 0 but is + # not off. But 0 is a special value in homekit. When you turn on a + # homekit accessory it will try to restore the last brightness state + # which will be the last value saved by char_brightness.set_value. + # But if it is set to 0, HomeKit will update the brightness to 100 as + # it thinks 0 is off. + # + # Therefore, if the the brighness is 0 and the device is still on, + # the brightness is mapped to 1 otherwise the update is ignored in + # order to avoid this incorrect behavior. + if brightness == 0: + if state == STATE_ON: + self.char_brightness.set_value(1) + else: + self.char_brightness.set_value(brightness) self._flag[CHAR_BRIGHTNESS] = False # Handle color temperature diff --git a/homeassistant/components/homekit/type_media_players.py b/homeassistant/components/homekit/type_media_players.py index d9b24782610..9942c42a0de 100644 --- a/homeassistant/components/homekit/type_media_players.py +++ b/homeassistant/components/homekit/type_media_players.py @@ -6,16 +6,16 @@ from pyhap.const import CATEGORY_SWITCH, CATEGORY_TELEVISION from homeassistant.components.media_player import ( ATTR_INPUT_SOURCE, ATTR_INPUT_SOURCE_LIST, - ATTR_MEDIA_VOLUME_MUTED, ATTR_MEDIA_VOLUME_LEVEL, - SERVICE_SELECT_SOURCE, + ATTR_MEDIA_VOLUME_MUTED, DOMAIN, + SERVICE_SELECT_SOURCE, SUPPORT_PAUSE, SUPPORT_PLAY, + SUPPORT_SELECT_SOURCE, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, - SUPPORT_SELECT_SOURCE, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -26,13 +26,13 @@ from homeassistant.const import ( SERVICE_MEDIA_STOP, SERVICE_TURN_OFF, SERVICE_TURN_ON, - SERVICE_VOLUME_MUTE, - SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN, + SERVICE_VOLUME_MUTE, SERVICE_VOLUME_SET, + SERVICE_VOLUME_UP, STATE_OFF, - STATE_PLAYING, STATE_PAUSED, + STATE_PLAYING, STATE_UNKNOWN, ) @@ -46,23 +46,23 @@ from .const import ( CHAR_IDENTIFIER, CHAR_INPUT_SOURCE_TYPE, CHAR_IS_CONFIGURED, - CHAR_NAME, - CHAR_SLEEP_DISCOVER_MODE, CHAR_MUTE, + CHAR_NAME, CHAR_ON, CHAR_REMOTE_KEY, + CHAR_SLEEP_DISCOVER_MODE, + CHAR_VOLUME, CHAR_VOLUME_CONTROL_TYPE, CHAR_VOLUME_SELECTOR, - CHAR_VOLUME, CONF_FEATURE_LIST, FEATURE_ON_OFF, FEATURE_PLAY_PAUSE, FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE, + SERV_INPUT_SOURCE, SERV_SWITCH, SERV_TELEVISION, SERV_TELEVISION_SPEAKER, - SERV_INPUT_SOURCE, ) _LOGGER = logging.getLogger(__name__) @@ -422,7 +422,7 @@ class TelevisionMediaPlayer(HomeAccessory): self.char_input_source.set_value(index) else: _LOGGER.warning( - "%s: Sources out of sync. " "Restart HomeAssistant", + "%s: Sources out of sync. Restart HomeAssistant", self.entity_id, ) self.char_input_source.set_value(0) diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index 9adc3cc0600..79a9d156f10 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -7,6 +7,7 @@ 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_TARGET_TEMP_HIGH, @@ -19,7 +20,9 @@ from homeassistant.components.climate.const import ( DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, DOMAIN as DOMAIN_CLIMATE, + HVAC_MODE_AUTO, HVAC_MODE_COOL, + HVAC_MODE_FAN_ONLY, HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, @@ -60,13 +63,18 @@ from .util import temperature_to_homekit, temperature_to_states _LOGGER = logging.getLogger(__name__) +HC_HOMEKIT_VALID_MODES_WATER_HEATER = { + "Heat": 1, +} UNIT_HASS_TO_HOMEKIT = {TEMP_CELSIUS: 0, TEMP_FAHRENHEIT: 1} UNIT_HOMEKIT_TO_HASS = {c: s for s, c in UNIT_HASS_TO_HOMEKIT.items()} HC_HASS_TO_HOMEKIT = { HVAC_MODE_OFF: 0, HVAC_MODE_HEAT: 1, HVAC_MODE_COOL: 2, + HVAC_MODE_AUTO: 3, HVAC_MODE_HEAT_COOL: 3, + HVAC_MODE_FAN_ONLY: 2, } HC_HOMEKIT_TO_HASS = {c: s for s, c in HC_HASS_TO_HOMEKIT.items()} @@ -97,9 +105,9 @@ class Thermostat(HomeAccessory): # Add additional characteristics if auto mode is supported self.chars = [] - features = self.hass.states.get(self.entity_id).attributes.get( - ATTR_SUPPORTED_FEATURES, 0 - ) + state = self.hass.states.get(self.entity_id) + features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + if features & SUPPORT_TARGET_TEMPERATURE_RANGE: self.chars.extend( (CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_HEATING_THRESHOLD_TEMPERATURE) @@ -107,12 +115,44 @@ class Thermostat(HomeAccessory): serv_thermostat = self.add_preload_service(SERV_THERMOSTAT, self.chars) - # Current and target mode characteristics + # Current mode characteristics self.char_current_heat_cool = serv_thermostat.configure_char( CHAR_CURRENT_HEATING_COOLING, value=0 ) + + # Target mode characteristics + hc_modes = state.attributes.get(ATTR_HVAC_MODES, None) + if hc_modes is None: + _LOGGER.error( + "%s: HVAC modes not yet available. Please disable auto start for homekit.", + self.entity_id, + ) + hc_modes = ( + HVAC_MODE_HEAT, + HVAC_MODE_COOL, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, + ) + + # determine available modes for this entity, prefer AUTO over HEAT_COOL and COOL over FAN_ONLY + self.hc_homekit_to_hass = { + c: s + for s, c in HC_HASS_TO_HOMEKIT.items() + if ( + s in hc_modes + and not ( + (s == HVAC_MODE_HEAT_COOL and HVAC_MODE_AUTO in hc_modes) + or (s == HVAC_MODE_FAN_ONLY and HVAC_MODE_COOL in hc_modes) + ) + ) + } + hc_valid_values = {k: v for v, k in self.hc_homekit_to_hass.items()} + self.char_target_heat_cool = serv_thermostat.configure_char( - CHAR_TARGET_HEATING_COOLING, value=0, setter_callback=self.set_heat_cool + CHAR_TARGET_HEATING_COOLING, + value=0, + setter_callback=self.set_heat_cool, + valid_values=hc_valid_values, ) # Current and target temperature characteristics @@ -185,7 +225,7 @@ class Thermostat(HomeAccessory): """Change operation mode to value if call came from HomeKit.""" _LOGGER.debug("%s: Set heat-cool to %d", self.entity_id, value) self._flag_heat_cool = True - hass_value = HC_HOMEKIT_TO_HASS[value] + hass_value = self.hc_homekit_to_hass[value] params = {ATTR_ENTITY_ID: self.entity_id, ATTR_HVAC_MODE: hass_value} self.call_service( DOMAIN_CLIMATE, SERVICE_SET_HVAC_MODE_THERMOSTAT, params, hass_value @@ -318,7 +358,10 @@ class WaterHeater(HomeAccessory): CHAR_CURRENT_HEATING_COOLING, value=1 ) self.char_target_heat_cool = serv_thermostat.configure_char( - CHAR_TARGET_HEATING_COOLING, value=1, setter_callback=self.set_heat_cool + CHAR_TARGET_HEATING_COOLING, + value=1, + setter_callback=self.set_heat_cool, + valid_values=HC_HOMEKIT_VALID_MODES_WATER_HEATER, ) self.char_current_temp = serv_thermostat.configure_char( diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 608c9a974e5..0fe97cfca63 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -103,7 +103,7 @@ def validate_entity_config(values): if not isinstance(config, dict): raise vol.Invalid( - "The configuration for {} must be " " a dictionary.".format(entity) + "The configuration for {} must be a dictionary.".format(entity) ) if domain in ("alarm_control_panel", "lock"): diff --git a/homeassistant/components/homekit_controller/.translations/bg.json b/homeassistant/components/homekit_controller/.translations/bg.json index f8ce05b4bbe..b1909ca2ec0 100644 --- a/homeassistant/components/homekit_controller/.translations/bg.json +++ b/homeassistant/components/homekit_controller/.translations/bg.json @@ -24,7 +24,7 @@ "data": { "pairing_code": "\u041a\u043e\u0434 \u0437\u0430 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, - "description": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 HomeKit \u043a\u043e\u0434\u0430 \u0437\u0430 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0437\u0430 \u0434\u0430 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0442\u0435 \u0442\u043e\u0432\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e", + "description": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 HomeKit \u043a\u043e\u0434\u0430 \u0437\u0430 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 (\u0432\u044a\u0432 \u0444\u043e\u0440\u043c\u0430\u0442 XXX-XX-XXX) \u0437\u0430 \u0434\u0430 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0442\u0435 \u0442\u043e\u0432\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e", "title": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 HomeKit \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" }, "user": { diff --git a/homeassistant/components/homekit_controller/.translations/da.json b/homeassistant/components/homekit_controller/.translations/da.json index 2bcda4fb1ad..20b209752eb 100644 --- a/homeassistant/components/homekit_controller/.translations/da.json +++ b/homeassistant/components/homekit_controller/.translations/da.json @@ -3,38 +3,38 @@ "abort": { "accessory_not_found_error": "Parring kan ikke tilf\u00f8jes da enheden ikke l\u00e6ngere findes.", "already_configured": "Tilbeh\u00f8ret er allerede konfigureret med denne controller.", - "already_in_progress": "Enheds konfiguration er allerede i gang.", + "already_in_progress": "Enhedskonfiguration er allerede i gang.", "already_paired": "Dette tilbeh\u00f8r er allerede parret med en anden enhed. Nulstil tilbeh\u00f8ret og pr\u00f8v igen.", - "ignored_model": "HomeKit underst\u00f8ttelse til denne model er blokeret da en mere komplet native integration er til r\u00e5dighed.", + "ignored_model": "HomeKit-underst\u00f8ttelse af denne model er blokeret, da en mere funktionskomplet indbygget integration er tilg\u00e6ngelig.", "invalid_config_entry": "Denne enhed vises som klar til parring, men der er allerede en modstridende konfigurationspost for den i Home Assistant, som f\u00f8rst skal fjernes.", "no_devices": "Der blev ikke fundet nogen uparrede enheder" }, "error": { - "authentication_error": "Forkert HomeKit kode. Kontroller den og pr\u00f8v igen.", - "busy_error": "Enheden n\u00e6gtede at tilf\u00f8je parring da den allerede parrer med en anden controller.", - "max_peers_error": "Enheden n\u00e6gtede at tilf\u00f8je parring da den ikke har nok frit parrings lager.", - "max_tries_error": "Enheden n\u00e6gtede at tilf\u00f8je parring da den har modtaget mere end 100 mislykkede godkendelsesfors\u00f8g.", + "authentication_error": "Forkert HomeKit-kode. Kontroller den og pr\u00f8v igen.", + "busy_error": "Enheden n\u00e6gtede at parre da den allerede er parret med en anden controller.", + "max_peers_error": "Enheden n\u00e6gtede at parre da den ikke har nok frit parringslagerplads.", + "max_tries_error": "Enheden n\u00e6gtede at parre da den har modtaget mere end 100 mislykkede godkendelsesfors\u00f8g.", "pairing_failed": "En uh\u00e5ndteret fejl opstod under fors\u00f8g p\u00e5 at parre med denne enhed. Dette kan v\u00e6re en midlertidig fejl eller din enhed muligvis ikke underst\u00f8ttes i \u00f8jeblikket.", "unable_to_pair": "Kunne ikke parre, pr\u00f8v venligst igen.", "unknown_error": "Enhed rapporterede en ukendt fejl. Parring mislykkedes." }, - "flow_title": "HomeKit tilbeh\u00f8r: {name}", + "flow_title": "HomeKit-tilbeh\u00f8r: {name}", "step": { "pair": { "data": { "pairing_code": "Parringskode" }, - "description": "Indtast din HomeKit parringskode (i formatet XXX-XX-XXX) for at bruge dette tilbeh\u00f8r", - "title": "Par med HomeKit tilbeh\u00f8r" + "description": "Indtast din HomeKit-parringskode (i formatet XXX-XX-XXX) for at bruge dette tilbeh\u00f8r", + "title": "Par med HomeKit-tilbeh\u00f8r" }, "user": { "data": { "device": "Enhed" }, "description": "V\u00e6lg den enhed du vil parre med", - "title": "Par med HomeKit tilbeh\u00f8r" + "title": "Par med HomeKit-tilbeh\u00f8r" } }, - "title": "HomeKit tilbeh\u00f8r" + "title": "HomeKit-tilbeh\u00f8r" } } \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 4ca54099d24..444f64b6f38 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -5,15 +5,13 @@ import homekit from homekit.model.characteristics import CharacteristicsTypes from homeassistant.core import callback -from homeassistant.helpers.entity import Entity from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.entity import Entity -# We need an import from .config_flow, without it .config_flow is never loaded. -from .config_flow import HomekitControllerFlowHandler # noqa: F401 -from .connection import get_accessory_information, HKDevice -from .const import CONTROLLER, ENTITY_MAP, KNOWN_DEVICES -from .const import DOMAIN +from .config_flow import normalize_hkid +from .connection import HKDevice, get_accessory_information +from .const import CONTROLLER, DOMAIN, ENTITY_MAP, KNOWN_DEVICES from .storage import EntityMapStorage _LOGGER = logging.getLogger(__name__) @@ -109,7 +107,6 @@ class HomeKitEntity(Entity): setup_fn = getattr(self, f"_setup_{setup_fn_name}", None) if not setup_fn: return - # pylint: disable=not-callable setup_fn(char) @callback @@ -132,7 +129,6 @@ class HomeKitEntity(Entity): if not update_fn: continue - # pylint: disable=not-callable update_fn(result["value"]) self.async_write_ha_state() @@ -184,6 +180,12 @@ async def async_setup_entry(hass, entry): conn = HKDevice(hass, entry, entry.data) hass.data[KNOWN_DEVICES][conn.unique_id] = conn + # For backwards compat + if entry.unique_id is None: + hass.config_entries.async_update_entry( + entry, unique_id=normalize_hkid(conn.unique_id) + ) + if not await conn.async_setup(): del hass.data[KNOWN_DEVICES][conn.unique_id] raise ConfigEntryNotReady diff --git a/homeassistant/components/homekit_controller/alarm_control_panel.py b/homeassistant/components/homekit_controller/alarm_control_panel.py index bb45a6c33d9..8cdbe9b2f36 100644 --- a/homeassistant/components/homekit_controller/alarm_control_panel.py +++ b/homeassistant/components/homekit_controller/alarm_control_panel.py @@ -4,6 +4,11 @@ import logging from homekit.model.characteristics import CharacteristicsTypes from homeassistant.components.alarm_control_panel import AlarmControlPanel +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, +) from homeassistant.const import ( ATTR_BATTERY_LEVEL, STATE_ALARM_ARMED_AWAY, @@ -88,6 +93,11 @@ class HomeKitAlarmControlPanel(HomeKitEntity, AlarmControlPanel): """Return the state of the device.""" return self._state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT + async def async_alarm_disarm(self, code=None): """Send disarm command.""" await self.set_alarm_state(STATE_ALARM_DISARMED, code) diff --git a/homeassistant/components/homekit_controller/binary_sensor.py b/homeassistant/components/homekit_controller/binary_sensor.py index 1e1c8ef5d44..2998ce18641 100644 --- a/homeassistant/components/homekit_controller/binary_sensor.py +++ b/homeassistant/components/homekit_controller/binary_sensor.py @@ -3,7 +3,10 @@ import logging from homekit.model.characteristics import CharacteristicsTypes -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_SMOKE, + BinarySensorDevice, +) from . import KNOWN_DEVICES, HomeKitEntity @@ -57,7 +60,37 @@ class HomeKitContactSensor(HomeKitEntity, BinarySensorDevice): return self._state == 1 -ENTITY_TYPES = {"motion": HomeKitMotionSensor, "contact": HomeKitContactSensor} +class HomeKitSmokeSensor(HomeKitEntity, BinarySensorDevice): + """Representation of a Homekit smoke sensor.""" + + def __init__(self, *args): + """Initialise the entity.""" + super().__init__(*args) + self._state = None + + @property + def device_class(self) -> str: + """Return the class of this sensor.""" + return DEVICE_CLASS_SMOKE + + def get_characteristic_types(self): + """Define the homekit characteristics the entity is tracking.""" + return [CharacteristicsTypes.SMOKE_DETECTED] + + def _update_smoke_detected(self, value): + self._state = value + + @property + def is_on(self): + """Return true if smoke is currently detected.""" + return self._state == 1 + + +ENTITY_TYPES = { + "motion": HomeKitMotionSensor, + "contact": HomeKitContactSensor, + "smoke": HomeKitSmokeSensor, +} async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py index 1f9118ff838..d0ab7bd2e99 100644 --- a/homeassistant/components/homekit_controller/climate.py +++ b/homeassistant/components/homekit_controller/climate.py @@ -4,20 +4,20 @@ import logging from homekit.model.characteristics import CharacteristicsTypes from homeassistant.components.climate import ( - ClimateDevice, - DEFAULT_MIN_HUMIDITY, DEFAULT_MAX_HUMIDITY, + DEFAULT_MIN_HUMIDITY, + ClimateDevice, ) from homeassistant.components.climate.const import ( - HVAC_MODE_HEAT_COOL, + CURRENT_HVAC_COOL, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, HVAC_MODE_COOL, HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, - CURRENT_HVAC_IDLE, - CURRENT_HVAC_HEAT, - CURRENT_HVAC_COOL, - SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_HUMIDITY, + SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 40bf87d6f0a..507a5cbb70a 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -1,22 +1,25 @@ """Config flow to configure homekit_controller.""" -import os import json import logging +import os +import re import homekit +from homekit.controller.ip_implementation import IpPairing import voluptuous as vol from homeassistant import config_entries from homeassistant.core import callback +from .connection import get_accessory_name, get_bridge_information from .const import DOMAIN, KNOWN_DEVICES -from .connection import get_bridge_information, get_accessory_name - HOMEKIT_IGNORE = ["Home Assistant Bridge"] HOMEKIT_DIR = ".homekit" PAIRING_FILE = "pairing.json" +PIN_FORMAT = re.compile(r"^(\d{3})-{0,1}(\d{2})-{0,1}(\d{3})$") + _LOGGER = logging.getLogger(__name__) @@ -46,6 +49,11 @@ def load_old_pairings(hass): return old_pairings +def normalize_hkid(hkid): + """Normalize a hkid so that it is safe to compare with other normalized hkids.""" + return hkid.lower() + + @callback def find_existing_host(hass, serial): """Return a set of the configured hosts.""" @@ -54,6 +62,20 @@ def find_existing_host(hass, serial): return entry +def ensure_pin_format(pin): + """ + Ensure a pin code is correctly formatted. + + Ensures a pin code is in the format 111-11-111. Handles codes with and without dashes. + + If incorrect code is entered, an exception is raised. + """ + match = PIN_FORMAT.search(pin) + if not match: + raise homekit.exceptions.MalformedPinError(f"Invalid PIN code f{pin}") + return "{}-{}-{}".format(*match.groups()) + + @config_entries.HANDLERS.register(DOMAIN) class HomekitControllerFlowHandler(config_entries.ConfigFlow): """Handle a HomeKit config flow.""" @@ -77,6 +99,9 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): key = user_input["device"] self.hkid = self.devices[key]["id"] self.model = self.devices[key]["md"] + await self.async_set_unique_id( + normalize_hkid(self.hkid), raise_on_progress=False + ) return await self.async_step_pair() all_hosts = await self.hass.async_add_executor_job(self.controller.discover, 5) @@ -100,6 +125,38 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): ), ) + async def async_step_unignore(self, user_input): + """Rediscover a previously ignored discover.""" + unique_id = user_input["unique_id"] + await self.async_set_unique_id(unique_id) + + records = await self.hass.async_add_executor_job(self.controller.discover, 5) + for record in records: + if normalize_hkid(record["id"]) != unique_id: + continue + return await self.async_step_zeroconf( + { + "host": record["address"], + "port": record["port"], + "hostname": record["name"], + "type": "_hap._tcp.local.", + "name": record["name"], + "properties": { + "md": record["md"], + "pv": record["pv"], + "id": unique_id, + "c#": record["c#"], + "s#": record["s#"], + "ff": record["ff"], + "ci": record["ci"], + "sf": record["sf"], + "sh": "", + }, + } + ) + + return self.async_abort(reason="no_devices") + async def async_step_zeroconf(self, discovery_info): """Handle a discovered HomeKit accessory. @@ -120,18 +177,6 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): status_flags = int(properties["sf"]) paired = not status_flags & 0x01 - _LOGGER.debug("Discovered device %s (%s - %s)", name, model, hkid) - - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 - self.context["hkid"] = hkid - self.context["title_placeholders"] = {"name": name} - - # If multiple HomekitControllerFlowHandler end up getting created - # for the same accessory dont let duplicates hang around - active_flows = self._async_in_progress() - if any(hkid == flow["context"]["hkid"] for flow in active_flows): - return self.async_abort(reason="already_in_progress") - # The configuration number increases every time the characteristic map # needs updating. Some devices use a slightly off-spec name so handle # both cases. @@ -143,21 +188,27 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): ) config_num = None - if paired: - if hkid in self.hass.data.get(KNOWN_DEVICES, {}): - # The device is already paired and known to us - # According to spec we should monitor c# (config_num) for - # changes. If it changes, we check for new entities - conn = self.hass.data[KNOWN_DEVICES][hkid] - if conn.config_num != config_num: - _LOGGER.debug( - "HomeKit info %s: c# incremented, refreshing entities", hkid - ) - self.hass.async_create_task( - conn.async_refresh_entity_map(config_num) - ) - return self.async_abort(reason="already_configured") + # If the device is already paired and known to us we should monitor c# + # (config_num) for changes. If it changes, we check for new entities + if paired and hkid in self.hass.data.get(KNOWN_DEVICES, {}): + conn = self.hass.data[KNOWN_DEVICES][hkid] + if conn.config_num != config_num: + _LOGGER.debug( + "HomeKit info %s: c# incremented, refreshing entities", hkid + ) + self.hass.async_create_task(conn.async_refresh_entity_map(config_num)) + return self.async_abort(reason="already_configured") + _LOGGER.debug("Discovered device %s (%s - %s)", name, model, hkid) + + 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 + self.context["title_placeholders"] = {"name": name} + + if paired: old_pairings = await self.hass.async_add_executor_job( load_old_pairings, self.hass ) @@ -194,7 +245,6 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): async def async_import_legacy_pairing(self, discovery_props, pairing_data): """Migrate a legacy pairing to config entries.""" - from homekit.controller.ip_implementation import IpPairing hkid = discovery_props["id"] @@ -244,6 +294,8 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): if pair_info: code = pair_info["pairing_code"] try: + code = ensure_pin_format(code) + await self.hass.async_add_executor_job(self.finish_pairing, code) pairing = self.controller.pairings.get(self.hkid) @@ -251,6 +303,9 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): return await self._entry_from_accessory(pairing) errors["pairing_code"] = "unable_to_pair" + except homekit.exceptions.MalformedPinError: + # Library claimed pin was invalid before even making an API call + errors["pairing_code"] = "authentication_error" except homekit.AuthenticationError: # PairSetup M4 - SRP proof failed # PairSetup M6 - Ed25519 signature verification failed diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index 1cb2131fb8f..3ccfa8b0139 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -3,18 +3,18 @@ import asyncio import datetime import logging +from homekit.controller.ip_implementation import IpPairing from homekit.exceptions import ( AccessoryDisconnectedError, AccessoryNotFoundError, EncryptionError, ) -from homekit.model.services import ServicesTypes from homekit.model.characteristics import CharacteristicsTypes +from homekit.model.services import ServicesTypes from homeassistant.helpers.event import async_track_time_interval -from .const import DOMAIN, HOMEKIT_ACCESSORY_DISPATCH, ENTITY_MAP - +from .const import DOMAIN, ENTITY_MAP, HOMEKIT_ACCESSORY_DISPATCH DEFAULT_SCAN_INTERVAL = datetime.timedelta(seconds=60) RETRY_INTERVAL = 60 # seconds @@ -57,7 +57,6 @@ class HKDevice: def __init__(self, hass, config_entry, pairing_data): """Initialise a generic HomeKit device.""" - from homekit.controller.ip_implementation import IpPairing self.hass = hass self.config_entry = config_entry diff --git a/homeassistant/components/homekit_controller/const.py b/homeassistant/components/homekit_controller/const.py index 09a7df2a2bf..204f0e07d3e 100644 --- a/homeassistant/components/homekit_controller/const.py +++ b/homeassistant/components/homekit_controller/const.py @@ -26,4 +26,5 @@ HOMEKIT_ACCESSORY_DISPATCH = { "light": "sensor", "temperature": "sensor", "battery": "sensor", + "smoke": "binary_sensor", } diff --git a/homeassistant/components/homekit_controller/cover.py b/homeassistant/components/homekit_controller/cover.py index 0606778acb5..7e5591d9505 100644 --- a/homeassistant/components/homekit_controller/cover.py +++ b/homeassistant/components/homekit_controller/cover.py @@ -11,8 +11,8 @@ from homeassistant.components.cover import ( SUPPORT_OPEN, SUPPORT_OPEN_TILT, SUPPORT_SET_POSITION, - SUPPORT_STOP, SUPPORT_SET_TILT_POSITION, + SUPPORT_STOP, CoverDevice, ) from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING diff --git a/homeassistant/components/homekit_controller/storage.py b/homeassistant/components/homekit_controller/storage.py index 46d095b5631..ffc2da5fbf2 100644 --- a/homeassistant/components/homekit_controller/storage.py +++ b/homeassistant/components/homekit_controller/storage.py @@ -1,7 +1,7 @@ """Helpers for HomeKit data stored in HA storage.""" -from homeassistant.helpers.storage import Store from homeassistant.core import callback +from homeassistant.helpers.storage import Store from .const import DOMAIN diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 42fb73f6da2..d0776baa106 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -1,8 +1,9 @@ """Support for HomeMatic devices.""" -from datetime import timedelta, datetime +from datetime import datetime, timedelta from functools import partial import logging +from pyhomematic import HMConnection import voluptuous as vol from homeassistant.const import ( @@ -297,6 +298,7 @@ CONFIG_SCHEMA = vol.Schema( vol.Optional(CONF_HOSTS, default={}): { cv.match_all: { vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional( CONF_USERNAME, default=DEFAULT_USERNAME ): cv.string, @@ -366,7 +368,6 @@ SCHEMA_SERVICE_PUT_PARAMSET = vol.Schema( def setup(hass, config): """Set up the Homematic component.""" - from pyhomematic import HMConnection conf = config[DOMAIN] hass.data[DATA_CONF] = remotes = {} @@ -392,7 +393,7 @@ def setup(hass, config): for sname, sconfig in conf[CONF_HOSTS].items(): remotes[sname] = { "ip": sconfig.get(CONF_HOST), - "port": DEFAULT_PORT, + "port": sconfig[CONF_PORT], "username": sconfig.get(CONF_USERNAME), "password": sconfig.get(CONF_PASSWORD), "connect": False, diff --git a/homeassistant/components/homematic/manifest.json b/homeassistant/components/homematic/manifest.json index 5db547e3f0a..8a86fd19c7d 100644 --- a/homeassistant/components/homematic/manifest.json +++ b/homeassistant/components/homematic/manifest.json @@ -3,7 +3,7 @@ "name": "Homematic", "documentation": "https://www.home-assistant.io/integrations/homematic", "requirements": [ - "pyhomematic==0.1.61" + "pyhomematic==0.1.62" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/homematicip_cloud/__init__.py b/homeassistant/components/homematicip_cloud/__init__.py index 8cd41e0b980..62f3f9ec5d4 100644 --- a/homeassistant/components/homematicip_cloud/__init__.py +++ b/homeassistant/components/homematicip_cloud/__init__.py @@ -1,8 +1,10 @@ """Support for HomematicIP Cloud devices.""" import logging from pathlib import Path +from typing import Optional from homematicip.aio.group import AsyncHeatingGroup +from homematicip.aio.home import AsyncHome from homematicip.base.helpers import handle_config import voluptuous as vol @@ -135,7 +137,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: ) ) - async def _async_activate_eco_mode_with_duration(service): + async def _async_activate_eco_mode_with_duration(service) -> None: """Service to activate eco mode with duration.""" duration = service.data[ATTR_DURATION] hapid = service.data.get(ATTR_ACCESSPOINT_ID) @@ -155,7 +157,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: schema=SCHEMA_ACTIVATE_ECO_MODE_WITH_DURATION, ) - async def _async_activate_eco_mode_with_period(service): + async def _async_activate_eco_mode_with_period(service) -> None: """Service to activate eco mode with period.""" endtime = service.data[ATTR_ENDTIME] hapid = service.data.get(ATTR_ACCESSPOINT_ID) @@ -175,7 +177,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: schema=SCHEMA_ACTIVATE_ECO_MODE_WITH_PERIOD, ) - async def _async_activate_vacation(service): + async def _async_activate_vacation(service) -> None: """Service to activate vacation.""" endtime = service.data[ATTR_ENDTIME] temperature = service.data[ATTR_TEMPERATURE] @@ -196,7 +198,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: schema=SCHEMA_ACTIVATE_VACATION, ) - async def _async_deactivate_eco_mode(service): + async def _async_deactivate_eco_mode(service) -> None: """Service to deactivate eco mode.""" hapid = service.data.get(ATTR_ACCESSPOINT_ID) @@ -215,7 +217,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: schema=SCHEMA_DEACTIVATE_ECO_MODE, ) - async def _async_deactivate_vacation(service): + async def _async_deactivate_vacation(service) -> None: """Service to deactivate vacation.""" hapid = service.data.get(ATTR_ACCESSPOINT_ID) @@ -234,7 +236,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: schema=SCHEMA_DEACTIVATE_VACATION, ) - async def _set_active_climate_profile(service): + async def _set_active_climate_profile(service) -> None: """Service to set the active climate profile.""" entity_id_list = service.data[ATTR_ENTITY_ID] climate_profile_index = service.data[ATTR_CLIMATE_PROFILE_INDEX] - 1 @@ -257,7 +259,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: schema=SCHEMA_SET_ACTIVE_CLIMATE_PROFILE, ) - async def _async_dump_hap_config(service): + async def _async_dump_hap_config(service) -> None: """Service to dump the configuration of a Homematic IP Access Point.""" config_path = ( service.data.get(ATTR_CONFIG_OUTPUT_PATH) or hass.config.config_dir @@ -287,7 +289,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: schema=SCHEMA_DUMP_HAP_CONFIG, ) - def _get_home(hapid: str): + def _get_home(hapid: str) -> Optional[AsyncHome]: """Return a HmIP home.""" hap = hass.data[DOMAIN].get(hapid) if hap: @@ -324,7 +326,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool return True -async def async_unload_entry(hass, entry): +async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: """Unload a config entry.""" hap = hass.data[DOMAIN].pop(entry.data[HMIPC_HAPID]) return await hap.async_reset() diff --git a/homeassistant/components/homematicip_cloud/alarm_control_panel.py b/homeassistant/components/homematicip_cloud/alarm_control_panel.py index 8ebb35b12c1..f9a91203426 100644 --- a/homeassistant/components/homematicip_cloud/alarm_control_panel.py +++ b/homeassistant/components/homematicip_cloud/alarm_control_panel.py @@ -1,9 +1,14 @@ """Support for HomematicIP Cloud alarm control panel.""" import logging +from typing import Any, Dict from homematicip.functionalHomes import SecurityAndAlarmHome from homeassistant.components.alarm_control_panel import AlarmControlPanel +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, @@ -21,7 +26,9 @@ _LOGGER = logging.getLogger(__name__) CONST_ALARM_CONTROL_PANEL_NAME = "HmIP Alarm Control Panel" -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None +) -> None: """Set up the HomematicIP Cloud alarm control devices.""" pass @@ -40,9 +47,10 @@ class HomematicipAlarmControlPanel(AlarmControlPanel): def __init__(self, hap: HomematicipHAP) -> None: """Initialize the alarm control panel.""" self._home = hap.home + _LOGGER.info("Setting up %s", self.name) @property - def device_info(self): + def device_info(self) -> Dict[str, Any]: """Return device specific attributes.""" return { "identifiers": {(HMIPC_DOMAIN, f"ACP {self._home.id}")}, @@ -70,26 +78,31 @@ class HomematicipAlarmControlPanel(AlarmControlPanel): return STATE_ALARM_DISARMED @property - def _security_and_alarm(self): + def _security_and_alarm(self) -> SecurityAndAlarmHome: return self._home.get_functionalHome(SecurityAndAlarmHome) - async def async_alarm_disarm(self, code=None): + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY + + async def async_alarm_disarm(self, code=None) -> None: """Send disarm command.""" await self._home.set_security_zones_activation(False, False) - async def async_alarm_arm_home(self, code=None): + async def async_alarm_arm_home(self, code=None) -> None: """Send arm home command.""" await self._home.set_security_zones_activation(False, True) - async def async_alarm_arm_away(self, code=None): + async def async_alarm_arm_away(self, code=None) -> None: """Send arm away command.""" await self._home.set_security_zones_activation(True, True) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" self._home.on_update(self._async_device_changed) - def _async_device_changed(self, *args, **kwargs): + def _async_device_changed(self, *args, **kwargs) -> None: """Handle device state changes.""" _LOGGER.debug("Event %s (%s)", self.name, CONST_ALARM_CONTROL_PANEL_NAME) self.async_schedule_update_ha_state() diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index b5b663055a1..83d48d0a7b1 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -1,5 +1,6 @@ """Support for HomematicIP Cloud binary sensor.""" import logging +from typing import Any, Dict from homematicip.aio.device import ( AsyncAccelerationSensor, @@ -72,7 +73,9 @@ SAM_DEVICE_ATTRIBUTES = { } -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None +) -> None: """Set up the HomematicIP Cloud binary sensor devices.""" pass @@ -82,17 +85,17 @@ async def async_setup_entry( ) -> None: """Set up the HomematicIP Cloud binary sensor from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] - devices = [] + entities = [] for device in hap.home.devices: if isinstance(device, AsyncAccelerationSensor): - devices.append(HomematicipAccelerationSensor(hap, device)) + entities.append(HomematicipAccelerationSensor(hap, device)) if isinstance(device, (AsyncContactInterface, AsyncFullFlushContactInterface)): - devices.append(HomematicipContactInterface(hap, device)) + entities.append(HomematicipContactInterface(hap, device)) if isinstance( device, (AsyncShutterContact, AsyncShutterContactMagnetic, AsyncRotaryHandleSensor), ): - devices.append(HomematicipShutterContact(hap, device)) + entities.append(HomematicipShutterContact(hap, device)) if isinstance( device, ( @@ -101,31 +104,31 @@ async def async_setup_entry( AsyncMotionDetectorPushButton, ), ): - devices.append(HomematicipMotionDetector(hap, device)) + entities.append(HomematicipMotionDetector(hap, device)) if isinstance(device, AsyncPresenceDetectorIndoor): - devices.append(HomematicipPresenceDetector(hap, device)) + entities.append(HomematicipPresenceDetector(hap, device)) if isinstance(device, AsyncSmokeDetector): - devices.append(HomematicipSmokeDetector(hap, device)) + entities.append(HomematicipSmokeDetector(hap, device)) if isinstance(device, AsyncWaterSensor): - devices.append(HomematicipWaterDetector(hap, device)) + entities.append(HomematicipWaterDetector(hap, device)) if isinstance(device, (AsyncWeatherSensorPlus, AsyncWeatherSensorPro)): - devices.append(HomematicipRainSensor(hap, device)) + entities.append(HomematicipRainSensor(hap, device)) if isinstance( device, (AsyncWeatherSensor, AsyncWeatherSensorPlus, AsyncWeatherSensorPro) ): - devices.append(HomematicipStormSensor(hap, device)) - devices.append(HomematicipSunshineSensor(hap, device)) + entities.append(HomematicipStormSensor(hap, device)) + entities.append(HomematicipSunshineSensor(hap, device)) if isinstance(device, AsyncDevice) and device.lowBat is not None: - devices.append(HomematicipBatterySensor(hap, device)) + entities.append(HomematicipBatterySensor(hap, device)) for group in hap.home.groups: if isinstance(group, AsyncSecurityGroup): - devices.append(HomematicipSecuritySensorGroup(hap, group)) + entities.append(HomematicipSecuritySensorGroup(hap, group)) elif isinstance(group, AsyncSecurityZoneGroup): - devices.append(HomematicipSecurityZoneSensorGroup(hap, group)) + entities.append(HomematicipSecurityZoneSensorGroup(hap, group)) - if devices: - async_add_entities(devices) + if entities: + async_add_entities(entities) class HomematicipAccelerationSensor(HomematicipGenericDevice, BinarySensorDevice): @@ -142,7 +145,7 @@ class HomematicipAccelerationSensor(HomematicipGenericDevice, BinarySensorDevice return self._device.accelerationSensorTriggered @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, Any]: """Return the state attributes of the acceleration sensor.""" state_attr = super().device_state_attributes @@ -296,7 +299,7 @@ class HomematicipSunshineSensor(HomematicipGenericDevice, BinarySensorDevice): return self._device.sunshine @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, Any]: """Return the state attributes of the illuminance sensor.""" state_attr = super().device_state_attributes @@ -346,7 +349,7 @@ class HomematicipSecurityZoneSensorGroup(HomematicipGenericDevice, BinarySensorD return True @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, Any]: """Return the state attributes of the security zone group.""" state_attr = super().device_state_attributes @@ -390,7 +393,7 @@ class HomematicipSecuritySensorGroup( super().__init__(hap, device, "Sensors") @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, Any]: """Return the state attributes of the security group.""" state_attr = super().device_state_attributes diff --git a/homeassistant/components/homematicip_cloud/climate.py b/homeassistant/components/homematicip_cloud/climate.py index 9673459e820..e3c922dc577 100644 --- a/homeassistant/components/homematicip_cloud/climate.py +++ b/homeassistant/components/homematicip_cloud/climate.py @@ -1,6 +1,6 @@ """Support for HomematicIP Cloud climate devices.""" import logging -from typing import Awaitable +from typing import Any, Dict, List, Optional, Union from homematicip.aio.device import AsyncHeatingThermostat, AsyncHeatingThermostatCompact from homematicip.aio.group import AsyncHeatingGroup @@ -10,6 +10,8 @@ from homematicip.functionalHomes import IndoorClimateHome from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_HEAT, @@ -41,7 +43,9 @@ HMIP_MANUAL_CM = "MANUAL" HMIP_ECO_CM = "ECO" -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None +) -> None: """Set up the HomematicIP Cloud climate devices.""" pass @@ -51,13 +55,13 @@ async def async_setup_entry( ) -> None: """Set up the HomematicIP climate from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] - devices = [] + entities = [] for device in hap.home.groups: if isinstance(device, AsyncHeatingGroup): - devices.append(HomematicipHeatingGroup(hap, device)) + entities.append(HomematicipHeatingGroup(hap, device)) - if devices: - async_add_entities(devices) + if entities: + async_add_entities(entities) class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): @@ -74,10 +78,10 @@ class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): super().__init__(hap, device) self._simple_heating = None if device.actualTemperature is None: - self._simple_heating = self._get_first_radiator_thermostat() + self._simple_heating = self._first_radiator_thermostat @property - def device_info(self): + def device_info(self) -> Dict[str, Any]: """Return device specific attributes.""" return { "identifiers": {(HMIPC_DOMAIN, self._device.id)}, @@ -127,7 +131,7 @@ class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): return HVAC_MODE_AUTO @property - def hvac_modes(self): + 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] @@ -139,7 +143,25 @@ class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): ) @property - def preset_mode(self): + def hvac_action(self) -> Optional[str]: + """ + Return the current hvac_action. + + This is only relevant for radiator thermostats. + """ + if ( + self._device.floorHeatingMode == "RADIATOR" + and self._has_radiator_thermostat + and self._heat_mode_enabled + ): + return ( + CURRENT_HVAC_HEAT if self._device.valvePosition else CURRENT_HVAC_IDLE + ) + + return None + + @property + def preset_mode(self) -> Optional[str]: """Return the current preset mode.""" if self._device.boostMode: return PRESET_BOOST @@ -162,7 +184,7 @@ class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): ) @property - def preset_modes(self): + 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. @@ -190,7 +212,7 @@ class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): """Return the maximum temperature.""" return self._device.maxTemperature - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs) -> None: """Set new target temperature.""" temperature = kwargs.get(ATTR_TEMPERATURE) if temperature is None: @@ -199,7 +221,7 @@ class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): if self.min_temp <= temperature <= self.max_temp: await self._device.set_point_temperature(temperature) - async def async_set_hvac_mode(self, hvac_mode: str) -> Awaitable[None]: + async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set new target hvac mode.""" if hvac_mode not in self.hvac_modes: return @@ -209,7 +231,7 @@ class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): else: await self._device.set_control_mode(HMIP_MANUAL_CM) - async def async_set_preset_mode(self, preset_mode: str) -> Awaitable[None]: + async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" if preset_mode not in self.preset_modes: return @@ -225,7 +247,7 @@ class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): await self._device.set_active_profile(profile_idx) @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, Any]: """Return the state attributes of the access point.""" state_attr = super().device_state_attributes @@ -242,12 +264,12 @@ class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): return state_attr @property - def _indoor_climate(self): + def _indoor_climate(self) -> IndoorClimateHome: """Return the hmip indoor climate functional home of this group.""" return self._home.get_functionalHome(IndoorClimateHome) @property - def _device_profiles(self): + def _device_profiles(self) -> List[str]: """Return the relevant profiles.""" return [ profile @@ -258,11 +280,11 @@ class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): ] @property - def _device_profile_names(self): + def _device_profile_names(self) -> List[str]: """Return a collection of profile names.""" return [profile.name for profile in self._device_profiles] - def _get_profile_idx_by_name(self, profile_name): + def _get_profile_idx_by_name(self, profile_name: str) -> int: """Return a profile index by name.""" relevant_index = self._relevant_profile_group index_name = [ @@ -274,19 +296,19 @@ class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): return relevant_index[index_name[0]] @property - def _heat_mode_enabled(self): + def _heat_mode_enabled(self) -> bool: """Return, if heating mode is enabled.""" return not self._device.cooling @property - def _disabled_by_cooling_mode(self): + def _disabled_by_cooling_mode(self) -> bool: """Return, if group is disabled by the cooling mode.""" return self._device.cooling and ( self._device.coolingIgnored or not self._device.coolingAllowed ) @property - def _relevant_profile_group(self): + def _relevant_profile_group(self) -> List[str]: """Return the relevant profile groups.""" if self._disabled_by_cooling_mode: return [] @@ -305,9 +327,12 @@ class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): @property def _has_radiator_thermostat(self) -> bool: """Return, if a radiator thermostat is in the hmip heating group.""" - return bool(self._get_first_radiator_thermostat()) + return bool(self._first_radiator_thermostat) - def _get_first_radiator_thermostat(self): + @property + def _first_radiator_thermostat( + self, + ) -> Optional[Union[AsyncHeatingThermostat, AsyncHeatingThermostatCompact]]: """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 1488f02f13b..8d85dfda328 100644 --- a/homeassistant/components/homematicip_cloud/config_flow.py +++ b/homeassistant/components/homematicip_cloud/config_flow.py @@ -1,5 +1,5 @@ """Config flow to configure the HomematicIP Cloud component.""" -from typing import Set +from typing import Any, Dict, Set import voluptuous as vol @@ -34,15 +34,15 @@ class HomematicipCloudFlowHandler(config_entries.ConfigFlow): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_PUSH - def __init__(self): + def __init__(self) -> None: """Initialize HomematicIP Cloud config flow.""" self.auth = None - async def async_step_user(self, user_input=None): + 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): + async def async_step_init(self, user_input=None) -> Dict[str, Any]: """Handle a flow start.""" errors = {} @@ -69,7 +69,7 @@ class HomematicipCloudFlowHandler(config_entries.ConfigFlow): errors=errors, ) - async def async_step_link(self, user_input=None): + async def async_step_link(self, user_input=None) -> Dict[str, Any]: """Attempt to link with the HomematicIP Cloud access point.""" errors = {} @@ -91,7 +91,7 @@ class HomematicipCloudFlowHandler(config_entries.ConfigFlow): return self.async_show_form(step_id="link", errors=errors) - async def async_step_import(self, import_info): + 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] authtoken = import_info[HMIPC_AUTHTOKEN] diff --git a/homeassistant/components/homematicip_cloud/cover.py b/homeassistant/components/homematicip_cloud/cover.py index 63ac6f7310c..e3efe9a9508 100644 --- a/homeassistant/components/homematicip_cloud/cover.py +++ b/homeassistant/components/homematicip_cloud/cover.py @@ -2,7 +2,12 @@ import logging from typing import Optional -from homematicip.aio.device import AsyncFullFlushBlind, AsyncFullFlushShutter +from homematicip.aio.device import ( + AsyncFullFlushBlind, + AsyncFullFlushShutter, + AsyncGarageDoorModuleTormatic, +) +from homematicip.base.enums import DoorCommand, DoorState from homeassistant.components.cover import ( ATTR_POSITION, @@ -22,7 +27,9 @@ HMIP_SLATS_OPEN = 0 HMIP_SLATS_CLOSED = 1 -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None +) -> None: """Set up the HomematicIP Cloud cover devices.""" pass @@ -32,15 +39,17 @@ async def async_setup_entry( ) -> None: """Set up the HomematicIP cover from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] - devices = [] + entities = [] for device in hap.home.devices: if isinstance(device, AsyncFullFlushBlind): - devices.append(HomematicipCoverSlats(hap, device)) + entities.append(HomematicipCoverSlats(hap, device)) elif isinstance(device, AsyncFullFlushShutter): - devices.append(HomematicipCoverShutter(hap, device)) + entities.append(HomematicipCoverShutter(hap, device)) + elif isinstance(device, AsyncGarageDoorModuleTormatic): + entities.append(HomematicipGarageDoorModuleTormatic(hap, device)) - if devices: - async_add_entities(devices) + if entities: + async_add_entities(entities) class HomematicipCoverShutter(HomematicipGenericDevice, CoverDevice): @@ -51,7 +60,7 @@ class HomematicipCoverShutter(HomematicipGenericDevice, CoverDevice): """Return current position of cover.""" return int((1 - self._device.shutterLevel) * 100) - async def async_set_cover_position(self, **kwargs): + async def async_set_cover_position(self, **kwargs) -> None: """Move the cover to a specific position.""" position = kwargs[ATTR_POSITION] # HmIP cover is closed:1 -> open:0 @@ -65,15 +74,15 @@ class HomematicipCoverShutter(HomematicipGenericDevice, CoverDevice): return self._device.shutterLevel == HMIP_COVER_CLOSED return None - async def async_open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs) -> None: """Open the cover.""" await self._device.set_shutter_level(HMIP_COVER_OPEN) - async def async_close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs) -> None: """Close the cover.""" await self._device.set_shutter_level(HMIP_COVER_CLOSED) - async def async_stop_cover(self, **kwargs): + async def async_stop_cover(self, **kwargs) -> None: """Stop the device if in motion.""" await self._device.set_shutter_stop() @@ -86,21 +95,53 @@ class HomematicipCoverSlats(HomematicipCoverShutter, CoverDevice): """Return current tilt position of cover.""" return int((1 - self._device.slatsLevel) * 100) - async def async_set_cover_tilt_position(self, **kwargs): + async def async_set_cover_tilt_position(self, **kwargs) -> None: """Move the cover to a specific tilt position.""" position = kwargs[ATTR_TILT_POSITION] # HmIP slats is closed:1 -> open:0 level = 1 - position / 100.0 await self._device.set_slats_level(level) - async def async_open_cover_tilt(self, **kwargs): + async def async_open_cover_tilt(self, **kwargs) -> None: """Open the slats.""" await self._device.set_slats_level(HMIP_SLATS_OPEN) - async def async_close_cover_tilt(self, **kwargs): + async def async_close_cover_tilt(self, **kwargs) -> None: """Close the slats.""" await self._device.set_slats_level(HMIP_SLATS_CLOSED) - async def async_stop_cover_tilt(self, **kwargs): + async def async_stop_cover_tilt(self, **kwargs) -> None: """Stop the device if in motion.""" await self._device.set_shutter_stop() + + +class HomematicipGarageDoorModuleTormatic(HomematicipGenericDevice, CoverDevice): + """Representation of a HomematicIP Garage Door Module for Tormatic.""" + + @property + def current_cover_position(self) -> int: + """Return current position of cover.""" + door_state_to_position = { + DoorState.CLOSED: 0, + DoorState.OPEN: 100, + DoorState.VENTILATION_POSITION: 10, + DoorState.POSITION_UNKNOWN: None, + } + return door_state_to_position.get(self._device.doorState) + + @property + def is_closed(self) -> Optional[bool]: + """Return if the cover is closed.""" + return self._device.doorState == DoorState.CLOSED + + async def async_open_cover(self, **kwargs) -> None: + """Open the cover.""" + await self._device.send_door_command(DoorCommand.OPEN) + + async def async_close_cover(self, **kwargs) -> None: + """Close the cover.""" + await self._device.send_door_command(DoorCommand.CLOSE) + + async def async_stop_cover(self, **kwargs) -> None: + """Stop the cover.""" + await self._device.send_door_command(DoorCommand.STOP) diff --git a/homeassistant/components/homematicip_cloud/device.py b/homeassistant/components/homematicip_cloud/device.py index 6c81775b688..f35b696767c 100644 --- a/homeassistant/components/homematicip_cloud/device.py +++ b/homeassistant/components/homematicip_cloud/device.py @@ -1,6 +1,6 @@ """Generic device for the HomematicIP Cloud component.""" import logging -from typing import Optional +from typing import Any, Dict, Optional from homematicip.aio.device import AsyncDevice from homematicip.aio.group import AsyncGroup @@ -79,7 +79,7 @@ class HomematicipGenericDevice(Entity): _LOGGER.info("Setting up %s (%s)", self.name, self._device.modelType) @property - def device_info(self): + def device_info(self) -> Dict[str, Any]: """Return device specific attributes.""" # Only physical devices should be HA devices. if isinstance(self._device, AsyncDevice): @@ -96,14 +96,14 @@ class HomematicipGenericDevice(Entity): } return None - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" self._hap.hmip_device_by_entity_id[self.entity_id] = self._device self._device.on_update(self._async_device_changed) self._device.on_remove(self._async_device_removed) @callback - def _async_device_changed(self, *args, **kwargs): + def _async_device_changed(self, *args, **kwargs) -> None: """Handle device state changes.""" # Don't update disabled entities if self.enabled: @@ -152,7 +152,7 @@ class HomematicipGenericDevice(Entity): entity_registry.async_remove(entity_id) @callback - def _async_device_removed(self, *args, **kwargs): + 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 @@ -193,7 +193,7 @@ class HomematicipGenericDevice(Entity): return None @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, Any]: """Return the state attributes of the generic device.""" state_attr = {} diff --git a/homeassistant/components/homematicip_cloud/hap.py b/homeassistant/components/homematicip_cloud/hap.py index bef04180c6f..63bdf3166eb 100644 --- a/homeassistant/components/homematicip_cloud/hap.py +++ b/homeassistant/components/homematicip_cloud/hap.py @@ -22,13 +22,13 @@ _LOGGER = logging.getLogger(__name__) class HomematicipAuth: """Manages HomematicIP client registration.""" - def __init__(self, hass, config): + def __init__(self, hass, config) -> None: """Initialize HomematicIP Cloud client registration.""" self.hass = hass self.config = config self.auth = None - async def async_setup(self): + async def async_setup(self) -> bool: """Connect to HomematicIP for registration.""" try: self.auth = await self.get_auth( @@ -38,7 +38,7 @@ class HomematicipAuth: except HmipcConnectionError: return False - async def async_checkbutton(self): + async def async_checkbutton(self) -> bool: """Check blue butten has been pressed.""" try: return await self.auth.isRequestAcknowledged() @@ -82,7 +82,7 @@ class HomematicipHAP: self._accesspoint_connected = True self.hmip_device_by_entity_id = {} - async def async_setup(self, tries: int = 0): + async def async_setup(self, tries: int = 0) -> bool: """Initialize connection.""" try: self.home = await self.get_hap( @@ -108,7 +108,7 @@ class HomematicipHAP: return True @callback - def async_update(self, *args, **kwargs): + def async_update(self, *args, **kwargs) -> None: """Async update the home device. Triggered when the HMIP HOME_CHANGED event has fired. @@ -141,23 +141,23 @@ class HomematicipHAP: self.home.update_home_only(args[0]) @callback - def async_create_entity(self, *args, **kwargs): + def async_create_entity(self, *args, **kwargs) -> None: """Create a device or a group.""" is_device = EventType(kwargs["event_type"]) == EventType.DEVICE_ADDED self.hass.async_create_task(self.async_create_entity_lazy(is_device)) - async def async_create_entity_lazy(self, is_device=True): + async def async_create_entity_lazy(self, is_device=True) -> None: """Delay entity creation to allow the user to enter a device name.""" if is_device: await asyncio.sleep(30) await self.hass.config_entries.async_reload(self.config_entry.entry_id) - async def get_state(self): + async def get_state(self) -> None: """Update HMIP state and tell Home Assistant.""" await self.home.get_current_state() self.update_all() - def get_state_finished(self, future): + def get_state_finished(self, future) -> None: """Execute when get_state coroutine has finished.""" try: future.result() @@ -167,18 +167,18 @@ class HomematicipHAP: _LOGGER.error("Updating state after HMIP access point reconnect failed") self.hass.async_create_task(self.home.disable_events()) - def set_all_to_unavailable(self): + def set_all_to_unavailable(self) -> None: """Set all devices to unavailable and tell Home Assistant.""" for device in self.home.devices: device.unreach = True self.update_all() - def update_all(self): + def update_all(self) -> None: """Signal all devices to update their state.""" for device in self.home.devices: device.fire_update_event() - async def async_connect(self): + async def async_connect(self) -> None: """Start WebSocket connection.""" tries = 0 while True: @@ -210,7 +210,7 @@ class HomematicipHAP: except asyncio.CancelledError: break - async def async_reset(self): + async def async_reset(self) -> bool: """Close the websocket connection.""" self._ws_close_requested = True if self._retry_task is not None: diff --git a/homeassistant/components/homematicip_cloud/light.py b/homeassistant/components/homematicip_cloud/light.py index c262b05d019..79083f031ae 100644 --- a/homeassistant/components/homematicip_cloud/light.py +++ b/homeassistant/components/homematicip_cloud/light.py @@ -1,5 +1,6 @@ """Support for HomematicIP Cloud lights.""" import logging +from typing import Any, Dict from homematicip.aio.device import ( AsyncBrandDimmer, @@ -33,7 +34,9 @@ ATTR_TODAY_ENERGY_KWH = "today_energy_kwh" ATTR_CURRENT_POWER_W = "current_power_w" -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None +) -> None: """Old way of setting up HomematicIP Cloud lights.""" pass @@ -43,16 +46,16 @@ async def async_setup_entry( ) -> None: """Set up the HomematicIP Cloud lights from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] - devices = [] + entities = [] for device in hap.home.devices: if isinstance(device, AsyncBrandSwitchMeasuring): - devices.append(HomematicipLightMeasuring(hap, device)) + entities.append(HomematicipLightMeasuring(hap, device)) elif isinstance(device, AsyncBrandSwitchNotificationLight): - devices.append(HomematicipLight(hap, device)) - devices.append( + entities.append(HomematicipLight(hap, device)) + entities.append( HomematicipNotificationLight(hap, device, device.topLightChannelIndex) ) - devices.append( + entities.append( HomematicipNotificationLight( hap, device, device.bottomLightChannelIndex ) @@ -61,10 +64,10 @@ async def async_setup_entry( device, (AsyncDimmer, AsyncPluggableDimmer, AsyncBrandDimmer, AsyncFullFlushDimmer), ): - devices.append(HomematicipDimmer(hap, device)) + entities.append(HomematicipDimmer(hap, device)) - if devices: - async_add_entities(devices) + if entities: + async_add_entities(entities) class HomematicipLight(HomematicipGenericDevice, Light): @@ -79,11 +82,11 @@ class HomematicipLight(HomematicipGenericDevice, Light): """Return true if device is on.""" return self._device.on - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs) -> None: """Turn the device on.""" await self._device.turn_on() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs) -> None: """Turn the device off.""" await self._device.turn_off() @@ -92,7 +95,7 @@ class HomematicipLightMeasuring(HomematicipLight): """Representation of a HomematicIP Cloud measuring light device.""" @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, Any]: """Return the state attributes of the generic device.""" state_attr = super().device_state_attributes @@ -127,14 +130,14 @@ class HomematicipDimmer(HomematicipGenericDevice, Light): """Flag supported features.""" return SUPPORT_BRIGHTNESS - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs) -> None: """Turn the light on.""" if ATTR_BRIGHTNESS in kwargs: await self._device.set_dim_level(kwargs[ATTR_BRIGHTNESS] / 255.0) else: await self._device.set_dim_level(1) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs) -> None: """Turn the light off.""" await self._device.set_dim_level(0) @@ -184,7 +187,7 @@ class HomematicipNotificationLight(HomematicipGenericDevice, Light): return self._color_switcher.get(simple_rgb_color, [0.0, 0.0]) @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, Any]: """Return the state attributes of the generic device.""" state_attr = super().device_state_attributes @@ -208,7 +211,7 @@ class HomematicipNotificationLight(HomematicipGenericDevice, Light): """Return a unique ID.""" return f"{self.__class__.__name__}_{self.post}_{self._device.id}" - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs) -> None: """Turn the light on.""" # Use hs_color from kwargs, # if not applicable use current hs_color. @@ -236,7 +239,7 @@ class HomematicipNotificationLight(HomematicipGenericDevice, Light): rampTime=transition, ) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs) -> None: """Turn the light off.""" simple_rgb_color = self._func_channel.simpleRGBColorState transition = kwargs.get(ATTR_TRANSITION, 0.5) @@ -250,7 +253,7 @@ class HomematicipNotificationLight(HomematicipGenericDevice, Light): ) -def _convert_color(color) -> RGBColorState: +def _convert_color(color: tuple) -> RGBColorState: """ Convert the given color to the reduced RGBColorState color. diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index 4feef19c8da..14677dd71a0 100644 --- a/homeassistant/components/homematicip_cloud/manifest.json +++ b/homeassistant/components/homematicip_cloud/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homematicip_cloud", "requirements": [ - "homematicip==0.10.13" + "homematicip==0.10.15" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index acbf72f6ae9..a8ca3d17eb9 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -1,5 +1,6 @@ """Support for HomematicIP Cloud sensors.""" import logging +from typing import Any, Dict from homematicip.aio.device import ( AsyncBrandSwitchMeasuring, @@ -55,7 +56,9 @@ ILLUMINATION_DEVICE_ATTRIBUTES = { } -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None +) -> None: """Set up the HomematicIP Cloud sensors devices.""" pass @@ -65,11 +68,11 @@ async def async_setup_entry( ) -> None: """Set up the HomematicIP Cloud sensors from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] - devices = [HomematicipAccesspointStatus(hap)] + entities = [HomematicipAccesspointStatus(hap)] for device in hap.home.devices: if isinstance(device, (AsyncHeatingThermostat, AsyncHeatingThermostatCompact)): - devices.append(HomematicipHeatingThermostat(hap, device)) - devices.append(HomematicipTemperatureSensor(hap, device)) + entities.append(HomematicipHeatingThermostat(hap, device)) + entities.append(HomematicipTemperatureSensor(hap, device)) if isinstance( device, ( @@ -81,8 +84,8 @@ async def async_setup_entry( AsyncWeatherSensorPro, ), ): - devices.append(HomematicipTemperatureSensor(hap, device)) - devices.append(HomematicipHumiditySensor(hap, device)) + entities.append(HomematicipTemperatureSensor(hap, device)) + entities.append(HomematicipHumiditySensor(hap, device)) if isinstance( device, ( @@ -96,7 +99,7 @@ async def async_setup_entry( AsyncWeatherSensorPro, ), ): - devices.append(HomematicipIlluminanceSensor(hap, device)) + entities.append(HomematicipIlluminanceSensor(hap, device)) if isinstance( device, ( @@ -105,18 +108,18 @@ async def async_setup_entry( AsyncFullFlushSwitchMeasuring, ), ): - devices.append(HomematicipPowerSensor(hap, device)) + entities.append(HomematicipPowerSensor(hap, device)) if isinstance( device, (AsyncWeatherSensor, AsyncWeatherSensorPlus, AsyncWeatherSensorPro) ): - devices.append(HomematicipWindspeedSensor(hap, device)) + entities.append(HomematicipWindspeedSensor(hap, device)) if isinstance(device, (AsyncWeatherSensorPlus, AsyncWeatherSensorPro)): - devices.append(HomematicipTodayRainSensor(hap, device)) + entities.append(HomematicipTodayRainSensor(hap, device)) if isinstance(device, AsyncPassageDetector): - devices.append(HomematicipPassageDetectorDeltaCounter(hap, device)) + entities.append(HomematicipPassageDetectorDeltaCounter(hap, device)) - if devices: - async_add_entities(devices) + if entities: + async_add_entities(entities) class HomematicipAccesspointStatus(HomematicipGenericDevice): @@ -127,7 +130,7 @@ class HomematicipAccesspointStatus(HomematicipGenericDevice): super().__init__(hap, hap.home) @property - def device_info(self): + def device_info(self) -> Dict[str, Any]: """Return device specific attributes.""" # Adds a sensor to the existing HAP device return { @@ -158,7 +161,7 @@ class HomematicipAccesspointStatus(HomematicipGenericDevice): return "%" @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, Any]: """Return the state attributes of the access point.""" state_attr = super().device_state_attributes @@ -246,7 +249,7 @@ class HomematicipTemperatureSensor(HomematicipGenericDevice): return TEMP_CELSIUS @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, Any]: """Return the state attributes of the windspeed sensor.""" state_attr = super().device_state_attributes @@ -283,7 +286,7 @@ class HomematicipIlluminanceSensor(HomematicipGenericDevice): return "lx" @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, Any]: """Return the state attributes of the wind speed sensor.""" state_attr = super().device_state_attributes @@ -336,7 +339,7 @@ class HomematicipWindspeedSensor(HomematicipGenericDevice): return "km/h" @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, Any]: """Return the state attributes of the wind speed sensor.""" state_attr = super().device_state_attributes @@ -378,7 +381,7 @@ class HomematicipPassageDetectorDeltaCounter(HomematicipGenericDevice): return self._device.leftRightCounterDelta @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, Any]: """Return the state attributes of the delta counter.""" state_attr = super().device_state_attributes diff --git a/homeassistant/components/homematicip_cloud/switch.py b/homeassistant/components/homematicip_cloud/switch.py index dae6019b378..8e15313a4fe 100644 --- a/homeassistant/components/homematicip_cloud/switch.py +++ b/homeassistant/components/homematicip_cloud/switch.py @@ -1,5 +1,6 @@ """Support for HomematicIP Cloud switches.""" import logging +from typing import Any, Dict from homematicip.aio.device import ( AsyncBrandSwitchMeasuring, @@ -24,7 +25,9 @@ from .hap import HomematicipHAP _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None +) -> None: """Set up the HomematicIP Cloud switch devices.""" pass @@ -34,7 +37,7 @@ async def async_setup_entry( ) -> None: """Set up the HomematicIP switch from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] - devices = [] + entities = [] for device in hap.home.devices: if isinstance(device, AsyncBrandSwitchMeasuring): # BrandSwitchMeasuring inherits PlugableSwitchMeasuring @@ -44,27 +47,27 @@ async def async_setup_entry( elif isinstance( device, (AsyncPlugableSwitchMeasuring, AsyncFullFlushSwitchMeasuring) ): - devices.append(HomematicipSwitchMeasuring(hap, device)) + entities.append(HomematicipSwitchMeasuring(hap, device)) elif isinstance( device, (AsyncPlugableSwitch, AsyncPrintedCircuitBoardSwitchBattery) ): - devices.append(HomematicipSwitch(hap, device)) + entities.append(HomematicipSwitch(hap, device)) elif isinstance(device, AsyncOpenCollector8Module): for channel in range(1, 9): - devices.append(HomematicipMultiSwitch(hap, device, channel)) + entities.append(HomematicipMultiSwitch(hap, device, channel)) elif isinstance(device, AsyncMultiIOBox): for channel in range(1, 3): - devices.append(HomematicipMultiSwitch(hap, device, channel)) + entities.append(HomematicipMultiSwitch(hap, device, channel)) elif isinstance(device, AsyncPrintedCircuitBoardSwitch2): for channel in range(1, 3): - devices.append(HomematicipMultiSwitch(hap, device, channel)) + entities.append(HomematicipMultiSwitch(hap, device, channel)) for group in hap.home.groups: if isinstance(group, AsyncSwitchingGroup): - devices.append(HomematicipGroupSwitch(hap, group)) + entities.append(HomematicipGroupSwitch(hap, group)) - if devices: - async_add_entities(devices) + if entities: + async_add_entities(entities) class HomematicipSwitch(HomematicipGenericDevice, SwitchDevice): @@ -79,11 +82,11 @@ class HomematicipSwitch(HomematicipGenericDevice, SwitchDevice): """Return true if device is on.""" return self._device.on - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs) -> None: """Turn the device on.""" await self._device.turn_on() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs) -> None: """Turn the device off.""" await self._device.turn_off() @@ -111,7 +114,7 @@ class HomematicipGroupSwitch(HomematicipGenericDevice, SwitchDevice): return True @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, Any]: """Return the state attributes of the switch-group.""" state_attr = super().device_state_attributes @@ -120,11 +123,11 @@ class HomematicipGroupSwitch(HomematicipGenericDevice, SwitchDevice): return state_attr - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs) -> None: """Turn the group on.""" await self._device.turn_on() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs) -> None: """Turn the group off.""" await self._device.turn_off() @@ -148,7 +151,7 @@ class HomematicipSwitchMeasuring(HomematicipSwitch): class HomematicipMultiSwitch(HomematicipGenericDevice, SwitchDevice): """Representation of a HomematicIP Cloud multi switch device.""" - def __init__(self, hap: HomematicipHAP, device, channel: int): + def __init__(self, hap: HomematicipHAP, device, channel: int) -> None: """Initialize the multi switch device.""" self.channel = channel super().__init__(hap, device, f"Channel{channel}") @@ -163,10 +166,10 @@ class HomematicipMultiSwitch(HomematicipGenericDevice, SwitchDevice): """Return true if device is on.""" return self._device.functionalChannels[self.channel].on - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs) -> None: """Turn the device on.""" await self._device.turn_on(self.channel) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs) -> None: """Turn the device off.""" await self._device.turn_off(self.channel) diff --git a/homeassistant/components/homematicip_cloud/weather.py b/homeassistant/components/homematicip_cloud/weather.py index 5aa3f28c45d..ebc7eacf78e 100644 --- a/homeassistant/components/homematicip_cloud/weather.py +++ b/homeassistant/components/homematicip_cloud/weather.py @@ -37,7 +37,9 @@ HOME_WEATHER_CONDITION = { } -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None +) -> None: """Set up the HomematicIP Cloud weather sensor.""" pass @@ -47,17 +49,17 @@ async def async_setup_entry( ) -> None: """Set up the HomematicIP weather sensor from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] - devices = [] + entities = [] for device in hap.home.devices: if isinstance(device, AsyncWeatherSensorPro): - devices.append(HomematicipWeatherSensorPro(hap, device)) + entities.append(HomematicipWeatherSensorPro(hap, device)) elif isinstance(device, (AsyncWeatherSensor, AsyncWeatherSensorPlus)): - devices.append(HomematicipWeatherSensor(hap, device)) + entities.append(HomematicipWeatherSensor(hap, device)) - devices.append(HomematicipHomeWeather(hap)) + entities.append(HomematicipHomeWeather(hap)) - if devices: - async_add_entities(devices) + if entities: + async_add_entities(entities) class HomematicipWeatherSensor(HomematicipGenericDevice, WeatherEntity): diff --git a/homeassistant/components/homeworks/__init__.py b/homeassistant/components/homeworks/__init__.py index bd40336b8ba..c6296d8f4c6 100644 --- a/homeassistant/components/homeworks/__init__.py +++ b/homeassistant/components/homeworks/__init__.py @@ -1,6 +1,7 @@ """Support for Lutron Homeworks Series 4 and 8 systems.""" import logging +from pyhomeworks.pyhomeworks import HW_BUTTON_PRESSED, HW_BUTTON_RELEASED, Homeworks import voluptuous as vol from homeassistant.const import ( @@ -65,7 +66,6 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, base_config): """Start Homeworks controller.""" - from pyhomeworks.pyhomeworks import Homeworks def hw_callback(msg_type, values): """Dispatch state changes.""" @@ -98,7 +98,7 @@ class HomeworksDevice: """Base class of a Homeworks device.""" def __init__(self, controller, addr, name): - """Controller, address, and name of the device.""" + """Initialize Homeworks device.""" self._addr = addr self._name = name self._controller = controller @@ -138,7 +138,6 @@ class HomeworksKeypadEvent: @callback def _update_callback(self, msg_type, values): """Fire events if button is pressed or released.""" - from pyhomeworks.pyhomeworks import HW_BUTTON_PRESSED, HW_BUTTON_RELEASED if msg_type == HW_BUTTON_PRESSED: event = EVENT_BUTTON_PRESS diff --git a/homeassistant/components/homeworks/light.py b/homeassistant/components/homeworks/light.py index d1854b4dbf3..2c0034ee986 100644 --- a/homeassistant/components/homeworks/light.py +++ b/homeassistant/components/homeworks/light.py @@ -1,6 +1,8 @@ """Support for Lutron Homeworks lights.""" import logging +from pyhomeworks.pyhomeworks import HW_LIGHT_CHANGED + from homeassistant.components.light import ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light from homeassistant.const import CONF_NAME from homeassistant.core import callback @@ -93,7 +95,6 @@ class HomeworksLight(HomeworksDevice, Light): @callback def _update_callback(self, msg_type, values): """Process device specific messages.""" - from pyhomeworks.pyhomeworks import HW_LIGHT_CHANGED if msg_type == HW_LIGHT_CHANGED: self._level = int((values[1] * 255.0) / 100.0) diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index 15efac87587..f8537bfe96a 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -1,43 +1,43 @@ """Support for Honeywell (US) Total Connect Comfort climate systems.""" import datetime import logging -from typing import Any, Dict, Optional, List +from typing import Any, Dict, List, Optional import requests -import voluptuous as vol import somecomfort +import voluptuous as vol -from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice from homeassistant.components.climate.const import ( ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, + CURRENT_HVAC_COOL, + CURRENT_HVAC_FAN, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, FAN_AUTO, FAN_DIFFUSE, FAN_ON, + HVAC_MODE_COOL, + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, + PRESET_AWAY, + PRESET_NONE, SUPPORT_AUX_HEAT, SUPPORT_FAN_MODE, SUPPORT_PRESET_MODE, SUPPORT_TARGET_HUMIDITY, SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, - CURRENT_HVAC_COOL, - CURRENT_HVAC_HEAT, - CURRENT_HVAC_IDLE, - CURRENT_HVAC_FAN, - HVAC_MODE_OFF, - HVAC_MODE_HEAT, - HVAC_MODE_COOL, - HVAC_MODE_HEAT_COOL, - PRESET_AWAY, - PRESET_NONE, ) from homeassistant.const import ( + ATTR_TEMPERATURE, CONF_PASSWORD, + CONF_REGION, CONF_USERNAME, TEMP_CELSIUS, TEMP_FAHRENHEIT, - ATTR_TEMPERATURE, - CONF_REGION, ) import homeassistant.helpers.config_validation as cv @@ -160,9 +160,7 @@ class HoneywellUSThermostat(ClimateDevice): self._username = username self._password = password - _LOGGER.debug( - "latestData = %s ", device._data # pylint: disable=protected-access - ) + _LOGGER.debug("latestData = %s ", device._data) # not all honeywell HVACs support all modes mappings = [v for k, v in HVAC_MODE_TO_HW_MODE.items() if device.raw_ui_data[k]] @@ -174,13 +172,13 @@ class HoneywellUSThermostat(ClimateDevice): | SUPPORT_TARGET_TEMPERATURE_RANGE ) - if device._data["canControlHumidification"]: # pylint: disable=protected-access + if device._data["canControlHumidification"]: self._supported_features |= SUPPORT_TARGET_HUMIDITY if device.raw_ui_data["SwitchEmergencyHeatAllowed"]: self._supported_features |= SUPPORT_AUX_HEAT - if not device._data["hasFan"]: # pylint: disable=protected-access + if not device._data["hasFan"]: return # not all honeywell fans support all modes diff --git a/homeassistant/components/hook/switch.py b/homeassistant/components/hook/switch.py index d26f35e2dfc..14c4d4ba662 100644 --- a/homeassistant/components/hook/switch.py +++ b/homeassistant/components/hook/switch.py @@ -1,13 +1,13 @@ """Support Hook, available at hooksmarthome.com.""" -import logging import asyncio +import logging -import voluptuous as vol -import async_timeout import aiohttp +import async_timeout +import voluptuous as vol -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, CONF_TOKEN +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice +from homeassistant.const import CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/horizon/media_player.py b/homeassistant/components/horizon/media_player.py index 8bed30e88f3..44e93d26a40 100644 --- a/homeassistant/components/horizon/media_player.py +++ b/homeassistant/components/horizon/media_player.py @@ -2,10 +2,12 @@ from datetime import timedelta import logging +from horimote import Client, keys +from horimote.exceptions import AuthenticationError import voluptuous as vol from homeassistant import util -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, SUPPORT_NEXT_TRACK, @@ -56,8 +58,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Horizon platform.""" - from horimote import Client, keys - from horimote.exceptions import AuthenticationError host = config[CONF_HOST] name = config[CONF_NAME] @@ -81,12 +81,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class HorizonDevice(MediaPlayerDevice): """Representation of a Horizon HD Recorder.""" - def __init__(self, client, name, keys): + def __init__(self, client, name, remote_keys): """Initialize the remote.""" self._client = client self._name = name self._state = None - self._keys = keys + self._keys = remote_keys @property def name(self): @@ -177,7 +177,6 @@ class HorizonDevice(MediaPlayerDevice): def _send(self, key=None, channel=None): """Send a key to the Horizon device.""" - from horimote.exceptions import AuthenticationError try: if key: diff --git a/homeassistant/components/html5/const.py b/homeassistant/components/html5/const.py new file mode 100644 index 00000000000..1d0689511b2 --- /dev/null +++ b/homeassistant/components/html5/const.py @@ -0,0 +1,3 @@ +"""Constants for the HTML5 component.""" +DOMAIN = "html5" +SERVICE_DISMISS = "dismiss" diff --git a/homeassistant/components/html5/manifest.json b/homeassistant/components/html5/manifest.json index 667a5789182..dd794ae0386 100644 --- a/homeassistant/components/html5/manifest.json +++ b/homeassistant/components/html5/manifest.json @@ -2,11 +2,7 @@ "domain": "html5", "name": "HTML5 Notifications", "documentation": "https://www.home-assistant.io/integrations/html5", - "requirements": [ - "pywebpush==1.9.2" - ], - "dependencies": ["frontend"], - "codeowners": [ - "@robbiet480" - ] + "requirements": ["pywebpush==1.9.2"], + "dependencies": ["http"], + "codeowners": ["@robbiet480"] } diff --git a/homeassistant/components/html5/notify.py b/homeassistant/components/html5/notify.py index 18b7ff27ab4..b966f5ae6a1 100644 --- a/homeassistant/components/html5/notify.py +++ b/homeassistant/components/html5/notify.py @@ -1,23 +1,30 @@ """HTML5 Push Messaging notification service.""" from datetime import datetime, timedelta - from functools import partial -from urllib.parse import urlparse import json import logging import time +from urllib.parse import urlparse import uuid from aiohttp.hdrs import AUTHORIZATION import jwt -from pywebpush import WebPusher from py_vapid import Vapid +from pywebpush import WebPusher import voluptuous as vol from voluptuous.humanize import humanize_error from homeassistant.components import websocket_api from homeassistant.components.frontend import add_manifest_json_key from homeassistant.components.http import HomeAssistantView +from homeassistant.components.notify import ( + ATTR_DATA, + ATTR_TARGET, + ATTR_TITLE, + ATTR_TITLE_DEFAULT, + PLATFORM_SCHEMA, + BaseNotificationService, +) from homeassistant.const import ( HTTP_BAD_REQUEST, HTTP_INTERNAL_SERVER_ERROR, @@ -29,22 +36,12 @@ from homeassistant.helpers import config_validation as cv from homeassistant.util import ensure_unique_string from homeassistant.util.json import load_json, save_json -from homeassistant.components.notify import ( - ATTR_DATA, - ATTR_TARGET, - ATTR_TITLE, - ATTR_TITLE_DEFAULT, - DOMAIN, - PLATFORM_SCHEMA, - BaseNotificationService, -) +from .const import DOMAIN, SERVICE_DISMISS _LOGGER = logging.getLogger(__name__) REGISTRATIONS_FILE = "html5_push_registrations.conf" -SERVICE_DISMISS = "html5_dismiss" - ATTR_GCM_SENDER_ID = "gcm_sender_id" ATTR_GCM_API_KEY = "gcm_api_key" ATTR_VAPID_PUB_KEY = "vapid_pub_key" @@ -348,12 +345,12 @@ class HTML5PushCallbackView(HomeAssistantView): if parts[0].lower() != "bearer": return self.json_message( - "Authorization header must " "start with Bearer", + "Authorization header must start with Bearer", status_code=HTTP_UNAUTHORIZED, ) if len(parts) != 2: return self.json_message( - "Authorization header must " "be Bearer token", + "Authorization header must be Bearer token", status_code=HTTP_UNAUTHORIZED, ) @@ -510,7 +507,7 @@ class HTML5NotificationService(BaseNotificationService): info = REGISTER_SCHEMA(info) except vol.Invalid: _LOGGER.error( - "%s is not a valid HTML5 push notification" " target", target + "%s is not a valid HTML5 push notification target", target ) continue payload[ATTR_DATA][ATTR_JWT] = add_jwt( diff --git a/homeassistant/components/html5/services.yaml b/homeassistant/components/html5/services.yaml index e69de29bb2d..5fd068a64dc 100644 --- a/homeassistant/components/html5/services.yaml +++ b/homeassistant/components/html5/services.yaml @@ -0,0 +1,9 @@ +dismiss: + description: Dismiss a html5 notification. + fields: + target: + description: An array of targets. Optional. + example: ['my_phone', 'my_tablet'] + data: + description: Extended information of notification. Supports tag. Optional. + example: '{ "tag": "tagname" }' diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 4d3985a7af3..c720d134c9f 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -26,7 +26,6 @@ from .real_ip import setup_real_ip from .static import CACHE_HEADERS, CachingStaticResource from .view import HomeAssistantView # noqa: F401 - # mypy: allow-untyped-defs, no-check-untyped-defs DOMAIN = "http" diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index 97bd9b7d4bc..58814b77e2d 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -1,17 +1,16 @@ """Authentication for HTTP component.""" import logging +import secrets from aiohttp import hdrs from aiohttp.web import middleware import jwt -from homeassistant.auth.util import generate_secret from homeassistant.core import callback from homeassistant.util import dt as dt_util from .const import KEY_AUTHENTICATED, KEY_HASS_USER, KEY_REAL_IP - # mypy: allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) @@ -27,7 +26,7 @@ def async_sign_path(hass, refresh_token_id, path, expiration): secret = hass.data.get(DATA_SIGN_SECRET) if secret is None: - secret = hass.data[DATA_SIGN_SECRET] = generate_secret() + secret = hass.data[DATA_SIGN_SECRET] = secrets.token_hex() now = dt_util.utcnow() return "{}?{}={}".format( diff --git a/homeassistant/components/http/ban.py b/homeassistant/components/http/ban.py index 7d1e24f3698..da406c071e4 100644 --- a/homeassistant/components/http/ban.py +++ b/homeassistant/components/http/ban.py @@ -17,7 +17,6 @@ from homeassistant.util.yaml import dump from .const import KEY_REAL_IP - # mypy: allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) @@ -97,7 +96,7 @@ async def process_wrong_login(request): """ remote_addr = request[KEY_REAL_IP] - msg = "Login attempt or request with invalid authentication " "from {}".format( + msg = "Login attempt or request with invalid authentication from {}".format( remote_addr ) _LOGGER.warning(msg) @@ -151,7 +150,7 @@ async def process_success_login(request): and request.app[KEY_FAILED_LOGIN_ATTEMPTS][remote_addr] > 0 ): _LOGGER.debug( - "Login success, reset failed login attempts counter" " from %s", remote_addr + "Login success, reset failed login attempts counter from %s", remote_addr ) request.app[KEY_FAILED_LOGIN_ATTEMPTS].pop(remote_addr) diff --git a/homeassistant/components/http/cors.py b/homeassistant/components/http/cors.py index de4547f4782..2d99a049e4b 100644 --- a/homeassistant/components/http/cors.py +++ b/homeassistant/components/http/cors.py @@ -1,11 +1,10 @@ """Provide CORS support for the HTTP component.""" +from aiohttp.hdrs import ACCEPT, AUTHORIZATION, CONTENT_TYPE, ORIGIN from aiohttp.web_urldispatcher import Resource, ResourceRoute, StaticResource -from aiohttp.hdrs import ACCEPT, CONTENT_TYPE, ORIGIN, AUTHORIZATION from homeassistant.const import HTTP_HEADER_X_REQUESTED_WITH from homeassistant.core import callback - # mypy: allow-untyped-defs, no-check-untyped-defs ALLOWED_CORS_HEADERS = [ @@ -23,6 +22,7 @@ def setup_cors(app, origins): """Set up CORS.""" # This import should remain here. That way the HTTP integration can always # be imported by other integrations without it's requirements being installed. + # pylint: disable=import-outside-toplevel import aiohttp_cors cors = aiohttp_cors.setup( diff --git a/homeassistant/components/http/data_validator.py b/homeassistant/components/http/data_validator.py index 5945a4ca402..51b3b5617e4 100644 --- a/homeassistant/components/http/data_validator.py +++ b/homeassistant/components/http/data_validator.py @@ -4,7 +4,6 @@ import logging import voluptuous as vol - # mypy: allow-untyped-defs _LOGGER = logging.getLogger(__name__) @@ -21,6 +20,9 @@ class RequestDataValidator: def __init__(self, schema, allow_empty=False): """Initialize the decorator.""" + if isinstance(schema, dict): + schema = vol.Schema(schema) + self._schema = schema self._allow_empty = allow_empty diff --git a/homeassistant/components/http/real_ip.py b/homeassistant/components/http/real_ip.py index f327c86a4c1..f2334ce0a2f 100644 --- a/homeassistant/components/http/real_ip.py +++ b/homeassistant/components/http/real_ip.py @@ -8,7 +8,6 @@ from homeassistant.core import callback from .const import KEY_REAL_IP - # mypy: allow-untyped-defs diff --git a/homeassistant/components/http/static.py b/homeassistant/components/http/static.py index e6a70c9f643..a5fe686a651 100644 --- a/homeassistant/components/http/static.py +++ b/homeassistant/components/http/static.py @@ -3,18 +3,15 @@ from pathlib import Path from aiohttp import hdrs from aiohttp.web import FileResponse -from aiohttp.web_exceptions import HTTPNotFound, HTTPForbidden +from aiohttp.web_exceptions import HTTPForbidden, HTTPNotFound from aiohttp.web_urldispatcher import StaticResource - # mypy: allow-untyped-defs CACHE_TIME = 31 * 86400 # = 1 month CACHE_HEADERS = {hdrs.CACHE_CONTROL: f"public, max-age={CACHE_TIME}"} -# https://github.com/PyCQA/astroid/issues/633 -# pylint: disable=duplicate-bases class CachingStaticResource(StaticResource): """Static Resource handler that will add cache headers.""" diff --git a/homeassistant/components/http/view.py b/homeassistant/components/http/view.py index 804c90d4f96..e60091684d3 100644 --- a/homeassistant/components/http/view.py +++ b/homeassistant/components/http/view.py @@ -34,8 +34,8 @@ class HomeAssistantView: requires_auth = True cors_allowed = False - # pylint: disable=no-self-use - def context(self, request): + @staticmethod + def context(request): """Generate a context from a request.""" user = request.get("hass_user") if user is None: @@ -43,7 +43,8 @@ class HomeAssistantView: return Context(user_id=user.id) - def json(self, result, status_code=200, headers=None): + @staticmethod + def json(result, status_code=200, headers=None): """Return a JSON response.""" try: msg = json.dumps( @@ -141,9 +142,11 @@ def request_handler_factory(view, handler): elif result is None: result = b"" elif not isinstance(result, bytes): - assert False, ( - "Result should be None, string, bytes or Response. " "Got: {}" - ).format(result) + assert ( + False + ), "Result should be None, string, bytes or Response. Got: {}".format( + result + ) return web.Response(body=result, status=status_code) diff --git a/homeassistant/components/huawei_lte/.translations/bg.json b/homeassistant/components/huawei_lte/.translations/bg.json index de5cbb32b79..44746468b35 100644 --- a/homeassistant/components/huawei_lte/.translations/bg.json +++ b/homeassistant/components/huawei_lte/.translations/bg.json @@ -1,10 +1,13 @@ { "config": { "abort": { - "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "already_in_progress": "\u0422\u043e\u0432\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0432\u0435\u0447\u0435 \u0441\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430", + "not_huawei_lte": "\u041d\u0435 \u0435 Huawei LTE \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" }, "error": { "connection_failed": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435\u0442\u043e \u0435 \u043d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e", + "connection_timeout": "\u0412\u0440\u0435\u043c\u0435\u0442\u043e \u0437\u0430 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0438\u0437\u0442\u0435\u0447\u0435", "incorrect_password": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u043d\u0430 \u043f\u0430\u0440\u043e\u043b\u0430", "incorrect_username": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u043d\u043e \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435", "incorrect_username_or_password": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u043d\u043e \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435 \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u0430", diff --git a/homeassistant/components/huawei_lte/.translations/ca.json b/homeassistant/components/huawei_lte/.translations/ca.json index b213da018d2..594c2e3b16d 100644 --- a/homeassistant/components/huawei_lte/.translations/ca.json +++ b/homeassistant/components/huawei_lte/.translations/ca.json @@ -33,6 +33,7 @@ "step": { "init": { "data": { + "name": "Nom del servei de notificacions", "recipient": "Destinataris de notificacions SMS", "track_new_devices": "Segueix dispositius nous" } diff --git a/homeassistant/components/huawei_lte/.translations/da.json b/homeassistant/components/huawei_lte/.translations/da.json new file mode 100644 index 00000000000..19bc69b77fd --- /dev/null +++ b/homeassistant/components/huawei_lte/.translations/da.json @@ -0,0 +1,43 @@ +{ + "config": { + "abort": { + "already_configured": "Denne enhed er allerede konfigureret", + "already_in_progress": "Denne enhed er allerede ved at blive konfigureret", + "not_huawei_lte": "Ikke en Huawei LTE-enhed" + }, + "error": { + "connection_failed": "Forbindelsen mislykkedes", + "connection_timeout": "Timeout for forbindelse", + "incorrect_password": "Forkert adgangskode", + "incorrect_username": "Forkert brugernavn", + "incorrect_username_or_password": "Forkert brugernavn eller adgangskode", + "invalid_url": "Ugyldig webadresse", + "login_attempts_exceeded": "Maksimale loginfors\u00f8g overskredet. Pr\u00f8v igen senere", + "response_error": "Ukendt fejl fra enheden", + "unknown_connection_error": "Ukendt fejl ved tilslutning til enheden" + }, + "step": { + "user": { + "data": { + "password": "Adgangskode", + "url": "Webadresse", + "username": "Brugernavn" + }, + "description": "Indtast oplysninger om enhedsadgang. Det er valgfrit at specificere brugernavn og adgangskode, men muligg\u00f8r underst\u00f8ttelse af flere integrationsfunktioner. P\u00e5 den anden side kan brug af en autoriseret forbindelse for\u00e5rsage problemer med at f\u00e5 adgang til enhedens webgr\u00e6nseflade uden for Home Assistant, mens integrationen er aktiv, og omvendt.", + "title": "Konfigurer Huawei LTE" + } + }, + "title": "Huawei LTE" + }, + "options": { + "step": { + "init": { + "data": { + "name": "Navn p\u00e5 meddelelsestjeneste (\u00e6ndring kr\u00e6ver genstart)", + "recipient": "Modtagere af SMS-meddelelse", + "track_new_devices": "Spor nye enheder" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/.translations/en.json b/homeassistant/components/huawei_lte/.translations/en.json index 52aaafe595c..c5f2b4a2a02 100644 --- a/homeassistant/components/huawei_lte/.translations/en.json +++ b/homeassistant/components/huawei_lte/.translations/en.json @@ -33,6 +33,7 @@ "step": { "init": { "data": { + "name": "Notification service name (change requires restart)", "recipient": "SMS notification recipients", "track_new_devices": "Track new devices" } diff --git a/homeassistant/components/huawei_lte/.translations/es.json b/homeassistant/components/huawei_lte/.translations/es.json index 92ccf8fc048..c35d1eacf23 100644 --- a/homeassistant/components/huawei_lte/.translations/es.json +++ b/homeassistant/components/huawei_lte/.translations/es.json @@ -33,6 +33,7 @@ "step": { "init": { "data": { + "name": "Nombre del servicio de notificaci\u00f3n", "recipient": "Destinatarios de notificaciones por SMS", "track_new_devices": "Rastrea nuevos dispositivos" } diff --git a/homeassistant/components/huawei_lte/.translations/fr.json b/homeassistant/components/huawei_lte/.translations/fr.json index 5effea3d003..1b2be78109d 100644 --- a/homeassistant/components/huawei_lte/.translations/fr.json +++ b/homeassistant/components/huawei_lte/.translations/fr.json @@ -7,7 +7,7 @@ }, "error": { "connection_failed": "La connexion a \u00e9chou\u00e9", - "connection_timeout": "D\u00e9lai de connection d\u00e9pass\u00e9", + "connection_timeout": "D\u00e9lai de connexion d\u00e9pass\u00e9", "incorrect_password": "Mot de passe incorrect", "incorrect_username": "Nom d'utilisateur incorrect", "incorrect_username_or_password": "identifiant ou mot de passe incorrect", @@ -33,6 +33,7 @@ "step": { "init": { "data": { + "name": "Nom du service de notification", "recipient": "Destinataires des notifications SMS", "track_new_devices": "Suivre les nouveaux appareils" } diff --git a/homeassistant/components/huawei_lte/.translations/it.json b/homeassistant/components/huawei_lte/.translations/it.json index bcbae3b1b25..4ad17ecaa36 100644 --- a/homeassistant/components/huawei_lte/.translations/it.json +++ b/homeassistant/components/huawei_lte/.translations/it.json @@ -33,6 +33,7 @@ "step": { "init": { "data": { + "name": "Nome del servizio di notifica (la modifica richiede il riavvio)", "recipient": "Destinatari della notifica SMS", "track_new_devices": "Traccia nuovi dispositivi" } diff --git a/homeassistant/components/huawei_lte/.translations/ko.json b/homeassistant/components/huawei_lte/.translations/ko.json index b21e0aa0a23..f6b3d855679 100644 --- a/homeassistant/components/huawei_lte/.translations/ko.json +++ b/homeassistant/components/huawei_lte/.translations/ko.json @@ -1,10 +1,13 @@ { "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", + "already_in_progress": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "not_huawei_lte": "\ud654\uc6e8\uc774 LTE \uae30\uae30\uac00 \uc544\ub2d8" }, "error": { "connection_failed": "\uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", + "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", "incorrect_username_or_password": "\uc0ac\uc6a9\uc790 \uc774\ub984 \ub610\ub294 \ube44\ubc00\ubc88\ud638\uac00 \uc77c\uce58\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", @@ -30,6 +33,7 @@ "step": { "init": { "data": { + "name": "\uc54c\ub9bc \uc11c\ube44\uc2a4 \uc774\ub984 (\ubcc0\uacbd \uc2dc \ub2e4\uc2dc \uc2dc\uc791\ud574\uc57c \ud568)", "recipient": "SMS \uc54c\ub9bc \uc218\uc2e0\uc790", "track_new_devices": "\uc0c8\ub85c\uc6b4 \uae30\uae30 \ucd94\uc801" } diff --git a/homeassistant/components/huawei_lte/.translations/lb.json b/homeassistant/components/huawei_lte/.translations/lb.json index 3c8f0464a55..56d383edba3 100644 --- a/homeassistant/components/huawei_lte/.translations/lb.json +++ b/homeassistant/components/huawei_lte/.translations/lb.json @@ -33,6 +33,7 @@ "step": { "init": { "data": { + "name": "Numm vum Notifikatioun's Service", "recipient": "Empf\u00e4nger vun SMS Notifikatioune", "track_new_devices": "Nei Apparater verfollegen" } diff --git a/homeassistant/components/huawei_lte/.translations/nl.json b/homeassistant/components/huawei_lte/.translations/nl.json index 6d5e5c3e957..297ec922abf 100644 --- a/homeassistant/components/huawei_lte/.translations/nl.json +++ b/homeassistant/components/huawei_lte/.translations/nl.json @@ -33,6 +33,7 @@ "step": { "init": { "data": { + "name": "Naam meldingsservice (wijziging vereist opnieuw opstarten)", "recipient": "Ontvangers van sms-berichten", "track_new_devices": "Volg nieuwe apparaten" } diff --git a/homeassistant/components/huawei_lte/.translations/nn.json b/homeassistant/components/huawei_lte/.translations/nn.json new file mode 100644 index 00000000000..1a5c63f10f8 --- /dev/null +++ b/homeassistant/components/huawei_lte/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Huawei LTE" + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/.translations/no.json b/homeassistant/components/huawei_lte/.translations/no.json index 35a5d531c5d..39cb5bf87fe 100644 --- a/homeassistant/components/huawei_lte/.translations/no.json +++ b/homeassistant/components/huawei_lte/.translations/no.json @@ -33,6 +33,7 @@ "step": { "init": { "data": { + "name": "Navn p\u00e5 varslingstjeneste (endring krever omstart)", "recipient": "Mottakere av SMS-varsling", "track_new_devices": "Spor nye enheter" } diff --git a/homeassistant/components/huawei_lte/.translations/pl.json b/homeassistant/components/huawei_lte/.translations/pl.json index 3851d0a409f..a4e7d72852a 100644 --- a/homeassistant/components/huawei_lte/.translations/pl.json +++ b/homeassistant/components/huawei_lte/.translations/pl.json @@ -33,6 +33,7 @@ "step": { "init": { "data": { + "name": "Nazwa us\u0142ugi powiadomie\u0144 (zmiana wymaga ponownego uruchomienia)", "recipient": "Odbiorcy powiadomie\u0144 SMS", "track_new_devices": "\u015aled\u017a nowe urz\u0105dzenia" } diff --git a/homeassistant/components/huawei_lte/.translations/ru.json b/homeassistant/components/huawei_lte/.translations/ru.json index ec28325dcdd..3850b86167a 100644 --- a/homeassistant/components/huawei_lte/.translations/ru.json +++ b/homeassistant/components/huawei_lte/.translations/ru.json @@ -33,6 +33,7 @@ "step": { "init": { "data": { + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0441\u043b\u0443\u0436\u0431\u044b \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0439 (\u043f\u043e\u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u043a)", "recipient": "\u041f\u043e\u043b\u0443\u0447\u0430\u0442\u0435\u043b\u0438 SMS-\u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0439", "track_new_devices": "\u041e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0442\u044c \u043d\u043e\u0432\u044b\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" } diff --git a/homeassistant/components/huawei_lte/.translations/sl.json b/homeassistant/components/huawei_lte/.translations/sl.json index 0b4964069b2..5022e358ca7 100644 --- a/homeassistant/components/huawei_lte/.translations/sl.json +++ b/homeassistant/components/huawei_lte/.translations/sl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Ta naprava je \u017ee nastavljena", + "already_configured": "Ta naprava je \u017ee konfigurirana", "already_in_progress": "Ta naprava se \u017ee nastavlja", "not_huawei_lte": "Ni naprava Huawei LTE" }, diff --git a/homeassistant/components/huawei_lte/.translations/zh-Hant.json b/homeassistant/components/huawei_lte/.translations/zh-Hant.json index 37f1111b77f..201e9afec4b 100644 --- a/homeassistant/components/huawei_lte/.translations/zh-Hant.json +++ b/homeassistant/components/huawei_lte/.translations/zh-Hant.json @@ -33,6 +33,7 @@ "step": { "init": { "data": { + "name": "\u901a\u77e5\u670d\u52d9\u540d\u7a31\uff08\u8b8a\u66f4\u5f8c\u9700\u91cd\u555f\uff09", "recipient": "\u7c21\u8a0a\u901a\u77e5\u6536\u4ef6\u8005", "track_new_devices": "\u8ffd\u8e64\u65b0\u8a2d\u5099" } diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index fa1423edcca..97a57405ae0 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -3,12 +3,11 @@ from collections import defaultdict from datetime import timedelta from functools import partial -from urllib.parse import urlparse import ipaddress import logging from typing import Any, Callable, Dict, List, Set, Tuple +from urllib.parse import urlparse -import voluptuous as vol import attr from getmac import get_mac_address from huawei_lte_api.AuthorizedConnection import AuthorizedConnection @@ -20,13 +19,16 @@ from huawei_lte_api.exceptions import ( ) from requests.exceptions import Timeout from url_normalize import url_normalize +import voluptuous as vol +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN -from homeassistant.config_entries import ConfigEntry, SOURCE_IMPORT +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( + CONF_NAME, CONF_PASSWORD, CONF_RECIPIENT, CONF_URL, @@ -35,8 +37,11 @@ from homeassistant.const import ( ) from homeassistant.core import CALLBACK_TYPE from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import config_validation as cv, discovery -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import ( + config_validation as cv, + device_registry as dr, + discovery, +) from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -45,22 +50,29 @@ from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import HomeAssistantType + from .const import ( + ADMIN_SERVICES, ALL_KEYS, CONNECTION_TIMEOUT, DEFAULT_DEVICE_NAME, + DEFAULT_NOTIFY_SERVICE_NAME, DOMAIN, KEY_DEVICE_BASIC_INFORMATION, KEY_DEVICE_INFORMATION, KEY_DEVICE_SIGNAL, KEY_DIALUP_MOBILE_DATASWITCH, + KEY_MONITORING_STATUS, KEY_MONITORING_TRAFFIC_STATISTICS, KEY_WLAN_HOST_LIST, + SERVICE_CLEAR_TRAFFIC_STATISTICS, + SERVICE_REBOOT, + SERVICE_RESUME_INTEGRATION, + SERVICE_SUSPEND_INTEGRATION, UPDATE_OPTIONS_SIGNAL, UPDATE_SIGNAL, ) - _LOGGER = logging.getLogger(__name__) # dicttoxml (used by huawei-lte-api) has uselessly verbose INFO level. @@ -75,9 +87,10 @@ NOTIFY_SCHEMA = vol.Any( None, vol.Schema( { + vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_RECIPIENT): vol.Any( None, vol.All(cv.ensure_list, [cv.string]) - ) + ), } ), ) @@ -101,6 +114,15 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) +SERVICE_SCHEMA = vol.Schema({vol.Optional(CONF_URL): cv.url}) + +CONFIG_ENTRY_PLATFORMS = ( + BINARY_SENSOR_DOMAIN, + DEVICE_TRACKER_DOMAIN, + SENSOR_DOMAIN, + SWITCH_DOMAIN, +) + @attr.s class Router: @@ -118,6 +140,7 @@ class Router: ) unload_handlers: List[CALLBACK_TYPE] = attr.ib(init=False, factory=list) client: Client + suspended = attr.ib(init=False, default=False) def __attrs_post_init__(self): """Set up internal state on init.""" @@ -139,53 +162,64 @@ class Router: @property def device_connections(self) -> Set[Tuple[str, str]]: """Get router connections for device registry.""" - return {(dr.CONNECTION_NETWORK_MAC, self.mac)} + return {(dr.CONNECTION_NETWORK_MAC, self.mac)} if self.mac else set() + + def _get_data(self, key: str, func: Callable[[None], Any]) -> None: + if not self.subscriptions.get(key): + return + _LOGGER.debug("Getting %s for subscribers %s", key, self.subscriptions[key]) + try: + self.data[key] = func() + except ResponseErrorNotSupportedException: + _LOGGER.info( + "%s not supported by device, excluding from future updates", key + ) + self.subscriptions.pop(key) + except ResponseErrorLoginRequiredException: + if isinstance(self.connection, AuthorizedConnection): + _LOGGER.debug("Trying to authorize again...") + if self.connection.enforce_authorized_connection(): + _LOGGER.debug( + "...success, %s will be updated by a future periodic run", key, + ) + else: + _LOGGER.debug("...failed") + return + _LOGGER.info( + "%s requires authorization, excluding from future updates", key + ) + self.subscriptions.pop(key) + finally: + _LOGGER.debug("%s=%s", key, self.data.get(key)) def update(self) -> None: """Update router data.""" - def get_data(key: str, func: Callable[[None], Any]) -> None: - if not self.subscriptions[key]: - return - _LOGGER.debug("Getting %s for subscribers %s", key, self.subscriptions[key]) - try: - self.data[key] = func() - except ResponseErrorNotSupportedException: - _LOGGER.info( - "%s not supported by device, excluding from future updates", key - ) - self.subscriptions.pop(key) - except ResponseErrorLoginRequiredException: - _LOGGER.info( - "%s requires authorization, excluding from future updates", key - ) - self.subscriptions.pop(key) - finally: - _LOGGER.debug("%s=%s", key, self.data.get(key)) + if self.suspended: + _LOGGER.debug("Integration suspended, not updating data") + return - get_data(KEY_DEVICE_INFORMATION, self.client.device.information) + self._get_data(KEY_DEVICE_INFORMATION, self.client.device.information) if self.data.get(KEY_DEVICE_INFORMATION): # Full information includes everything in basic self.subscriptions.pop(KEY_DEVICE_BASIC_INFORMATION, None) - get_data(KEY_DEVICE_BASIC_INFORMATION, self.client.device.basic_information) - get_data(KEY_DEVICE_SIGNAL, self.client.device.signal) - get_data(KEY_DIALUP_MOBILE_DATASWITCH, self.client.dial_up.mobile_dataswitch) - get_data( + self._get_data( + KEY_DEVICE_BASIC_INFORMATION, self.client.device.basic_information + ) + self._get_data(KEY_DEVICE_SIGNAL, self.client.device.signal) + self._get_data( + KEY_DIALUP_MOBILE_DATASWITCH, self.client.dial_up.mobile_dataswitch + ) + self._get_data(KEY_MONITORING_STATUS, self.client.monitoring.status) + self._get_data( KEY_MONITORING_TRAFFIC_STATISTICS, self.client.monitoring.traffic_statistics ) - get_data(KEY_WLAN_HOST_LIST, self.client.wlan.host_list) + self._get_data(KEY_WLAN_HOST_LIST, self.client.wlan.host_list) self.signal_update() - def cleanup(self, *_) -> None: - """Clean up resources.""" - - self.subscriptions.clear() - - for handler in self.unload_handlers: - handler() - self.unload_handlers.clear() - + def logout(self) -> None: + """Log out router session.""" if not isinstance(self.connection, AuthorizedConnection): return try: @@ -197,6 +231,17 @@ class Router: except Exception: # pylint: disable=broad-except _LOGGER.warning("Logout error", exc_info=True) + def cleanup(self, *_) -> None: + """Clean up resources.""" + + self.subscriptions.clear() + + for handler in self.unload_handlers: + handler() + self.unload_handlers.clear() + + self.logout() + @attr.s class HuaweiLteData: @@ -232,6 +277,13 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) ): new_options[f"{CONF_RECIPIENT}_from_yaml"] = yaml_recipient new_options[CONF_RECIPIENT] = yaml_recipient + yaml_notify_name = yaml_config.get(NOTIFY_DOMAIN, {}).get(CONF_NAME) + if ( + yaml_notify_name is not None + and yaml_notify_name != config_entry.options.get(f"{CONF_NAME}_from_yaml") + ): + new_options[f"{CONF_NAME}_from_yaml"] = yaml_notify_name + new_options[CONF_NAME] = yaml_notify_name # Update entry if overrides were found if new_data or new_options: hass.config_entries.async_update_entry( @@ -314,7 +366,7 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) ) # Forward config entry setup to platforms - for domain in (DEVICE_TRACKER_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN): + for domain in CONFIG_ENTRY_PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(config_entry, domain) ) @@ -323,7 +375,11 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) hass, NOTIFY_DOMAIN, DOMAIN, - {CONF_URL: url, CONF_RECIPIENT: config_entry.options.get(CONF_RECIPIENT)}, + { + CONF_URL: url, + CONF_NAME: config_entry.options.get(CONF_NAME, DEFAULT_NOTIFY_SERVICE_NAME), + CONF_RECIPIENT: config_entry.options.get(CONF_RECIPIENT), + }, hass.data[DOMAIN].hass_config, ) @@ -357,7 +413,7 @@ async def async_unload_entry( """Unload config entry.""" # Forward config entry unload to platforms - for domain in (DEVICE_TRACKER_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN): + for domain in CONFIG_ENTRY_PLATFORMS: await hass.config_entries.async_forward_entry_unload(config_entry, domain) # Forget about the router and invoke its cleanup @@ -377,6 +433,56 @@ async def async_setup(hass: HomeAssistantType, config) -> bool: for router_config in config.get(DOMAIN, []): domain_config[url_normalize(router_config.pop(CONF_URL))] = router_config + def service_handler(service) -> None: + """Apply a service.""" + url = service.data.get(CONF_URL) + routers = hass.data[DOMAIN].routers + if url: + router = routers.get(url) + elif not routers: + _LOGGER.error("%s: no routers configured", service.service) + return + elif len(routers) == 1: + router = next(iter(routers.values())) + else: + _LOGGER.error( + "%s: more than one router configured, must specify one of URLs %s", + service.service, + sorted(routers), + ) + return + if not router: + _LOGGER.error("%s: router %s unavailable", service.service, url) + return + + if service.service == SERVICE_CLEAR_TRAFFIC_STATISTICS: + if router.suspended: + _LOGGER.debug("%s: ignored, integration suspended", service.service) + return + result = router.client.monitoring.set_clear_traffic() + _LOGGER.debug("%s: %s", service.service, result) + elif service.service == SERVICE_REBOOT: + if router.suspended: + _LOGGER.debug("%s: ignored, integration suspended", service.service) + return + result = router.client.device.reboot() + _LOGGER.debug("%s: %s", service.service, result) + elif service.service == SERVICE_RESUME_INTEGRATION: + # Login will be handled automatically on demand + router.suspended = False + _LOGGER.debug("%s: %s", service.service, "done") + elif service.service == SERVICE_SUSPEND_INTEGRATION: + router.logout() + router.suspended = True + _LOGGER.debug("%s: %s", service.service, "done") + else: + _LOGGER.error("%s: unsupported service", service.service) + + for service in ADMIN_SERVICES: + hass.helpers.service.async_register_admin_service( + DOMAIN, service, service_handler, schema=SERVICE_SCHEMA, + ) + for url, router_config in domain_config.items(): hass.async_create_task( hass.config_entries.flow.async_init( diff --git a/homeassistant/components/huawei_lte/binary_sensor.py b/homeassistant/components/huawei_lte/binary_sensor.py new file mode 100644 index 00000000000..104933fe714 --- /dev/null +++ b/homeassistant/components/huawei_lte/binary_sensor.py @@ -0,0 +1,122 @@ +"""Support for Huawei LTE binary sensors.""" + +import logging +from typing import Optional + +import attr +from huawei_lte_api.enums.cradle import ConnectionStatusEnum + +from homeassistant.components.binary_sensor import ( + DOMAIN as BINARY_SENSOR_DOMAIN, + BinarySensorDevice, +) +from homeassistant.const import CONF_URL + +from . import HuaweiLteBaseEntity +from .const import DOMAIN, KEY_MONITORING_STATUS + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up from config entry.""" + router = hass.data[DOMAIN].routers[config_entry.data[CONF_URL]] + entities = [] + + if router.data.get(KEY_MONITORING_STATUS): + entities.append(HuaweiLteMobileConnectionBinarySensor(router)) + + async_add_entities(entities, True) + + +@attr.s +class HuaweiLteBaseBinarySensor(HuaweiLteBaseEntity, BinarySensorDevice): + """Huawei LTE binary sensor device base class.""" + + key: str + item: str + _raw_state: Optional[str] = attr.ib(init=False, default=None) + + async def async_added_to_hass(self): + """Subscribe to needed data on add.""" + await super().async_added_to_hass() + self.router.subscriptions[self.key].add(f"{BINARY_SENSOR_DOMAIN}/{self.item}") + + async def async_will_remove_from_hass(self): + """Unsubscribe from needed data on remove.""" + await super().async_will_remove_from_hass() + self.router.subscriptions[self.key].remove( + f"{BINARY_SENSOR_DOMAIN}/{self.item}" + ) + + async def async_update(self): + """Update state.""" + try: + value = self.router.data[self.key][self.item] + except KeyError: + _LOGGER.debug("%s[%s] not in data", self.key, self.item) + self._available = False + return + self._available = True + self._raw_state = str(value) + + +CONNECTION_STATE_ATTRIBUTES = { + str(ConnectionStatusEnum.CONNECTING): "Connecting", + str(ConnectionStatusEnum.DISCONNECTING): "Disconnecting", + str(ConnectionStatusEnum.CONNECT_FAILED): "Connect failed", + str(ConnectionStatusEnum.CONNECT_STATUS_NULL): "Status not available", + str(ConnectionStatusEnum.CONNECT_STATUS_ERROR): "Status error", +} + + +@attr.s +class HuaweiLteMobileConnectionBinarySensor(HuaweiLteBaseBinarySensor): + """Huawei LTE mobile connection binary sensor.""" + + def __attrs_post_init__(self): + """Initialize identifiers.""" + self.key = KEY_MONITORING_STATUS + self.item = "ConnectionStatus" + + @property + def _entity_name(self) -> str: + return "Mobile connection" + + @property + def _device_unique_id(self) -> str: + return f"{self.key}.{self.item}" + + @property + def is_on(self) -> bool: + """Return whether the binary sensor is on.""" + return self._raw_state and int(self._raw_state) in ( + ConnectionStatusEnum.CONNECTED, + ConnectionStatusEnum.DISCONNECTING, + ) + + @property + def assumed_state(self) -> bool: + """Return True if real state is assumed, not known.""" + return not self._raw_state or int(self._raw_state) not in ( + ConnectionStatusEnum.CONNECT_FAILED, + ConnectionStatusEnum.CONNECTED, + ConnectionStatusEnum.DISCONNECTED, + ) + + @property + def icon(self): + """Return mobile connectivity sensor icon.""" + return "mdi:signal" if self.is_on else "mdi:signal-off" + + @property + def device_state_attributes(self): + """Get additional attributes related to connection status.""" + attributes = super().device_state_attributes + if self._raw_state in CONNECTION_STATE_ATTRIBUTES: + if attributes is None: + attributes = {} + attributes["additional_state"] = CONNECTION_STATE_ATTRIBUTES[ + self._raw_state + ] + return attributes diff --git a/homeassistant/components/huawei_lte/config_flow.py b/homeassistant/components/huawei_lte/config_flow.py index 1bc3753bdd7..0dcdb6636c6 100644 --- a/homeassistant/components/huawei_lte/config_flow.py +++ b/homeassistant/components/huawei_lte/config_flow.py @@ -3,15 +3,16 @@ from collections import OrderedDict import logging from typing import Optional +from urllib.parse import urlparse from huawei_lte_api.AuthorizedConnection import AuthorizedConnection from huawei_lte_api.Client import Client from huawei_lte_api.Connection import Connection from huawei_lte_api.exceptions import ( - LoginErrorUsernameWrongException, LoginErrorPasswordWrongException, - LoginErrorUsernamePasswordWrongException, LoginErrorUsernamePasswordOverrunException, + LoginErrorUsernamePasswordWrongException, + LoginErrorUsernameWrongException, ResponseErrorException, ) from requests.exceptions import Timeout @@ -19,15 +20,20 @@ from url_normalize import url_normalize import voluptuous as vol from homeassistant import config_entries -from homeassistant.components.ssdp import ATTR_HOST, ATTR_NAME, ATTR_PRESENTATIONURL -from homeassistant.const import CONF_PASSWORD, CONF_RECIPIENT, CONF_URL, CONF_USERNAME +from homeassistant.components import ssdp +from homeassistant.const import ( + CONF_NAME, + CONF_PASSWORD, + CONF_RECIPIENT, + CONF_URL, + CONF_USERNAME, +) from homeassistant.core import callback -from .const import CONNECTION_TIMEOUT, DEFAULT_DEVICE_NAME -# https://github.com/PyCQA/pylint/issues/3202 +# see https://github.com/PyCQA/pylint/issues/3202 about the DOMAIN's pylint issue +from .const import CONNECTION_TIMEOUT, DEFAULT_DEVICE_NAME, DEFAULT_NOTIFY_SERVICE_NAME from .const import DOMAIN # pylint: disable=unused-import - _LOGGER = logging.getLogger(__name__) @@ -209,13 +215,14 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle SSDP initiated config flow.""" # Attempt to distinguish from other non-LTE Huawei router devices, at least # some ones we are interested in have "Mobile Wi-Fi" friendlyName. - if "mobile" not in discovery_info.get(ATTR_NAME, "").lower(): + 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 discovery_info.get( - ATTR_PRESENTATIONURL, f"http://{discovery_info[ATTR_HOST]}/" + ssdp.ATTR_UPNP_PRESENTATION_URL, + f"http://{urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]).hostname}/", ) ) @@ -241,14 +248,22 @@ class OptionsFlowHandler(config_entries.OptionsFlow): 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) + # Preserve existing options, for example *_from_yaml markers + data = {**self.config_entry.options, **user_input} + return self.async_create_entry(title="", data=data) data_schema = vol.Schema( { + vol.Optional( + CONF_NAME, + default=self.config_entry.options.get( + CONF_NAME, DEFAULT_NOTIFY_SERVICE_NAME + ), + ): str, vol.Optional( CONF_RECIPIENT, default=self.config_entry.options.get(CONF_RECIPIENT, ""), - ): str + ): str, } ) return self.async_show_form(step_id="init", data_schema=data_schema) diff --git a/homeassistant/components/huawei_lte/const.py b/homeassistant/components/huawei_lte/const.py index 8dae63f6538..c6837fce06c 100644 --- a/homeassistant/components/huawei_lte/const.py +++ b/homeassistant/components/huawei_lte/const.py @@ -3,6 +3,7 @@ DOMAIN = "huawei_lte" DEFAULT_DEVICE_NAME = "LTE" +DEFAULT_NOTIFY_SERVICE_NAME = DOMAIN UPDATE_SIGNAL = f"{DOMAIN}_update" UPDATE_OPTIONS_SIGNAL = f"{DOMAIN}_options_update" @@ -12,13 +13,28 @@ UNIT_SECONDS = "s" CONNECTION_TIMEOUT = 10 +SERVICE_CLEAR_TRAFFIC_STATISTICS = "clear_traffic_statistics" +SERVICE_REBOOT = "reboot" +SERVICE_RESUME_INTEGRATION = "resume_integration" +SERVICE_SUSPEND_INTEGRATION = "suspend_integration" + +ADMIN_SERVICES = { + SERVICE_CLEAR_TRAFFIC_STATISTICS, + SERVICE_REBOOT, + SERVICE_RESUME_INTEGRATION, + SERVICE_SUSPEND_INTEGRATION, +} + KEY_DEVICE_BASIC_INFORMATION = "device_basic_information" KEY_DEVICE_INFORMATION = "device_information" KEY_DEVICE_SIGNAL = "device_signal" KEY_DIALUP_MOBILE_DATASWITCH = "dialup_mobile_dataswitch" +KEY_MONITORING_STATUS = "monitoring_status" KEY_MONITORING_TRAFFIC_STATISTICS = "monitoring_traffic_statistics" KEY_WLAN_HOST_LIST = "wlan_host_list" +BINARY_SENSOR_KEYS = {KEY_MONITORING_STATUS} + DEVICE_TRACKER_KEYS = {KEY_WLAN_HOST_LIST} SENSOR_KEYS = { @@ -29,4 +45,4 @@ SENSOR_KEYS = { SWITCH_KEYS = {KEY_DIALUP_MOBILE_DATASWITCH} -ALL_KEYS = DEVICE_TRACKER_KEYS | SENSOR_KEYS | SWITCH_KEYS +ALL_KEYS = BINARY_SENSOR_KEYS | DEVICE_TRACKER_KEYS | SENSOR_KEYS | SWITCH_KEYS diff --git a/homeassistant/components/huawei_lte/device_tracker.py b/homeassistant/components/huawei_lte/device_tracker.py index d95d99e7126..a9c61831fdd 100644 --- a/homeassistant/components/huawei_lte/device_tracker.py +++ b/homeassistant/components/huawei_lte/device_tracker.py @@ -2,7 +2,7 @@ import logging import re -from typing import Any, Dict, Set +from typing import Any, Dict, List, Optional, Set import attr from stringcase import snakecase @@ -15,10 +15,10 @@ from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.const import CONF_URL from homeassistant.helpers import entity_registry from homeassistant.helpers.dispatcher import async_dispatcher_connect + from . import HuaweiLteBaseEntity from .const import DOMAIN, KEY_WLAN_HOST_LIST, UPDATE_SIGNAL - _LOGGER = logging.getLogger(__name__) _DEVICE_SCAN = f"{DEVICE_TRACKER_DOMAIN}/device_scan" @@ -40,13 +40,17 @@ async def async_setup_entry(hass, config_entry, async_add_entities): # Initialize already tracked entities tracked: Set[str] = set() registry = await entity_registry.async_get_registry(hass) + known_entities: List[HuaweiLteScannerEntity] = [] for entity in registry.entities.values(): if ( entity.domain == DEVICE_TRACKER_DOMAIN and entity.config_entry_id == config_entry.entry_id ): tracked.add(entity.unique_id) - async_add_new_entities(hass, router.url, async_add_entities, tracked, True) + known_entities.append( + HuaweiLteScannerEntity(router, entity.unique_id.partition("-")[2]) + ) + async_add_entities(known_entities, True) # Tell parent router to poll hosts list to gather new devices router.subscriptions[KEY_WLAN_HOST_LIST].add(_DEVICE_SCAN) @@ -66,13 +70,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_new_entities(hass, router.url, async_add_entities, tracked) -def async_add_new_entities( - hass, router_url, async_add_entities, tracked, included: bool = False -): - """Add new entities. - - :param included: if True, setup only items in tracked, and vice versa - """ +def async_add_new_entities(hass, router_url, async_add_entities, tracked): + """Add new entities that are not already being tracked.""" router = hass.data[DOMAIN].routers[router_url] try: hosts = router.data[KEY_WLAN_HOST_LIST]["Hosts"]["Host"] @@ -83,8 +82,7 @@ def async_add_new_entities( new_entities = [] for host in (x for x in hosts if x.get("MacAddress")): entity = HuaweiLteScannerEntity(router, host["MacAddress"]) - tracking = entity.unique_id in tracked - if tracking != included: + if entity.unique_id in tracked: continue tracked.add(entity.unique_id) new_entities.append(entity) @@ -113,12 +111,16 @@ class HuaweiLteScannerEntity(HuaweiLteBaseEntity, ScannerEntity): mac: str = attr.ib() _is_connected: bool = attr.ib(init=False, default=False) - _name: str = attr.ib(init=False, default="device") + _hostname: Optional[str] = attr.ib(init=False, default=None) _device_state_attributes: Dict[str, Any] = attr.ib(init=False, factory=dict) + def __attrs_post_init__(self): + """Initialize internal state.""" + self._device_state_attributes["mac_address"] = self.mac + @property def _entity_name(self) -> str: - return self._name + return self._hostname or self.mac @property def _device_unique_id(self) -> str: @@ -145,11 +147,9 @@ class HuaweiLteScannerEntity(HuaweiLteBaseEntity, ScannerEntity): host = next((x for x in hosts if x.get("MacAddress") == self.mac), None) self._is_connected = host is not None if self._is_connected: - self._name = host.get("HostName", self.mac) + self._hostname = host.get("HostName") self._device_state_attributes = { - _better_snakecase(k): v - for k, v in host.items() - if k not in ("MacAddress", "HostName") + _better_snakecase(k): v for k, v in host.items() if k != "HostName" } diff --git a/homeassistant/components/huawei_lte/manifest.json b/homeassistant/components/huawei_lte/manifest.json index 4ea54188688..8fd4ba4bec1 100644 --- a/homeassistant/components/huawei_lte/manifest.json +++ b/homeassistant/components/huawei_lte/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/huawei_lte", "requirements": [ "getmac==0.8.1", - "huawei-lte-api==1.4.3", + "huawei-lte-api==1.4.4", "stringcase==1.2.0", "url-normalize==1.4.1" ], diff --git a/homeassistant/components/huawei_lte/notify.py b/homeassistant/components/huawei_lte/notify.py index 4b5a63756b5..5619a5d702c 100644 --- a/homeassistant/components/huawei_lte/notify.py +++ b/homeassistant/components/huawei_lte/notify.py @@ -6,13 +6,12 @@ from typing import Any, List import attr from huawei_lte_api.exceptions import ResponseErrorException -from homeassistant.components.notify import BaseNotificationService, ATTR_TARGET +from homeassistant.components.notify import ATTR_TARGET, BaseNotificationService from homeassistant.const import CONF_RECIPIENT, CONF_URL from . import Router from .const import DOMAIN - _LOGGER = logging.getLogger(__name__) @@ -45,6 +44,12 @@ class HuaweiLteSmsNotificationService(BaseNotificationService): if not targets or not message: return + if self.router.suspended: + _LOGGER.debug( + "Integration suspended, not sending notification to %s", targets + ) + return + try: resp = self.router.client.sms.send_sms( phone_numbers=targets, message=message diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index 99170d4e7c0..3b6b75edfba 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -6,12 +6,11 @@ from typing import Optional import attr -from homeassistant.const import CONF_URL, STATE_UNKNOWN from homeassistant.components.sensor import ( DEVICE_CLASS_SIGNAL_STRENGTH, DOMAIN as SENSOR_DOMAIN, ) -from homeassistant.helpers import entity_registry +from homeassistant.const import CONF_URL, STATE_UNKNOWN from . import HuaweiLteBaseEntity from .const import ( @@ -23,7 +22,6 @@ from .const import ( UNIT_SECONDS, ) - _LOGGER = logging.getLogger(__name__) @@ -170,23 +168,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): HuaweiLteSensor(router, key, item, SENSOR_META.get((key, item), {})) ) - # Pre-0.97 unique id migration. Old ones used the device serial number - # (see comments in HuaweiLteData._setup_lte for more info), as well as - # had a bug that joined the path str with periods, not the path components, - # resulting e.g. *_device_signal.sinr to end up as - # *_d.e.v.i.c.e._.s.i.g.n.a.l...s.i.n.r - entreg = await entity_registry.async_get_registry(hass) - for entid, ent in entreg.entities.items(): - if ent.platform != DOMAIN: - continue - for sensor in sensors: - oldsuf = ".".join(f"{sensor.key}.{sensor.item}") - if ent.unique_id.endswith(f"_{oldsuf}"): - entreg.async_update_entity(entid, new_unique_id=sensor.unique_id) - _LOGGER.debug( - "Updated entity %s unique id to %s", entid, sensor.unique_id - ) - async_add_entities(sensors, True) diff --git a/homeassistant/components/huawei_lte/services.yaml b/homeassistant/components/huawei_lte/services.yaml new file mode 100644 index 00000000000..bcb9be33299 --- /dev/null +++ b/homeassistant/components/huawei_lte/services.yaml @@ -0,0 +1,30 @@ +clear_traffic_statistics: + description: Clear traffic statistics. + fields: + url: + description: URL of router to clear; optional when only one is configured. + example: http://192.168.100.1/ + +reboot: + description: Reboot router. + fields: + url: + description: URL of router to reboot; optional when only one is configured. + example: http://192.168.100.1/ + +resume_integration: + description: Resume suspended integration. + fields: + url: + description: URL of router to resume integration for; optional when only one is configured. + example: http://192.168.100.1/ + +suspend_integration: + description: > + Suspend integration. Suspending logs the integration out from the router, and stops accessing it. + Useful e.g. if accessing the router web interface from another source such as a web browser is temporarily required. + Invoke the resume_integration service to resume. + fields: + url: + description: URL of router to resume integration for; optional when only one is configured. + example: http://192.168.100.1/ diff --git a/homeassistant/components/huawei_lte/strings.json b/homeassistant/components/huawei_lte/strings.json index 17684253671..c5f2b4a2a02 100644 --- a/homeassistant/components/huawei_lte/strings.json +++ b/homeassistant/components/huawei_lte/strings.json @@ -7,13 +7,13 @@ }, "error": { "connection_failed": "Connection failed", + "connection_timeout": "Connection timeout", "incorrect_password": "Incorrect password", "incorrect_username": "Incorrect username", "incorrect_username_or_password": "Incorrect username or password", "invalid_url": "Invalid URL", "login_attempts_exceeded": "Maximum login attempts exceeded, please try again later", "response_error": "Unknown error from device", - "connection_timeout": "Connection timeout", "unknown_connection_error": "Unknown error connecting to device" }, "step": { @@ -33,6 +33,7 @@ "step": { "init": { "data": { + "name": "Notification service name (change requires restart)", "recipient": "SMS notification recipients", "track_new_devices": "Track new devices" } diff --git a/homeassistant/components/huawei_lte/switch.py b/homeassistant/components/huawei_lte/switch.py index bff82227b80..44d2da0c898 100644 --- a/homeassistant/components/huawei_lte/switch.py +++ b/homeassistant/components/huawei_lte/switch.py @@ -11,10 +11,10 @@ from homeassistant.components.switch import ( SwitchDevice, ) from homeassistant.const import CONF_URL + from . import HuaweiLteBaseEntity from .const import DOMAIN, KEY_DIALUP_MOBILE_DATASWITCH - _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/huawei_router/device_tracker.py b/homeassistant/components/huawei_router/device_tracker.py index b7b5731dfd3..4b52060e425 100644 --- a/homeassistant/components/huawei_router/device_tracker.py +++ b/homeassistant/components/huawei_router/device_tracker.py @@ -1,19 +1,19 @@ """Support for HUAWEI routers.""" import base64 +from collections import namedtuple import logging import re -from collections import namedtuple import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/hue/.translations/bg.json b/homeassistant/components/hue/.translations/bg.json index 04ee6d13831..5f28f4bde40 100644 --- a/homeassistant/components/hue/.translations/bg.json +++ b/homeassistant/components/hue/.translations/bg.json @@ -26,6 +26,6 @@ "title": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 \u0445\u044a\u0431" } }, - "title": "\u0411\u0430\u0437\u043e\u0432\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f Philips Hue" + "title": "Philips Hue" } } \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/da.json b/homeassistant/components/hue/.translations/da.json index 756f3b7e44b..afcfd7071e7 100644 --- a/homeassistant/components/hue/.translations/da.json +++ b/homeassistant/components/hue/.translations/da.json @@ -1,17 +1,17 @@ { "config": { "abort": { - "all_configured": "Alle Philips Hue brigdes er konfigureret", + "all_configured": "Alle Philips Hue-broer er allerede konfigureret", "already_configured": "Bridgen er allerede konfigureret", - "already_in_progress": "Bro konfiguration er allerede i gang.", + "already_in_progress": "Bro-konfiguration er allerede i gang.", "cannot_connect": "Kunne ikke oprette forbindelse til bridgen", - "discover_timeout": "Ingen Philips Hue bridge fundet", - "no_bridges": "Ingen Philips Hue bridge fundet", - "not_hue_bridge": "Ikke en Hue bro", + "discover_timeout": "Ingen Philips Hue-bro fundet", + "no_bridges": "Ingen Philips Hue-broer fundet", + "not_hue_bridge": "Ikke en Hue-bro", "unknown": "Ukendt fejl opstod" }, "error": { - "linking": "Ukendt sammenkoblings fejl opstod", + "linking": "Der opstod en ukendt linkfejl.", "register_failed": "Det lykkedes ikke at registrere, pr\u00f8v igen" }, "step": { @@ -22,8 +22,8 @@ "title": "V\u00e6lg Hue bridge" }, "link": { - "description": "Tryk p\u00e5 knappen p\u00e5 bridgen for at registrere Philips Hue med Home Assistant. \n\n ! [Placering af knap p\u00e5 bro] (/static/images/config_philips_hue.jpg)", - "title": "Link Hub" + "description": "Tryk p\u00e5 knappen p\u00e5 broen for at registrere Philips Hue med Home Assistant. \n\n ![Placering af knap p\u00e5 bro](/static/images/config_philips_hue.jpg)", + "title": "Forbind Hub" } }, "title": "Philips Hue" diff --git a/homeassistant/components/hue/.translations/ro.json b/homeassistant/components/hue/.translations/ro.json index a2ecf8964b6..9da771a52dc 100644 --- a/homeassistant/components/hue/.translations/ro.json +++ b/homeassistant/components/hue/.translations/ro.json @@ -19,6 +19,7 @@ "link": { "description": "Ap\u0103sa\u021bi butonul de pe pod pentru a \u00eenregistra Philips Hue cu Home Assistant. \n\n ! [Loca\u021bia butonului pe pod] (/ static / images / config_philips_hue.jpg)" } - } + }, + "title": "Philips Hue" } } \ No newline at end of file diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index 027ec205195..cbcb21db7d0 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -2,16 +2,14 @@ import ipaddress import logging +from aiohue.util import normalize_bridge_id import voluptuous as vol -from homeassistant import config_entries -from homeassistant.const import CONF_FILENAME, CONF_HOST +from homeassistant import config_entries, core +from homeassistant.const import CONF_HOST from homeassistant.helpers import config_validation as cv, device_registry as dr from .bridge import HueBridge -from .config_flow import ( - configured_hosts, -) # Loading the config flow file will register the flow from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -32,8 +30,6 @@ BRIDGE_CONFIG_SCHEMA = vol.Schema( { # Validate as IP address and then convert back to a string. vol.Required(CONF_HOST): vol.All(ipaddress.ip_address, cv.string), - # This is for legacy reasons and is only used for importing auth. - vol.Optional(CONF_FILENAME, default=PHUE_CONFIG_FILE): cv.string, vol.Optional( CONF_ALLOW_UNREACHABLE, default=DEFAULT_ALLOW_UNREACHABLE ): cv.boolean, @@ -65,7 +61,6 @@ async def async_setup(hass, config): hass.data[DOMAIN] = {} hass.data[DATA_CONFIGS] = {} - configured = configured_hosts(hass) # User has configured bridges if CONF_BRIDGES not in conf: @@ -73,36 +68,37 @@ async def async_setup(hass, config): bridges = conf[CONF_BRIDGES] + configured_hosts = set( + entry.data["host"] for entry in hass.config_entries.async_entries(DOMAIN) + ) + for bridge_conf in bridges: host = bridge_conf[CONF_HOST] # Store config in hass.data so the config entry can find it hass.data[DATA_CONFIGS][host] = bridge_conf - # If configured, the bridge will be set up during config entry phase - if host in configured: + if host in configured_hosts: continue - # No existing config entry found, try importing it or trigger link - # config flow if no existing auth. Because we're inside the setup of - # this component we'll have to use hass.async_add_job to avoid a - # deadlock: creating a config entry will set up the component but the - # setup would block till the entry is created! + # No existing config entry found, trigger link config flow. Because we're + # inside the setup of this component we'll have to use hass.async_add_job + # to avoid a deadlock: creating a config entry will set up the component + # but the setup would block till the entry is created! hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, - data={ - "host": bridge_conf[CONF_HOST], - "path": bridge_conf[CONF_FILENAME], - }, + data={"host": bridge_conf[CONF_HOST]}, ) ) return True -async def async_setup_entry(hass, entry): +async def async_setup_entry( + hass: core.HomeAssistant, entry: config_entries.ConfigEntry +): """Set up a bridge from a config entry.""" host = entry.data["host"] config = hass.data[DATA_CONFIGS].get(host) @@ -121,6 +117,13 @@ async def async_setup_entry(hass, entry): hass.data[DOMAIN][host] = bridge config = bridge.api.config + + # For backwards compat + if entry.unique_id is None: + hass.config_entries.async_update_entry( + entry, unique_id=normalize_bridge_id(config.bridgeid) + ) + device_registry = await dr.async_get_registry(hass) device_registry.async_get_or_create( config_entry_id=entry.entry_id, @@ -133,9 +136,7 @@ async def async_setup_entry(hass, entry): ) if config.swupdate2_bridge_state == "readytoinstall": - err = ( - "Please check for software updates of the bridge " "in the Philips Hue App." - ) + err = "Please check for software updates of the bridge in the Philips Hue App." _LOGGER.warning(err) return True diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index 5015ec669aa..58a744dd5b0 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -3,8 +3,10 @@ import asyncio import aiohue import async_timeout +import slugify as unicode_slug import voluptuous as vol +from homeassistant import core from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, config_validation as cv @@ -32,6 +34,7 @@ class HueBridge: self.available = True self.authorized = False self.api = None + self.parallel_updates_semaphore = None @property def host(self): @@ -43,8 +46,15 @@ class HueBridge: host = self.host hass = self.hass + bridge = aiohue.Bridge( + host, + username=self.config_entry.data["username"], + websession=aiohttp_client.async_get_clientsession(hass), + ) + try: - self.api = await get_bridge(hass, host, self.config_entry.data["username"]) + await authenticate_bridge(hass, bridge) + except AuthenticationRequired: # Usernames can become invalid if hub is reset or user removed. # We are going to fail the config entry setup and initiate a new @@ -61,6 +71,8 @@ class HueBridge: LOGGER.exception("Unknown error connecting with Hue bridge at %s", host) return False + self.api = bridge + hass.async_create_task( hass.config_entries.async_forward_entry_setup(self.config_entry, "light") ) @@ -77,9 +89,19 @@ class HueBridge: DOMAIN, SERVICE_HUE_SCENE, self.hue_activate_scene, schema=SCENE_SCHEMA ) + self.parallel_updates_semaphore = asyncio.Semaphore( + 3 if self.api.config.modelid == "BSB001" else 10 + ) + self.authorized = True return True + async def async_request_call(self, coro): + """Process request batched.""" + + async with self.parallel_updates_semaphore: + return await coro + async def async_reset(self): """Reset this bridge to default state. @@ -163,21 +185,20 @@ class HueBridge: create_config_flow(self.hass, self.host) -async def get_bridge(hass, host, username=None): +async def authenticate_bridge(hass: core.HomeAssistant, bridge: aiohue.Bridge): """Create a bridge object and verify authentication.""" - bridge = aiohue.Bridge( - host, username=username, websession=aiohttp_client.async_get_clientsession(hass) - ) - try: with async_timeout.timeout(10): # Create username if we don't have one - if not username: - await bridge.create_user(f"home-assistant#{hass.config.location_name}") + if not bridge.username: + device_name = unicode_slug.slugify( + hass.config.location_name, max_length=19 + ) + await bridge.create_user(f"home-assistant#{device_name}") + # Initialize bridge (and validate our username) await bridge.initialize() - return bridge except (aiohue.LinkButtonNotPressed, aiohue.Unauthorized): raise AuthenticationRequired except (asyncio.TimeoutError, aiohue.RequestError): diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index ebd71ba7c1c..60000a68fb7 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -1,47 +1,23 @@ """Config flow to configure Philips Hue.""" import asyncio -import json -import os +from typing import Dict, Optional +from urllib.parse import urlparse -from aiohue.discovery import discover_nupnp +import aiohue +from aiohue.discovery import discover_nupnp, normalize_bridge_id import async_timeout import voluptuous as vol -from homeassistant import config_entries -from homeassistant.core import callback +from homeassistant import config_entries, core +from homeassistant.components import ssdp from homeassistant.helpers import aiohttp_client -from .bridge import get_bridge -from .const import DOMAIN, LOGGER +from .bridge import authenticate_bridge +from .const import DOMAIN, LOGGER # pylint: disable=unused-import from .errors import AuthenticationRequired, CannotConnect HUE_MANUFACTURERURL = "http://www.philips.com" - - -@callback -def configured_hosts(hass): - """Return a set of the configured hosts.""" - return set( - entry.data["host"] for entry in hass.config_entries.async_entries(DOMAIN) - ) - - -def _find_username_from_config(hass, filename): - """Load username from config. - - This was a legacy way of configuring Hue until Home Assistant 0.67. - """ - path = hass.config.path(filename) - - if not os.path.isfile(path): - return None - - with open(path) as inp: - try: - return list(json.load(inp).values())[0]["username"] - except ValueError: - # If we get invalid JSON - return None +HUE_IGNORED_BRIDGE_NAMES = ["HASS Bridge", "Espalexa"] class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @@ -54,23 +30,45 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize the Hue flow.""" - self.host = None + self.bridge: Optional[aiohue.Bridge] = None + self.discovered_bridges: Optional[Dict[str, aiohue.Bridge]] = None async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" + # This is for backwards compatibility. return await self.async_step_init(user_input) + @core.callback + def _async_get_bridge(self, host: str, bridge_id: Optional[str] = None): + """Return a bridge object.""" + if bridge_id is not None: + bridge_id = normalize_bridge_id(bridge_id) + + return aiohue.Bridge( + host, + websession=aiohttp_client.async_get_clientsession(self.hass), + bridge_id=bridge_id, + ) + async def async_step_init(self, user_input=None): """Handle a flow start.""" - if user_input is not None: - self.host = self.context["host"] = user_input["host"] - return await self.async_step_link() - - websession = aiohttp_client.async_get_clientsession(self.hass) + if ( + user_input is not None + and self.discovered_bridges is not None + # pylint: disable=unsupported-membership-test + and user_input["id"] in self.discovered_bridges + ): + # pylint: disable=unsubscriptable-object + self.bridge = self.discovered_bridges[user_input["id"]] + await self.async_set_unique_id(self.bridge.id, raise_on_progress=False) + # We pass user input to link so it will attempt to link right away + return await self.async_step_link({}) try: with async_timeout.timeout(5): - bridges = await discover_nupnp(websession=websession) + bridges = await discover_nupnp( + websession=aiohttp_client.async_get_clientsession(self.hass) + ) except asyncio.TimeoutError: return self.async_abort(reason="discover_timeout") @@ -78,20 +76,28 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="no_bridges") # Find already configured hosts - configured = configured_hosts(self.hass) + already_configured = self._async_current_ids(False) + bridges = [bridge for bridge in bridges if bridge.id not in already_configured] - hosts = [bridge.host for bridge in bridges if bridge.host not in configured] - - if not hosts: + if not bridges: return self.async_abort(reason="all_configured") - if len(hosts) == 1: - self.host = hosts[0] + if len(bridges) == 1: + self.bridge = bridges[0] + await self.async_set_unique_id(self.bridge.id, raise_on_progress=False) return await self.async_step_link() + self.discovered_bridges = {bridge.id: bridge for bridge in bridges} + return self.async_show_form( step_id="init", - data_schema=vol.Schema({vol.Required("host"): vol.In(hosts)}), + data_schema=vol.Schema( + { + vol.Required("id"): vol.In( + {bridge.id: bridge.host for bridge in bridges} + ) + } + ), ) async def async_step_link(self, user_input=None): @@ -100,31 +106,39 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): Given a configured host, will ask the user to press the link button to connect to the bridge. """ + if user_input is None: + return self.async_show_form(step_id="link") + + bridge = self.bridge + assert bridge is not None errors = {} - # We will always try linking in case the user has already pressed - # the link button. try: - bridge = await get_bridge(self.hass, self.host, username=None) + await authenticate_bridge(self.hass, bridge) - return await self._entry_from_bridge(bridge) + # Can happen if we come from import. + if self.unique_id is None: + await self.async_set_unique_id( + normalize_bridge_id(bridge.id), raise_on_progress=False + ) + + return self.async_create_entry( + title=bridge.config.name, + data={"host": bridge.host, "username": bridge.username}, + ) except AuthenticationRequired: errors["base"] = "register_failed" except CannotConnect: - LOGGER.error("Error connecting to the Hue bridge at %s", self.host) + LOGGER.error("Error connecting to the Hue bridge at %s", bridge.host) errors["base"] = "linking" except Exception: # pylint: disable=broad-except LOGGER.exception( - "Unknown error connecting with Hue bridge at %s", self.host + "Unknown error connecting with Hue bridge at %s", bridge.host ) errors["base"] = "linking" - # If there was no user input, do not show the errors. - if user_input is None: - errors = {} - return self.async_show_form(step_id="link", errors=errors) async def async_step_ssdp(self, discovery_info): @@ -133,120 +147,58 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): This flow is triggered by the SSDP component. It will check if the host is already configured and delegate to the import step if not. """ - from homeassistant.components.ssdp import ATTR_MANUFACTURERURL - - if discovery_info[ATTR_MANUFACTURERURL] != HUE_MANUFACTURERURL: + # Filter out non-Hue bridges #1 + if discovery_info[ssdp.ATTR_UPNP_MANUFACTURER_URL] != HUE_MANUFACTURERURL: return self.async_abort(reason="not_hue_bridge") - # Filter out emulated Hue - if "HASS Bridge" in discovery_info.get("name", ""): - return self.async_abort(reason="already_configured") - - host = self.context["host"] = discovery_info.get("host") - + # Filter out non-Hue bridges #2 if any( - host == flow["context"].get("host") for flow in self._async_in_progress() + name in discovery_info.get(ssdp.ATTR_UPNP_FRIENDLY_NAME, "") + for name in HUE_IGNORED_BRIDGE_NAMES ): - return self.async_abort(reason="already_in_progress") + return self.async_abort(reason="not_hue_bridge") - if host in configured_hosts(self.hass): - return self.async_abort(reason="already_configured") + if ( + ssdp.ATTR_SSDP_LOCATION not in discovery_info + or ssdp.ATTR_UPNP_SERIAL not in discovery_info + ): + return self.async_abort(reason="not_hue_bridge") - # This value is based off host/description.xml and is, weirdly, missing - # 4 characters in the middle of the serial compared to results returned - # from the NUPNP API or when querying the bridge API for bridgeid. - # (on first gen Hue hub) - serial = discovery_info.get("serial") + host = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]).hostname - return await self.async_step_import( - { - "host": host, - # This format is the legacy format that Hue used for discovery - "path": f"phue-{serial}.conf", - } - ) + bridge = self._async_get_bridge(host, discovery_info[ssdp.ATTR_UPNP_SERIAL]) + + await self.async_set_unique_id(bridge.id) + self._abort_if_unique_id_configured() + self.bridge = bridge + return await self.async_step_link() async def async_step_homekit(self, homekit_info): """Handle HomeKit discovery.""" - host = self.context["host"] = homekit_info.get("host") + bridge = self._async_get_bridge( + homekit_info["host"], homekit_info["properties"]["id"] + ) - if any( - host == flow["context"].get("host") for flow in self._async_in_progress() - ): - return self.async_abort(reason="already_in_progress") - - if host in configured_hosts(self.hass): - return self.async_abort(reason="already_configured") - - return await self.async_step_import({"host": host}) + await self.async_set_unique_id(bridge.id) + self._abort_if_unique_id_configured() + self.bridge = bridge + return await self.async_step_link() async def async_step_import(self, import_info): """Import a new bridge as a config entry. - Will read authentication from Phue config file if available. - This flow is triggered by `async_setup` for both configured and discovered bridges. Triggered for any bridge that does not have a config entry yet (based on host). This flow is also triggered by `async_step_discovery`. - - If an existing config file is found, we will validate the credentials - and create an entry. Otherwise we will delegate to `link` step which - will ask user to link the bridge. """ - host = self.context["host"] = import_info["host"] - path = import_info.get("path") + # Check if host exists, abort if so. + if any( + import_info["host"] == entry.data["host"] + for entry in self._async_current_entries() + ): + return self.async_abort(reason="already_configured") - if path is not None: - username = await self.hass.async_add_job( - _find_username_from_config, self.hass, self.hass.config.path(path) - ) - else: - username = None - - try: - bridge = await get_bridge(self.hass, host, username) - - LOGGER.info("Imported authentication for %s from %s", host, path) - - return await self._entry_from_bridge(bridge) - except AuthenticationRequired: - self.host = host - - LOGGER.info("Invalid authentication for %s, requesting link.", host) - - return await self.async_step_link() - - except CannotConnect: - LOGGER.error("Error connecting to the Hue bridge at %s", host) - return self.async_abort(reason="cannot_connect") - - except Exception: # pylint: disable=broad-except - LOGGER.exception("Unknown error connecting with Hue bridge at %s", host) - return self.async_abort(reason="unknown") - - async def _entry_from_bridge(self, bridge): - """Return a config entry from an initialized bridge.""" - # Remove all other entries of hubs with same ID or host - host = bridge.host - bridge_id = bridge.config.bridgeid - - same_hub_entries = [ - entry.entry_id - for entry in self.hass.config_entries.async_entries(DOMAIN) - if entry.data["bridge_id"] == bridge_id or entry.data["host"] == host - ] - - if same_hub_entries: - await asyncio.wait( - [ - self.hass.config_entries.async_remove(entry_id) - for entry_id in same_hub_entries - ] - ) - - return self.async_create_entry( - title=bridge.config.name, - data={"host": host, "bridge_id": bridge_id, "username": bridge.username}, - ) + self.bridge = self._async_get_bridge(import_info["host"]) + return await self.async_step_link() diff --git a/homeassistant/components/hue/helpers.py b/homeassistant/components/hue/helpers.py index af0f996b537..8a5fa973e4f 100644 --- a/homeassistant/components/hue/helpers.py +++ b/homeassistant/components/hue/helpers.py @@ -1,7 +1,7 @@ """Helper functions for Philips Hue.""" +from homeassistant import config_entries from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg from homeassistant.helpers.entity_registry import async_get_registry as get_ent_reg -from homeassistant import config_entries from .const import DOMAIN diff --git a/homeassistant/components/hue/light.py b/homeassistant/components/hue/light.py index d58e4608b65..d81bbd4c438 100644 --- a/homeassistant/components/hue/light.py +++ b/homeassistant/components/hue/light.py @@ -202,7 +202,7 @@ async def async_update_items( try: start = monotonic() with async_timeout.timeout(4): - await api.update() + await bridge.async_request_call(api.update()) except aiohue.Unauthorized: await bridge.handle_unauthorized_error() return @@ -277,7 +277,7 @@ class HueLight(Light): _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." + 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 @@ -434,9 +434,9 @@ class HueLight(Light): command["effect"] = "none" if self.is_group: - await self.light.set_action(**command) + await self.bridge.async_request_call(self.light.set_action(**command)) else: - await self.light.set_state(**command) + await self.bridge.async_request_call(self.light.set_state(**command)) async def async_turn_off(self, **kwargs): """Turn the specified or all lights off.""" @@ -457,9 +457,9 @@ class HueLight(Light): command["alert"] = "none" if self.is_group: - await self.light.set_action(**command) + await self.bridge.async_request_call(self.light.set_action(**command)) else: - await self.light.set_state(**command) + await self.bridge.async_request_call(self.light.set_state(**command)) async def async_update(self): """Synchronize state with bridge.""" diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index c90b6181559..75384e012e0 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -3,21 +3,15 @@ "name": "Philips Hue", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hue", - "requirements": [ - "aiohue==1.9.2" - ], + "requirements": ["aiohue==1.10.1"], "ssdp": [ { "manufacturer": "Royal Philips Electronics" } ], "homekit": { - "models": [ - "BSB002" - ] + "models": ["BSB002"] }, "dependencies": [], - "codeowners": [ - "@balloob" - ] + "codeowners": ["@balloob"] } diff --git a/homeassistant/components/hue/sensor_base.py b/homeassistant/components/hue/sensor_base.py index bf64fed0c95..f7882b102c0 100644 --- a/homeassistant/components/hue/sensor_base.py +++ b/homeassistant/components/hue/sensor_base.py @@ -101,7 +101,7 @@ class SensorManager: try: start = monotonic() with async_timeout.timeout(4): - await api.update() + await self.bridge.async_request_call(api.update()) except Unauthorized: await self.bridge.handle_unauthorized_error() return diff --git a/homeassistant/components/hunterdouglas_powerview/scene.py b/homeassistant/components/hunterdouglas_powerview/scene.py index d55a970f1e4..3f2ac79306c 100644 --- a/homeassistant/components/hunterdouglas_powerview/scene.py +++ b/homeassistant/components/hunterdouglas_powerview/scene.py @@ -1,12 +1,16 @@ """Support for Powerview scenes from a Powerview hub.""" import logging +from aiopvapi.helpers.aiorequest import AioRequest +from aiopvapi.resources.scene import Scene as PvScene +from aiopvapi.rooms import Rooms +from aiopvapi.scenes import Scenes import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.components.scene import Scene, DOMAIN +from homeassistant.components.scene import DOMAIN, Scene from homeassistant.const import CONF_PLATFORM from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id _LOGGER = logging.getLogger(__name__) @@ -33,11 +37,6 @@ STATE_ATTRIBUTE_ROOM_NAME = "roomName" async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up home assistant scene entries.""" - # from aiopvapi.hub import Hub - from aiopvapi.helpers.aiorequest import AioRequest - from aiopvapi.scenes import Scenes - from aiopvapi.rooms import Rooms - from aiopvapi.resources.scene import Scene as PvScene hub_address = config.get(HUB_ADDRESS) websession = async_get_clientsession(hass) diff --git a/homeassistant/components/hydrawise/__init__.py b/homeassistant/components/hydrawise/__init__.py index e111c157460..57ed29d9780 100644 --- a/homeassistant/components/hydrawise/__init__.py +++ b/homeassistant/components/hydrawise/__init__.py @@ -2,12 +2,13 @@ from datetime import timedelta import logging +from hydrawiser.core import Hydrawiser from requests.exceptions import ConnectTimeout, HTTPError import voluptuous as vol from homeassistant.const import ATTR_ATTRIBUTION, CONF_ACCESS_TOKEN, CONF_SCAN_INTERVAL -import homeassistant.helpers.config_validation as cv from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import track_time_interval @@ -73,8 +74,6 @@ def setup(hass, config): scan_interval = conf.get(CONF_SCAN_INTERVAL) try: - from hydrawiser.core import Hydrawiser - hydrawise = Hydrawiser(user_token=access_token) hass.data[DATA_HYDRAWISE] = HydrawiseHub(hydrawise) except (ConnectTimeout, HTTPError) as ex: diff --git a/homeassistant/components/ialarm/alarm_control_panel.py b/homeassistant/components/ialarm/alarm_control_panel.py index 845c6b9021f..24ab2bc7a80 100644 --- a/homeassistant/components/ialarm/alarm_control_panel.py +++ b/homeassistant/components/ialarm/alarm_control_panel.py @@ -2,10 +2,15 @@ import logging import re +from pyialarm import IAlarm import voluptuous as vol import homeassistant.components.alarm_control_panel as alarm from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, +) from homeassistant.const import ( CONF_CODE, CONF_HOST, @@ -62,7 +67,6 @@ class IAlarmPanel(alarm.AlarmControlPanel): def __init__(self, name, code, username, password, url): """Initialize the iAlarm status.""" - from pyialarm import IAlarm self._name = name self._code = str(code) if code else None @@ -91,6 +95,11 @@ class IAlarmPanel(alarm.AlarmControlPanel): """Return the state of the device.""" return self._state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY + def update(self): """Return the state of the device.""" status = self._client.get_status() diff --git a/homeassistant/components/iaqualink/.translations/lb.json b/homeassistant/components/iaqualink/.translations/lb.json index 4beb11214bc..db8d67eea75 100644 --- a/homeassistant/components/iaqualink/.translations/lb.json +++ b/homeassistant/components/iaqualink/.translations/lb.json @@ -12,7 +12,7 @@ "password": "Passwuert", "username": "Benotzernumm / E-Mail Adresse" }, - "description": "Gitt den Benotznumm an d'Passwuert fir \u00e4ren iAqualink Kont un.", + "description": "Gitt den Benotzernumm an d'Passwuert fir \u00e4ren iAqualink Kont un.", "title": "Mat iAqualink verbannen" } }, diff --git a/homeassistant/components/iaqualink/.translations/ru.json b/homeassistant/components/iaqualink/.translations/ru.json index 9a93c19ef20..8c8e30fe067 100644 --- a/homeassistant/components/iaqualink/.translations/ru.json +++ b/homeassistant/components/iaqualink/.translations/ru.json @@ -12,7 +12,7 @@ "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u041b\u043e\u0433\u0438\u043d / \u0410\u0434\u0440\u0435\u0441 \u044d\u043b. \u043f\u043e\u0447\u0442\u044b" }, - "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\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 iAqualink.", + "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.", "title": "Jandy iAqualink" } }, diff --git a/homeassistant/components/iaqualink/__init__.py b/homeassistant/components/iaqualink/__init__.py index fc1eb3b248a..16c8deac72e 100644 --- a/homeassistant/components/iaqualink/__init__.py +++ b/homeassistant/components/iaqualink/__init__.py @@ -5,8 +5,6 @@ import logging from typing import Any, Dict import aiohttp.client_exceptions -import voluptuous as vol - from iaqualink import ( AqualinkBinarySensor, AqualinkClient, @@ -17,6 +15,7 @@ from iaqualink import ( AqualinkThermostat, AqualinkToggle, ) +import voluptuous as vol from homeassistant import config_entries from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN @@ -29,18 +28,17 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback 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_connect, async_dispatcher_send, ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType, HomeAssistantType from .const import DOMAIN, UPDATE_INTERVAL - _LOGGER = logging.getLogger(__name__) ATTR_CONFIG = "config" diff --git a/homeassistant/components/iaqualink/binary_sensor.py b/homeassistant/components/iaqualink/binary_sensor.py index 09c9322a587..30d419c1bce 100644 --- a/homeassistant/components/iaqualink/binary_sensor.py +++ b/homeassistant/components/iaqualink/binary_sensor.py @@ -2,9 +2,9 @@ import logging from homeassistant.components.binary_sensor import ( - BinarySensorDevice, DEVICE_CLASS_COLD, DOMAIN, + BinarySensorDevice, ) from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType diff --git a/homeassistant/components/iaqualink/climate.py b/homeassistant/components/iaqualink/climate.py index f41d17837c2..36f3303774a 100644 --- a/homeassistant/components/iaqualink/climate.py +++ b/homeassistant/components/iaqualink/climate.py @@ -22,7 +22,7 @@ from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.helpers.typing import HomeAssistantType from . import AqualinkEntity, refresh_system -from .const import DOMAIN as AQUALINK_DOMAIN, CLIMATE_SUPPORTED_MODES +from .const import CLIMATE_SUPPORTED_MODES, DOMAIN as AQUALINK_DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/iaqualink/config_flow.py b/homeassistant/components/iaqualink/config_flow.py index ec83477d253..d577fe448aa 100644 --- a/homeassistant/components/iaqualink/config_flow.py +++ b/homeassistant/components/iaqualink/config_flow.py @@ -1,9 +1,8 @@ """Config flow to configure zone component.""" from typing import Optional -import voluptuous as vol - from iaqualink import AqualinkClient, AqualinkLoginException +import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME diff --git a/homeassistant/components/icloud/.translations/ca.json b/homeassistant/components/icloud/.translations/ca.json new file mode 100644 index 00000000000..30e6c50b81b --- /dev/null +++ b/homeassistant/components/icloud/.translations/ca.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "username_exists": "El compte ja ha estat configurat" + }, + "error": { + "login": "Error d\u2019inici de sessi\u00f3: comprova el correu electr\u00f2nic i la contrasenya", + "send_verification_code": "No s'ha pogut enviar el codi de verificaci\u00f3", + "username_exists": "El compte ja ha estat configurat", + "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" + }, + "step": { + "trusted_device": { + "data": { + "trusted_device": "Dispositiu de confian\u00e7a" + }, + "description": "Selecciona el teu dispositiu de confian\u00e7a", + "title": "Dispositiu de confian\u00e7a iCloud" + }, + "user": { + "data": { + "password": "Contrasenya", + "username": "Correu electr\u00f2nic" + }, + "description": "Introdueix les teves credencials", + "title": "credencials d'iCloud" + }, + "verification_code": { + "data": { + "verification_code": "Codi de verificaci\u00f3" + }, + "description": "Introdueix el codi de verificaci\u00f3 que rebis d'iCloud", + "title": "Codi de verificaci\u00f3 iCloud" + } + }, + "title": "Apple iCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/.translations/da.json b/homeassistant/components/icloud/.translations/da.json new file mode 100644 index 00000000000..1a06bd8e0f2 --- /dev/null +++ b/homeassistant/components/icloud/.translations/da.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "username_exists": "Kontoen er allerede konfigureret" + }, + "error": { + "login": "Loginfejl: Kontroller din email og adgangskode", + "send_verification_code": "Bekr\u00e6ftelseskoden kunne ikke sendes", + "username_exists": "Kontoen er allerede konfigureret", + "validate_verification_code": "Bekr\u00e6ftelseskoden kunne ikke bekr\u00e6ftes, V\u00e6lg en betroet enhed, og start bekr\u00e6ftelsen igen" + }, + "step": { + "trusted_device": { + "data": { + "trusted_device": "Betroet enhed" + }, + "description": "V\u00e6lg din betroede enhed", + "title": "iCloud-enhed, der er tillid til" + }, + "user": { + "data": { + "password": "Adgangskode", + "username": "Email" + }, + "description": "Indtast dine legitimationsoplysninger", + "title": "iCloud-legitimationsoplysninger" + }, + "verification_code": { + "data": { + "verification_code": "Bekr\u00e6ftelseskode" + }, + "description": "Indtast venligst den bekr\u00e6ftelseskode, du lige har modtaget fra iCloud", + "title": "iCloud-bekr\u00e6ftelseskode" + } + }, + "title": "Apple iCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/.translations/en.json b/homeassistant/components/icloud/.translations/en.json new file mode 100644 index 00000000000..58101759356 --- /dev/null +++ b/homeassistant/components/icloud/.translations/en.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "username_exists": "Account already configured" + }, + "error": { + "login": "Login error: please check your email & password", + "send_verification_code": "Failed to send verification code", + "username_exists": "Account already configured", + "validate_verification_code": "Failed to verify your verification code, choose a trust device and start the verification again" + }, + "step": { + "trusted_device": { + "data": { + "trusted_device": "Trusted device" + }, + "description": "Select your trusted device", + "title": "iCloud trusted device" + }, + "user": { + "data": { + "password": "Password", + "username": "Email" + }, + "description": "Enter your credentials", + "title": "iCloud credentials" + }, + "verification_code": { + "data": { + "verification_code": "Verification code" + }, + "description": "Please enter the verification code you just received from iCloud", + "title": "iCloud verification code" + } + }, + "title": "Apple iCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/.translations/es.json b/homeassistant/components/icloud/.translations/es.json new file mode 100644 index 00000000000..13355fa2b8e --- /dev/null +++ b/homeassistant/components/icloud/.translations/es.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "username_exists": "Cuenta ya configurada" + }, + "error": { + "login": "Error de inicio de sesi\u00f3n: compruebe su direcci\u00f3n de correo electr\u00f3nico y contrase\u00f1a", + "send_verification_code": "Error al enviar el c\u00f3digo de verificaci\u00f3n", + "username_exists": "Cuenta ya configurada", + "validate_verification_code": "No se pudo verificar el c\u00f3digo de verificaci\u00f3n, elegir un dispositivo de confianza e iniciar la verificaci\u00f3n de nuevo" + }, + "step": { + "trusted_device": { + "data": { + "trusted_device": "Dispositivo de confianza" + }, + "description": "Seleccione su dispositivo de confianza", + "title": "Dispositivo de confianza iCloud" + }, + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Correo electr\u00f3nico" + }, + "description": "Ingrese sus credenciales", + "title": "Credenciales iCloud" + }, + "verification_code": { + "data": { + "verification_code": "C\u00f3digo de verificaci\u00f3n" + }, + "description": "Por favor, introduzca el c\u00f3digo de verificaci\u00f3n que acaba de recibir de iCloud", + "title": "C\u00f3digo de verificaci\u00f3n de iCloud" + } + }, + "title": "iCloud de Apple" + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/.translations/fr.json b/homeassistant/components/icloud/.translations/fr.json new file mode 100644 index 00000000000..81996d908a6 --- /dev/null +++ b/homeassistant/components/icloud/.translations/fr.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "username_exists": "Compte d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "login": "Erreur de connexion: veuillez v\u00e9rifier votre e-mail et votre mot de passe", + "send_verification_code": "\u00c9chec de l'envoi du code de v\u00e9rification", + "username_exists": "Compte d\u00e9j\u00e0 configur\u00e9", + "validate_verification_code": "Impossible de v\u00e9rifier votre code de v\u00e9rification, choisissez un appareil de confiance et recommencez la v\u00e9rification" + }, + "step": { + "trusted_device": { + "data": { + "trusted_device": "Appareil de confiance" + }, + "description": "S\u00e9lectionnez votre appareil de confiance", + "title": "Appareil de confiance iCloud" + }, + "user": { + "data": { + "password": "Mot de passe", + "username": "Email" + }, + "description": "Entrez vos identifiants", + "title": "Identifiants iCloud" + }, + "verification_code": { + "data": { + "verification_code": "Code de v\u00e9rification" + }, + "description": "Veuillez entrer le code de v\u00e9rification que vous venez de recevoir d'iCloud", + "title": "Code de v\u00e9rification iCloud" + } + }, + "title": "Apple iCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/.translations/it.json b/homeassistant/components/icloud/.translations/it.json new file mode 100644 index 00000000000..0a986f1fe77 --- /dev/null +++ b/homeassistant/components/icloud/.translations/it.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "username_exists": "Account gi\u00e0 configurato" + }, + "error": { + "login": "Errore di accesso: si prega di controllare la tua e-mail e la password", + "send_verification_code": "Impossibile inviare il codice di verifica", + "username_exists": "Account gi\u00e0 configurato", + "validate_verification_code": "Impossibile verificare il codice di verifica, scegliere un dispositivo attendibile e riavviare la verifica" + }, + "step": { + "trusted_device": { + "data": { + "trusted_device": "Dispositivo attendibile" + }, + "description": "Selezionare il dispositivo attendibile", + "title": "Dispositivo attendibile iCloud" + }, + "user": { + "data": { + "password": "Password", + "username": "E-mail" + }, + "description": "Inserisci le tue credenziali", + "title": "Credenziali iCloud" + }, + "verification_code": { + "data": { + "verification_code": "Codice di verifica" + }, + "description": "Inserisci il codice di verifica che hai appena ricevuto da iCloud", + "title": "Codice di verifica iCloud" + } + }, + "title": "Apple iCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/.translations/ja.json b/homeassistant/components/icloud/.translations/ja.json new file mode 100644 index 00000000000..f5c3be53639 --- /dev/null +++ b/homeassistant/components/icloud/.translations/ja.json @@ -0,0 +1,27 @@ +{ + "config": { + "step": { + "trusted_device": { + "data": { + "trusted_device": "\u4fe1\u983c\u3067\u304d\u308b\u30c7\u30d0\u30a4\u30b9" + }, + "description": "\u4fe1\u983c\u3067\u304d\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044", + "title": "iCloud \u306e\u4fe1\u983c\u3067\u304d\u308b\u30c7\u30d0\u30a4\u30b9" + }, + "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "E\u30e1\u30fc\u30eb" + }, + "description": "\u8cc7\u683c\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044", + "title": "iCloud \u306e\u8cc7\u683c\u60c5\u5831" + }, + "verification_code": { + "data": { + "verification_code": "\u8a8d\u8a3c\u30b3\u30fc\u30c9" + }, + "title": "iCloud \u306e\u8a8d\u8a3c\u30b3\u30fc\u30c9" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/.translations/ko.json b/homeassistant/components/icloud/.translations/ko.json new file mode 100644 index 00000000000..a689a895278 --- /dev/null +++ b/homeassistant/components/icloud/.translations/ko.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "username_exists": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, + "error": { + "login": "\ub85c\uadf8\uc778 \uc624\ub958: \uc774\uba54\uc77c \ubc0f \ube44\ubc00\ubc88\ud638\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694", + "send_verification_code": "\uc778\uc99d \ucf54\ub4dc\ub97c \ubcf4\ub0b4\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "username_exists": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\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" + }, + "step": { + "trusted_device": { + "data": { + "trusted_device": "\uc2e0\ub8b0\ud560 \uc218 \uc788\ub294 \uae30\uae30" + }, + "description": "\uc2e0\ub8b0\ud560 \uc218 \uc788\ub294 \uae30\uae30\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694", + "title": "iCloud \uac00 \uc2e0\ub8b0\ud560 \uc218 \uc788\ub294 \uae30\uae30" + }, + "user": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc774\uba54\uc77c" + }, + "description": "\uc790\uaca9 \uc99d\uba85\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694", + "title": "iCloud \uc790\uaca9 \uc99d\uba85" + }, + "verification_code": { + "data": { + "verification_code": "\uc778\uc99d \ucf54\ub4dc" + }, + "description": "iCloud \uc5d0\uc11c \ubc1b\uc740 \uc778\uc99d \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694", + "title": "iCloud \uc778\uc99d \ucf54\ub4dc" + } + }, + "title": "Apple iCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/.translations/lb.json b/homeassistant/components/icloud/.translations/lb.json new file mode 100644 index 00000000000..eaeb300f7a8 --- /dev/null +++ b/homeassistant/components/icloud/.translations/lb.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "username_exists": "Kont ass scho konfigur\u00e9iert" + }, + "error": { + "login": "Feeler beim Login: iwwerpr\u00e9ift \u00e4r E-Mail & Passwuert", + "send_verification_code": "Feeler beim sch\u00e9cken vum Verifikatiouns Code", + "username_exists": "Kont ass scho konfigur\u00e9iert", + "validate_verification_code": "Feeler beim iwwerpr\u00e9iwe vum Verifikatiouns Code, wielt ee vertrauten Apparat aus a start d'Iwwerpr\u00e9iwung nei" + }, + "step": { + "trusted_device": { + "data": { + "trusted_device": "Vertrauten Apparat" + }, + "description": "Wielt \u00e4ren vertrauten Apparat aus", + "title": "iCloud vertrauten Apparat" + }, + "user": { + "data": { + "password": "Passwuert", + "username": "E-Mail" + }, + "description": "F\u00ebllt \u00e4r Umeldungs Informatiounen aus", + "title": "iCloud Umeldungs Informatiounen" + }, + "verification_code": { + "data": { + "verification_code": "Verifikatiouns Code" + }, + "description": "Gitt de Verifikatiouns Code an deen dir elo grad vun iCloud kritt hutt", + "title": "iCloud Verifikatiouns Code" + } + }, + "title": "Apple iCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/.translations/nl.json b/homeassistant/components/icloud/.translations/nl.json new file mode 100644 index 00000000000..d35496b171b --- /dev/null +++ b/homeassistant/components/icloud/.translations/nl.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "username_exists": "Account reeds geconfigureerd" + }, + "error": { + "login": "Aanmeldingsfout: controleer uw e-mailadres en wachtwoord", + "send_verification_code": "Kan verificatiecode niet verzenden", + "username_exists": "Account reeds geconfigureerd", + "validate_verification_code": "Kan uw verificatiecode niet verifi\u00ebren, kies een vertrouwensapparaat en start de verificatie opnieuw" + }, + "step": { + "trusted_device": { + "data": { + "trusted_device": "Vertrouwd apparaat" + }, + "description": "Selecteer uw vertrouwde apparaat", + "title": "iCloud vertrouwd apparaat" + }, + "user": { + "data": { + "password": "Wachtwoord", + "username": "E-mail" + }, + "description": "Voer uw gegevens in", + "title": "iCloud inloggegevens" + }, + "verification_code": { + "data": { + "verification_code": "Verificatiecode" + }, + "description": "Voer de verificatiecode in die u zojuist van iCloud hebt ontvangen", + "title": "iCloud verificatiecode" + } + }, + "title": "Apple iCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/.translations/no.json b/homeassistant/components/icloud/.translations/no.json new file mode 100644 index 00000000000..a582b916310 --- /dev/null +++ b/homeassistant/components/icloud/.translations/no.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "username_exists": "Kontoen er allerede konfigurert" + }, + "error": { + "login": "Innloggingsfeil: vennligst sjekk e-postadressen og passordet ditt", + "send_verification_code": "Kunne ikke sende bekreftelseskode", + "username_exists": "Kontoen er allerede konfigurert", + "validate_verification_code": "Kunne ikke bekrefte bekreftelseskoden din, velg en tillitsenhet og start bekreftelsen p\u00e5 nytt" + }, + "step": { + "trusted_device": { + "data": { + "trusted_device": "P\u00e5litelig enhet" + }, + "description": "Velg den p\u00e5litelige enheten din", + "title": "iCloud p\u00e5litelig enhet" + }, + "user": { + "data": { + "password": "Passord", + "username": "E-post" + }, + "description": "Angi legitimasjonsbeskrivelsen", + "title": "iCloud-legitimasjon" + }, + "verification_code": { + "data": { + "verification_code": "iCloud-bekreftelseskode" + }, + "description": "Vennligst skriv inn bekreftelseskoden du nettopp har f\u00e5tt fra iCloud", + "title": "iCloud-bekreftelseskode" + } + }, + "title": "Apple iCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/.translations/pl.json b/homeassistant/components/icloud/.translations/pl.json new file mode 100644 index 00000000000..f154f77f186 --- /dev/null +++ b/homeassistant/components/icloud/.translations/pl.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "username_exists": "Konto jest ju\u017c skonfigurowane" + }, + "error": { + "login": "B\u0142\u0105d logowania: sprawd\u017a adres e-mail i has\u0142o", + "send_verification_code": "Nie uda\u0142o si\u0119 wys\u0142a\u0107 kodu weryfikacyjnego", + "username_exists": "Konto jest ju\u017c skonfigurowane", + "validate_verification_code": "Nie uda\u0142o si\u0119 zweryfikowa\u0107 kodu weryfikacyjnego, wybierz urz\u0105dzenie zaufane i ponownie rozpocznij weryfikacj\u0119" + }, + "step": { + "trusted_device": { + "data": { + "trusted_device": "Zaufane urz\u0105dzenie" + }, + "description": "Wybierz zaufane urz\u0105dzenie", + "title": "Zaufane urz\u0105dzenie iCloud" + }, + "user": { + "data": { + "password": "Has\u0142o", + "username": "E-mail" + }, + "description": "Wprowad\u017a dane uwierzytelniaj\u0105ce", + "title": "Dane uwierzytelniaj\u0105ce iCloud" + }, + "verification_code": { + "data": { + "verification_code": "Kod weryfikacyjny" + }, + "description": "Wprowad\u017a kod weryfikacyjny, kt\u00f3ry w\u0142a\u015bnie otrzyma\u0142e\u015b z iCloud", + "title": "Kod weryfikacyjny iCloud" + } + }, + "title": "Apple iCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/.translations/ru.json b/homeassistant/components/icloud/.translations/ru.json new file mode 100644 index 00000000000..000edd71e00 --- /dev/null +++ b/homeassistant/components/icloud/.translations/ru.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "username_exists": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430." + }, + "error": { + "login": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0432\u0445\u043e\u0434\u0430: \u043f\u0440\u043e\u0432\u0435\u0440\u044c\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.", + "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.", + "username_exists": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430.", + "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." + }, + "step": { + "trusted_device": { + "data": { + "trusted_device": "\u0414\u043e\u0432\u0435\u0440\u0435\u043d\u043d\u043e\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + }, + "description": "\u0412\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", + "title": "\u0414\u043e\u0432\u0435\u0440\u0435\u043d\u043d\u043e\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e iCloud" + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b" + }, + "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": "\u0423\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 iCloud" + }, + "verification_code": { + "data": { + "verification_code": "\u041a\u043e\u0434 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f, \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043d\u044b\u0439 \u043e\u0442 iCloud", + "title": "\u041a\u043e\u0434 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f iCloud" + } + }, + "title": "Apple iCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/.translations/sl.json b/homeassistant/components/icloud/.translations/sl.json new file mode 100644 index 00000000000..91cb4312cb3 --- /dev/null +++ b/homeassistant/components/icloud/.translations/sl.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "username_exists": "Ra\u010dun \u017ee nastavljen" + }, + "error": { + "login": "Napaka pri prijavi: preverite svoj e-po\u0161tni naslov in geslo", + "send_verification_code": "Kode za preverjanje ni bilo mogo\u010de poslati", + "username_exists": "Ra\u010dun \u017ee nastavljen", + "validate_verification_code": "Kode za preverjanje ni bilo mogo\u010de preveriti, izberi napravo za zaupanje in znova za\u017eeni preverjanje" + }, + "step": { + "trusted_device": { + "data": { + "trusted_device": "Zaupanja vredna naprava" + }, + "description": "Izberite svojo zaupanja vredno napravo", + "title": "iCloud zaupanja vredna naprava" + }, + "user": { + "data": { + "password": "Geslo", + "username": "E-po\u0161tni naslov" + }, + "description": "Vnesite svoje poverilnice", + "title": "iCloud poverilnice" + }, + "verification_code": { + "data": { + "verification_code": "Koda za preverjanje" + }, + "description": "Prosimo, vnesite kodo za preverjanje, ki ste jo pravkar prejeli od iCloud", + "title": "iCloud koda za preverjanje" + } + }, + "title": "Apple 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 new file mode 100644 index 00000000000..80d8ba1485b --- /dev/null +++ b/homeassistant/components/icloud/.translations/zh-Hant.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "username_exists": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "login": "\u767b\u5165\u932f\u8aa4\uff1a\u8acb\u78ba\u8a8d\u96fb\u5b50\u90f5\u4ef6\u8207\u79d8\u5bc6\u6b63\u78ba\u6027", + "send_verification_code": "\u50b3\u9001\u9a57\u8b49\u78bc\u5931\u6557", + "username_exists": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "validate_verification_code": "\u7121\u6cd5\u9a57\u8b49\u8f38\u5165\u9a57\u8b49\u78bc\uff0c\u9078\u64c7\u4e00\u90e8\u4fe1\u4efb\u8a2d\u5099\u3001\u7136\u5f8c\u91cd\u65b0\u57f7\u884c\u9a57\u8b49\u3002" + }, + "step": { + "trusted_device": { + "data": { + "trusted_device": "\u4fe1\u4efb\u8a2d\u5099" + }, + "description": "\u9078\u64c7\u4fe1\u4efb\u8a2d\u5099", + "title": "iCloud \u4fe1\u4efb\u8a2d\u5099" + }, + "user": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u96fb\u5b50\u90f5\u4ef6" + }, + "description": "\u8f38\u5165\u6191\u8b49", + "title": "iCloud \u6191\u8b49" + }, + "verification_code": { + "data": { + "verification_code": "\u9a57\u8b49\u78bc" + }, + "description": "\u8acb\u8f38\u5165\u6240\u6536\u5230\u7684 iCloud \u9a57\u8b49\u78bc", + "title": "iCloud \u9a57\u8b49\u78bc" + } + }, + "title": "Apple iCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/__init__.py b/homeassistant/components/icloud/__init__.py index 1169104c99d..c59f4098951 100644 --- a/homeassistant/components/icloud/__init__.py +++ b/homeassistant/components/icloud/__init__.py @@ -1 +1,601 @@ -"""The icloud component.""" +"""The iCloud component.""" +from datetime import timedelta +import logging +import operator +from typing import Dict + +from pyicloud import PyiCloudService +from pyicloud.exceptions import PyiCloudFailedLoginException, PyiCloudNoDevicesException +from pyicloud.services.findmyiphone import AppleDevice +import voluptuous as vol + +from homeassistant.components.zone import async_active_zone +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import ATTR_ATTRIBUTION, CONF_PASSWORD, CONF_USERNAME +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.event import track_point_in_utc_time +from homeassistant.helpers.storage import Store +from homeassistant.helpers.typing import ConfigType, HomeAssistantType, ServiceDataType +from homeassistant.util import slugify +from homeassistant.util.async_ import run_callback_threadsafe +from homeassistant.util.dt import utcnow +from homeassistant.util.location import distance + +from .const import ( + CONF_ACCOUNT_NAME, + CONF_GPS_ACCURACY_THRESHOLD, + CONF_MAX_INTERVAL, + DEFAULT_GPS_ACCURACY_THRESHOLD, + DEFAULT_MAX_INTERVAL, + DEVICE_BATTERY_LEVEL, + DEVICE_BATTERY_STATUS, + DEVICE_CLASS, + DEVICE_DISPLAY_NAME, + DEVICE_ID, + DEVICE_LOCATION, + DEVICE_LOCATION_LATITUDE, + DEVICE_LOCATION_LONGITUDE, + DEVICE_LOST_MODE_CAPABLE, + DEVICE_LOW_POWER_MODE, + DEVICE_NAME, + DEVICE_PERSON_ID, + DEVICE_RAW_DEVICE_MODEL, + DEVICE_STATUS, + DEVICE_STATUS_CODES, + DEVICE_STATUS_SET, + DOMAIN, + ICLOUD_COMPONENTS, + STORAGE_KEY, + STORAGE_VERSION, + TRACKER_UPDATE, +) + +ATTRIBUTION = "Data provided by Apple iCloud" + +# entity attributes +ATTR_ACCOUNT_FETCH_INTERVAL = "account_fetch_interval" +ATTR_BATTERY = "battery" +ATTR_BATTERY_STATUS = "battery_status" +ATTR_DEVICE_NAME = "device_name" +ATTR_DEVICE_STATUS = "device_status" +ATTR_LOW_POWER_MODE = "low_power_mode" +ATTR_OWNER_NAME = "owner_fullname" + +# services +SERVICE_ICLOUD_PLAY_SOUND = "play_sound" +SERVICE_ICLOUD_DISPLAY_MESSAGE = "display_message" +SERVICE_ICLOUD_LOST_DEVICE = "lost_device" +SERVICE_ICLOUD_UPDATE = "update" +ATTR_ACCOUNT = "account" +ATTR_LOST_DEVICE_MESSAGE = "message" +ATTR_LOST_DEVICE_NUMBER = "number" +ATTR_LOST_DEVICE_SOUND = "sound" + +SERVICE_SCHEMA = vol.Schema({vol.Optional(ATTR_ACCOUNT): cv.string}) + +SERVICE_SCHEMA_PLAY_SOUND = vol.Schema( + {vol.Required(ATTR_ACCOUNT): cv.string, vol.Required(ATTR_DEVICE_NAME): cv.string} +) + +SERVICE_SCHEMA_DISPLAY_MESSAGE = vol.Schema( + { + vol.Required(ATTR_ACCOUNT): cv.string, + vol.Required(ATTR_DEVICE_NAME): cv.string, + vol.Required(ATTR_LOST_DEVICE_MESSAGE): cv.string, + vol.Optional(ATTR_LOST_DEVICE_SOUND): cv.boolean, + } +) + +SERVICE_SCHEMA_LOST_DEVICE = vol.Schema( + { + vol.Required(ATTR_ACCOUNT): cv.string, + vol.Required(ATTR_DEVICE_NAME): cv.string, + vol.Required(ATTR_LOST_DEVICE_NUMBER): cv.string, + vol.Required(ATTR_LOST_DEVICE_MESSAGE): cv.string, + } +) + +ACCOUNT_SCHEMA = vol.Schema( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_ACCOUNT_NAME): cv.string, + vol.Optional(CONF_MAX_INTERVAL, default=DEFAULT_MAX_INTERVAL): cv.positive_int, + vol.Optional( + CONF_GPS_ACCURACY_THRESHOLD, default=DEFAULT_GPS_ACCURACY_THRESHOLD + ): cv.positive_int, + } +) + +CONFIG_SCHEMA = vol.Schema( + {DOMAIN: vol.Schema(vol.All(cv.ensure_list, [ACCOUNT_SCHEMA]))}, + extra=vol.ALLOW_EXTRA, +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: + """Set up iCloud from legacy config file.""" + + conf = config.get(DOMAIN) + if conf is None: + return True + + for account_conf in conf: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=account_conf + ) + ) + + return True + + +async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: + """Set up an iCloud account from a config entry.""" + + hass.data.setdefault(DOMAIN, {}) + + username = entry.data[CONF_USERNAME] + password = entry.data[CONF_PASSWORD] + account_name = entry.data.get(CONF_ACCOUNT_NAME) + max_interval = entry.data[CONF_MAX_INTERVAL] + gps_accuracy_threshold = entry.data[CONF_GPS_ACCURACY_THRESHOLD] + + icloud_dir = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + + account = IcloudAccount( + hass, + username, + password, + icloud_dir, + account_name, + max_interval, + gps_accuracy_threshold, + ) + await hass.async_add_executor_job(account.setup) + hass.data[DOMAIN][username] = account + + for component in ICLOUD_COMPONENTS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + def play_sound(service: ServiceDataType) -> None: + """Play sound on the device.""" + account = service.data[ATTR_ACCOUNT] + device_name = service.data.get(ATTR_DEVICE_NAME) + device_name = slugify(device_name.replace(" ", "", 99)) + + for device in _get_account(account).get_devices_with_name(device_name): + device.play_sound() + + def display_message(service: ServiceDataType) -> None: + """Display a message on the device.""" + account = service.data[ATTR_ACCOUNT] + device_name = service.data.get(ATTR_DEVICE_NAME) + device_name = slugify(device_name.replace(" ", "", 99)) + message = service.data.get(ATTR_LOST_DEVICE_MESSAGE) + sound = service.data.get(ATTR_LOST_DEVICE_SOUND, False) + + for device in _get_account(account).get_devices_with_name(device_name): + device.display_message(message, sound) + + def lost_device(service: ServiceDataType) -> None: + """Make the device in lost state.""" + account = service.data[ATTR_ACCOUNT] + device_name = service.data.get(ATTR_DEVICE_NAME) + device_name = slugify(device_name.replace(" ", "", 99)) + number = service.data.get(ATTR_LOST_DEVICE_NUMBER) + message = service.data.get(ATTR_LOST_DEVICE_MESSAGE) + + for device in _get_account(account).get_devices_with_name(device_name): + device.lost_device(number, message) + + def update_account(service: ServiceDataType) -> None: + """Call the update function of an iCloud account.""" + account = service.data.get(ATTR_ACCOUNT) + + if account is None: + for account in hass.data[DOMAIN].values(): + account.keep_alive() + else: + _get_account(account).keep_alive() + + def _get_account(account_identifier: str) -> any: + if account_identifier is None: + return None + + icloud_account = hass.data[DOMAIN].get(account_identifier, None) + if icloud_account is None: + for account in hass.data[DOMAIN].values(): + if account.name == account_identifier: + icloud_account = account + + if icloud_account is None: + raise Exception( + "No iCloud account with username or name " + account_identifier + ) + return icloud_account + + hass.services.async_register( + DOMAIN, SERVICE_ICLOUD_PLAY_SOUND, play_sound, schema=SERVICE_SCHEMA_PLAY_SOUND + ) + + hass.services.async_register( + DOMAIN, + SERVICE_ICLOUD_DISPLAY_MESSAGE, + display_message, + schema=SERVICE_SCHEMA_DISPLAY_MESSAGE, + ) + + hass.services.async_register( + DOMAIN, + SERVICE_ICLOUD_LOST_DEVICE, + lost_device, + schema=SERVICE_SCHEMA_LOST_DEVICE, + ) + + hass.services.async_register( + DOMAIN, SERVICE_ICLOUD_UPDATE, update_account, schema=SERVICE_SCHEMA + ) + + return True + + +class IcloudAccount: + """Representation of an iCloud account.""" + + def __init__( + self, + hass: HomeAssistantType, + username: str, + password: str, + icloud_dir: Store, + account_name: str, + max_interval: int, + gps_accuracy_threshold: int, + ): + """Initialize an iCloud account.""" + self.hass = hass + self._username = username + self._password = password + self._name = account_name or slugify(username.partition("@")[0]) + self._fetch_interval = max_interval + self._max_interval = max_interval + self._gps_accuracy_threshold = gps_accuracy_threshold + + self._icloud_dir = icloud_dir + + self.api = None + self._owner_fullname = None + self._family_members_fullname = {} + self._devices = {} + + self.unsub_device_tracker = None + + def setup(self): + """Set up an iCloud account.""" + try: + self.api = PyiCloudService( + self._username, self._password, self._icloud_dir.path + ) + except PyiCloudFailedLoginException as error: + self.api = None + _LOGGER.error("Error logging into iCloud Service: %s", error) + return + + user_info = None + try: + # Gets device owners infos + user_info = self.api.devices.response["userInfo"] + except PyiCloudNoDevicesException: + _LOGGER.error("No iCloud Devices found") + + self._owner_fullname = f"{user_info['firstName']} {user_info['lastName']}" + + self._family_members_fullname = {} + for prs_id, member in user_info["membersInfo"].items(): + self._family_members_fullname[ + prs_id + ] = f"{member['firstName']} {member['lastName']}" + + self._devices = {} + self.update_devices() + + def update_devices(self) -> None: + """Update iCloud devices.""" + if self.api is None: + return + + api_devices = {} + try: + api_devices = self.api.devices + except PyiCloudNoDevicesException: + _LOGGER.error("No iCloud Devices found") + + # Gets devices infos + for device in api_devices: + status = device.status(DEVICE_STATUS_SET) + device_id = status[DEVICE_ID] + device_name = status[DEVICE_NAME] + + if self._devices.get(device_id, None) is not None: + # Seen device -> updating + _LOGGER.debug("Updating iCloud device: %s", device_name) + self._devices[device_id].update(status) + else: + # New device, should be unique + _LOGGER.debug( + "Adding iCloud device: %s [model: %s]", + device_name, + status[DEVICE_RAW_DEVICE_MODEL], + ) + self._devices[device_id] = IcloudDevice(self, device, status) + self._devices[device_id].update(status) + + dispatcher_send(self.hass, TRACKER_UPDATE) + self._fetch_interval = self._determine_interval() + track_point_in_utc_time( + self.hass, + self.keep_alive, + utcnow() + timedelta(minutes=self._fetch_interval), + ) + + def _determine_interval(self) -> int: + """Calculate new interval between two API fetch (in minutes).""" + intervals = {} + for device in self._devices.values(): + if device.location is None: + continue + + current_zone = run_callback_threadsafe( + self.hass.loop, + async_active_zone, + self.hass, + device.location[DEVICE_LOCATION_LATITUDE], + device.location[DEVICE_LOCATION_LONGITUDE], + ).result() + + if current_zone is not None: + intervals[device.name] = self._max_interval + continue + + zones = ( + self.hass.states.get(entity_id) + for entity_id in sorted(self.hass.states.entity_ids("zone")) + ) + + distances = [] + for zone_state in zones: + zone_state_lat = zone_state.attributes[DEVICE_LOCATION_LATITUDE] + zone_state_long = zone_state.attributes[DEVICE_LOCATION_LONGITUDE] + zone_distance = distance( + device.location[DEVICE_LOCATION_LATITUDE], + device.location[DEVICE_LOCATION_LONGITUDE], + zone_state_lat, + zone_state_long, + ) + distances.append(round(zone_distance / 1000, 1)) + + if not distances: + continue + mindistance = min(distances) + + # Calculate out how long it would take for the device to drive + # to the nearest zone at 120 km/h: + interval = round(mindistance / 2, 0) + + # Never poll more than once per minute + interval = max(interval, 1) + + if interval > 180: + # Three hour drive? + # This is far enough that they might be flying + interval = self._max_interval + + if ( + device.battery_level is not None + and device.battery_level <= 33 + and mindistance > 3 + ): + # Low battery - let's check half as often + interval = interval * 2 + + intervals[device.name] = interval + + return max( + int(min(intervals.items(), key=operator.itemgetter(1))[1]), + self._max_interval, + ) + + def keep_alive(self, now=None) -> None: + """Keep the API alive.""" + if self.api is None: + self.setup() + + if self.api is None: + return + + self.api.authenticate() + self.update_devices() + + def get_devices_with_name(self, name: str) -> [any]: + """Get devices by name.""" + result = [] + name_slug = slugify(name.replace(" ", "", 99)) + for device in self.devices.values(): + if slugify(device.name.replace(" ", "", 99)) == name_slug: + result.append(device) + if not result: + raise Exception("No device with name " + name) + return result + + @property + def name(self) -> str: + """Return the account name.""" + return self._name + + @property + def username(self) -> str: + """Return the account username.""" + return self._username + + @property + def owner_fullname(self) -> str: + """Return the account owner fullname.""" + return self._owner_fullname + + @property + def family_members_fullname(self) -> Dict[str, str]: + """Return the account family members fullname.""" + return self._family_members_fullname + + @property + def fetch_interval(self) -> int: + """Return the account fetch interval.""" + return self._fetch_interval + + @property + def devices(self) -> Dict[str, any]: + """Return the account devices.""" + return self._devices + + +class IcloudDevice: + """Representation of a iCloud device.""" + + def __init__(self, account: IcloudAccount, device: AppleDevice, status): + """Initialize the iCloud device.""" + self._account = account + account_name = account.name + + self._device = device + self._status = status + + self._name = self._status[DEVICE_NAME] + self._device_id = self._status[DEVICE_ID] + self._device_class = self._status[DEVICE_CLASS] + self._device_model = self._status[DEVICE_DISPLAY_NAME] + + if self._status[DEVICE_PERSON_ID]: + owner_fullname = account.family_members_fullname[ + self._status[DEVICE_PERSON_ID] + ] + else: + owner_fullname = account.owner_fullname + + self._battery_level = None + self._battery_status = None + self._location = None + + self._attrs = { + ATTR_ATTRIBUTION: ATTRIBUTION, + CONF_ACCOUNT_NAME: account_name, + ATTR_ACCOUNT_FETCH_INTERVAL: self._account.fetch_interval, + ATTR_DEVICE_NAME: self._device_model, + ATTR_DEVICE_STATUS: None, + ATTR_OWNER_NAME: owner_fullname, + } + + def update(self, status) -> None: + """Update the iCloud device.""" + self._status = status + + self._status[ATTR_ACCOUNT_FETCH_INTERVAL] = self._account.fetch_interval + + device_status = DEVICE_STATUS_CODES.get(self._status[DEVICE_STATUS], "error") + self._attrs[ATTR_DEVICE_STATUS] = device_status + + if self._status[DEVICE_BATTERY_STATUS] != "Unknown": + self._battery_level = int(self._status.get(DEVICE_BATTERY_LEVEL, 0) * 100) + self._battery_status = self._status[DEVICE_BATTERY_STATUS] + low_power_mode = self._status[DEVICE_LOW_POWER_MODE] + + self._attrs[ATTR_BATTERY] = self._battery_level + self._attrs[ATTR_BATTERY_STATUS] = self._battery_status + self._attrs[ATTR_LOW_POWER_MODE] = low_power_mode + + if ( + self._status[DEVICE_LOCATION] + and self._status[DEVICE_LOCATION][DEVICE_LOCATION_LATITUDE] + ): + location = self._status[DEVICE_LOCATION] + self._location = location + + def play_sound(self) -> None: + """Play sound on the device.""" + if self._account.api is None: + return + + self._account.api.authenticate() + _LOGGER.debug("Playing sound for %s", self.name) + self.device.play_sound() + + def display_message(self, message: str, sound: bool = False) -> None: + """Display a message on the device.""" + if self._account.api is None: + return + + self._account.api.authenticate() + _LOGGER.debug("Displaying message for %s", self.name) + self.device.display_message("Subject not working", message, sound) + + def lost_device(self, number: str, message: str) -> None: + """Make the device in lost state.""" + if self._account.api is None: + return + + self._account.api.authenticate() + if self._status[DEVICE_LOST_MODE_CAPABLE]: + _LOGGER.debug("Make device lost for %s", self.name) + self.device.lost_device(number, message, None) + else: + _LOGGER.error("Cannot make device lost for %s", self.name) + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return self._device_id + + @property + def name(self) -> str: + """Return the Apple device name.""" + return self._name + + @property + def device(self) -> AppleDevice: + """Return the Apple device.""" + return self._device + + @property + def device_class(self) -> str: + """Return the Apple device class.""" + return self._device_class + + @property + def device_model(self) -> str: + """Return the Apple device model.""" + return self._device_model + + @property + def battery_level(self) -> int: + """Return the Apple device battery level.""" + return self._battery_level + + @property + def battery_status(self) -> str: + """Return the Apple device battery status.""" + return self._battery_status + + @property + def location(self) -> Dict[str, any]: + """Return the Apple device location.""" + return self._location + + @property + def 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 new file mode 100644 index 00000000000..cf05c07e26f --- /dev/null +++ b/homeassistant/components/icloud/config_flow.py @@ -0,0 +1,230 @@ +"""Config flow to configure the iCloud integration.""" +import logging +import os + +from pyicloud import PyiCloudService +from pyicloud.exceptions import PyiCloudException, PyiCloudFailedLoginException +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.util import slugify + +from .const import ( + CONF_ACCOUNT_NAME, + CONF_GPS_ACCURACY_THRESHOLD, + CONF_MAX_INTERVAL, + DEFAULT_GPS_ACCURACY_THRESHOLD, + DEFAULT_MAX_INTERVAL, + STORAGE_KEY, + STORAGE_VERSION, +) +from .const import DOMAIN # pylint: disable=unused-import + +CONF_TRUSTED_DEVICE = "trusted_device" +CONF_VERIFICATION_CODE = "verification_code" + +_LOGGER = logging.getLogger(__name__) + + +class IcloudFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a iCloud config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + def __init__(self): + """Initialize iCloud config flow.""" + self.api = None + self._username = None + self._password = None + self._account_name = None + self._max_interval = None + self._gps_accuracy_threshold = None + + self._trusted_device = None + self._verification_code = None + + def _configuration_exists(self, username: str, account_name: str) -> bool: + """Return True if username or account_name exists in configuration.""" + for entry in self._async_current_entries(): + if ( + entry.data[CONF_USERNAME] == username + or entry.data.get(CONF_ACCOUNT_NAME) == account_name + or slugify(entry.data[CONF_USERNAME].partition("@")[0]) == account_name + ): + return True + return False + + async 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_USERNAME, default=user_input.get(CONF_USERNAME, "") + ): str, + vol.Required( + CONF_PASSWORD, default=user_input.get(CONF_PASSWORD, "") + ): str, + } + ), + errors=errors or {}, + ) + + async def async_step_user(self, user_input=None): + """Handle a flow initiated by the user.""" + errors = {} + + icloud_dir = self.hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + + if not os.path.exists(icloud_dir.path): + await self.hass.async_add_executor_job(os.makedirs, icloud_dir.path) + + if user_input is None: + return await self._show_setup_form(user_input, errors) + + self._username = user_input[CONF_USERNAME] + self._password = user_input[CONF_PASSWORD] + self._account_name = user_input.get(CONF_ACCOUNT_NAME) + self._max_interval = user_input.get(CONF_MAX_INTERVAL, DEFAULT_MAX_INTERVAL) + self._gps_accuracy_threshold = user_input.get( + CONF_GPS_ACCURACY_THRESHOLD, DEFAULT_GPS_ACCURACY_THRESHOLD + ) + + if self._configuration_exists(self._username, self._account_name): + errors[CONF_USERNAME] = "username_exists" + return await self._show_setup_form(user_input, errors) + + try: + self.api = await self.hass.async_add_executor_job( + PyiCloudService, self._username, self._password, icloud_dir.path + ) + except PyiCloudFailedLoginException as error: + _LOGGER.error("Error logging into iCloud service: %s", error) + self.api = None + errors[CONF_USERNAME] = "login" + return await self._show_setup_form(user_input, errors) + + if self.api.requires_2fa: + return await self.async_step_trusted_device() + + return self.async_create_entry( + title=self._username, + data={ + CONF_USERNAME: self._username, + CONF_PASSWORD: self._password, + CONF_ACCOUNT_NAME: self._account_name, + CONF_MAX_INTERVAL: self._max_interval, + CONF_GPS_ACCURACY_THRESHOLD: self._gps_accuracy_threshold, + }, + ) + + async def async_step_import(self, user_input): + """Import a config entry.""" + if self._configuration_exists( + user_input[CONF_USERNAME], user_input.get(CONF_ACCOUNT_NAME) + ): + return self.async_abort(reason="username_exists") + + return await self.async_step_user(user_input) + + async def async_step_trusted_device(self, user_input=None, errors=None): + """We need a trusted device.""" + if errors is None: + errors = {} + + trusted_devices = await self.hass.async_add_executor_job( + getattr, self.api, "trusted_devices" + ) + trusted_devices_for_form = {} + for i, device in enumerate(trusted_devices): + trusted_devices_for_form[i] = device.get( + "deviceName", f"SMS to {device.get('phoneNumber')}" + ) + + if user_input is None: + return await self._show_trusted_device_form( + trusted_devices_for_form, user_input, errors + ) + + self._trusted_device = trusted_devices[int(user_input[CONF_TRUSTED_DEVICE])] + + if not await self.hass.async_add_executor_job( + self.api.send_verification_code, self._trusted_device + ): + _LOGGER.error("Failed to send verification code") + self._trusted_device = None + errors[CONF_TRUSTED_DEVICE] = "send_verification_code" + + return await self._show_trusted_device_form( + trusted_devices_for_form, user_input, errors + ) + + return await self.async_step_verification_code() + + async def _show_trusted_device_form( + self, trusted_devices, user_input=None, errors=None + ): + """Show the trusted_device form to the user.""" + + return self.async_show_form( + step_id=CONF_TRUSTED_DEVICE, + data_schema=vol.Schema( + { + vol.Required(CONF_TRUSTED_DEVICE): vol.All( + vol.Coerce(int), vol.In(trusted_devices) + ) + } + ), + errors=errors or {}, + ) + + async def async_step_verification_code(self, user_input=None): + """Ask the verification code to the user.""" + errors = {} + + if user_input is None: + return await self._show_verification_code_form(user_input) + + 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.") + 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) + self._trusted_device = None + self._verification_code = None + errors["base"] = "validate_verification_code" + + return await self.async_step_trusted_device(None, errors) + + return await self.async_step_user( + { + CONF_USERNAME: self._username, + CONF_PASSWORD: self._password, + CONF_ACCOUNT_NAME: self._account_name, + CONF_MAX_INTERVAL: self._max_interval, + CONF_GPS_ACCURACY_THRESHOLD: self._gps_accuracy_threshold, + } + ) + + async def _show_verification_code_form(self, user_input=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, + ) diff --git a/homeassistant/components/icloud/const.py b/homeassistant/components/icloud/const.py new file mode 100644 index 00000000000..ed2fc78fe6d --- /dev/null +++ b/homeassistant/components/icloud/const.py @@ -0,0 +1,84 @@ +"""iCloud component constants.""" + +DOMAIN = "icloud" +TRACKER_UPDATE = f"{DOMAIN}_tracker_update" + +CONF_ACCOUNT_NAME = "account_name" +CONF_MAX_INTERVAL = "max_interval" +CONF_GPS_ACCURACY_THRESHOLD = "gps_accuracy_threshold" + +DEFAULT_MAX_INTERVAL = 30 # min +DEFAULT_GPS_ACCURACY_THRESHOLD = 500 # meters + +# to store the cookie +STORAGE_KEY = DOMAIN +STORAGE_VERSION = 1 + +ICLOUD_COMPONENTS = ["device_tracker", "sensor"] + +# pyicloud.AppleDevice status +DEVICE_BATTERY_LEVEL = "batteryLevel" +DEVICE_BATTERY_STATUS = "batteryStatus" +DEVICE_CLASS = "deviceClass" +DEVICE_DISPLAY_NAME = "deviceDisplayName" +DEVICE_ID = "id" +DEVICE_LOCATION = "location" +DEVICE_LOCATION_HORIZONTAL_ACCURACY = "horizontalAccuracy" +DEVICE_LOCATION_LATITUDE = "latitude" +DEVICE_LOCATION_LONGITUDE = "longitude" +DEVICE_LOST_MODE_CAPABLE = "lostModeCapable" +DEVICE_LOW_POWER_MODE = "lowPowerMode" +DEVICE_NAME = "name" +DEVICE_PERSON_ID = "prsId" +DEVICE_RAW_DEVICE_MODEL = "rawDeviceModel" +DEVICE_STATUS = "deviceStatus" + +DEVICE_STATUS_SET = [ + "features", + "maxMsgChar", + "darkWake", + "fmlyShare", + DEVICE_STATUS, + "remoteLock", + "activationLocked", + DEVICE_CLASS, + DEVICE_ID, + "deviceModel", + DEVICE_RAW_DEVICE_MODEL, + "passcodeLength", + "canWipeAfterLock", + "trackingInfo", + DEVICE_LOCATION, + "msg", + DEVICE_BATTERY_LEVEL, + "remoteWipe", + "thisDevice", + "snd", + DEVICE_PERSON_ID, + "wipeInProgress", + DEVICE_LOW_POWER_MODE, + "lostModeEnabled", + "isLocating", + DEVICE_LOST_MODE_CAPABLE, + "mesg", + DEVICE_NAME, + DEVICE_BATTERY_STATUS, + "lockedTimestamp", + "lostTimestamp", + "locationCapable", + DEVICE_DISPLAY_NAME, + "lostDevice", + "deviceColor", + "wipedTimestamp", + "modelDisplayName", + "locationEnabled", + "isMac", + "locFoundEnabled", +] + +DEVICE_STATUS_CODES = { + "200": "online", + "201": "offline", + "203": "pending", + "204": "unregistered", +} diff --git a/homeassistant/components/icloud/device_tracker.py b/homeassistant/components/icloud/device_tracker.py index 2ecf904314f..511ce7f9447 100644 --- a/homeassistant/components/icloud/device_tracker.py +++ b/homeassistant/components/icloud/device_tracker.py @@ -1,547 +1,136 @@ -"""Platform that supports scanning iCloud.""" +"""Support for tracking for iCloud devices.""" import logging -import random -import os +from typing import Dict -import voluptuous as vol +from homeassistant.components.device_tracker import SOURCE_TYPE_GPS +from homeassistant.components.device_tracker.config_entry import TrackerEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_USERNAME +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.typing import HomeAssistantType -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD -from homeassistant.components.device_tracker import PLATFORM_SCHEMA -from homeassistant.components.device_tracker.const import ( +from . import IcloudDevice +from .const import ( + DEVICE_LOCATION_HORIZONTAL_ACCURACY, + DEVICE_LOCATION_LATITUDE, + DEVICE_LOCATION_LONGITUDE, DOMAIN, - ATTR_ATTRIBUTES, - ENTITY_ID_FORMAT, + TRACKER_UPDATE, ) -from homeassistant.components.device_tracker.legacy import DeviceScanner -from homeassistant.components.zone import async_active_zone -from homeassistant.helpers.event import track_utc_time_change -import homeassistant.helpers.config_validation as cv -from homeassistant.util import slugify -import homeassistant.util.dt as dt_util -from homeassistant.util.location import distance -from homeassistant.util.async_ import run_callback_threadsafe _LOGGER = logging.getLogger(__name__) -CONF_ACCOUNTNAME = "account_name" -CONF_MAX_INTERVAL = "max_interval" -CONF_GPS_ACCURACY_THRESHOLD = "gps_accuracy_threshold" -# entity attributes -ATTR_ACCOUNTNAME = "account_name" -ATTR_INTERVAL = "interval" -ATTR_DEVICENAME = "device_name" -ATTR_BATTERY = "battery" -ATTR_DISTANCE = "distance" -ATTR_DEVICESTATUS = "device_status" -ATTR_LOWPOWERMODE = "low_power_mode" -ATTR_BATTERYSTATUS = "battery_status" - -ICLOUDTRACKERS = {} - -_CONFIGURING = {} - -DEVICESTATUSSET = [ - "features", - "maxMsgChar", - "darkWake", - "fmlyShare", - "deviceStatus", - "remoteLock", - "activationLocked", - "deviceClass", - "id", - "deviceModel", - "rawDeviceModel", - "passcodeLength", - "canWipeAfterLock", - "trackingInfo", - "location", - "msg", - "batteryLevel", - "remoteWipe", - "thisDevice", - "snd", - "prsId", - "wipeInProgress", - "lowPowerMode", - "lostModeEnabled", - "isLocating", - "lostModeCapable", - "mesg", - "name", - "batteryStatus", - "lockedTimestamp", - "lostTimestamp", - "locationCapable", - "deviceDisplayName", - "lostDevice", - "deviceColor", - "wipedTimestamp", - "modelDisplayName", - "locationEnabled", - "isMac", - "locFoundEnabled", -] - -DEVICESTATUSCODES = { - "200": "online", - "201": "offline", - "203": "pending", - "204": "unregistered", -} - -SERVICE_SCHEMA = vol.Schema( - { - vol.Optional(ATTR_ACCOUNTNAME): vol.All(cv.ensure_list, [cv.slugify]), - vol.Optional(ATTR_DEVICENAME): cv.slugify, - vol.Optional(ATTR_INTERVAL): cv.positive_int, - } -) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(ATTR_ACCOUNTNAME): cv.slugify, - vol.Optional(CONF_MAX_INTERVAL, default=30): cv.positive_int, - vol.Optional(CONF_GPS_ACCURACY_THRESHOLD, default=1000): cv.positive_int, - } -) +async def async_setup_scanner( + hass: HomeAssistantType, config, see, discovery_info=None +): + """Old way of setting up the iCloud tracker.""" + pass -def setup_scanner(hass, config: dict, see, discovery_info=None): - """Set up the iCloud Scanner.""" - username = config.get(CONF_USERNAME) - password = config.get(CONF_PASSWORD) - account = config.get(CONF_ACCOUNTNAME, slugify(username.partition("@")[0])) - max_interval = config.get(CONF_MAX_INTERVAL) - gps_accuracy_threshold = config.get(CONF_GPS_ACCURACY_THRESHOLD) +async def async_setup_entry( + hass: HomeAssistantType, entry: ConfigEntry, async_add_entities +): + """Configure a dispatcher connection based on a config entry.""" + username = entry.data[CONF_USERNAME] - icloudaccount = Icloud( - hass, username, password, account, max_interval, gps_accuracy_threshold, see - ) + for device in hass.data[DOMAIN][username].devices.values(): + if device.location is None: + _LOGGER.debug("No position found for %s", device.name) + continue - if icloudaccount.api is not None: - ICLOUDTRACKERS[account] = icloudaccount + _LOGGER.debug("Adding device_tracker for %s", device.name) - else: - _LOGGER.error("No ICLOUDTRACKERS added") + async_add_entities([IcloudTrackerEntity(device)]) + + +class IcloudTrackerEntity(TrackerEntity): + """Represent a tracked device.""" + + def __init__(self, device: IcloudDevice): + """Set up the iCloud tracker entity.""" + self._device = device + self._unsub_dispatcher = None + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return self._device.unique_id + + @property + def name(self) -> str: + """Return the name of the device.""" + return self._device.name + + @property + def location_accuracy(self): + """Return the location accuracy of the device.""" + return self._device.location[DEVICE_LOCATION_HORIZONTAL_ACCURACY] + + @property + def latitude(self): + """Return latitude value of the device.""" + return self._device.location[DEVICE_LOCATION_LATITUDE] + + @property + def longitude(self): + """Return longitude value of the device.""" + return self._device.location[DEVICE_LOCATION_LONGITUDE] + + @property + def should_poll(self) -> bool: + """No polling needed.""" return False - def lost_iphone(call): - """Call the lost iPhone function if the device is found.""" - accounts = call.data.get(ATTR_ACCOUNTNAME, ICLOUDTRACKERS) - devicename = call.data.get(ATTR_DEVICENAME) - for account in accounts: - if account in ICLOUDTRACKERS: - ICLOUDTRACKERS[account].lost_iphone(devicename) + @property + def battery_level(self) -> int: + """Return the battery level of the device.""" + return self._device.battery_level - hass.services.register( - DOMAIN, "icloud_lost_iphone", lost_iphone, schema=SERVICE_SCHEMA - ) + @property + def source_type(self) -> str: + """Return the source type, eg gps or router, of the device.""" + return SOURCE_TYPE_GPS - def update_icloud(call): - """Call the update function of an iCloud account.""" - accounts = call.data.get(ATTR_ACCOUNTNAME, ICLOUDTRACKERS) - devicename = call.data.get(ATTR_DEVICENAME) - for account in accounts: - if account in ICLOUDTRACKERS: - ICLOUDTRACKERS[account].update_icloud(devicename) + @property + def icon(self) -> str: + """Return the icon.""" + return icon_for_icloud_device(self._device) - hass.services.register( - DOMAIN, "icloud_update", update_icloud, schema=SERVICE_SCHEMA - ) + @property + def device_state_attributes(self) -> Dict[str, any]: + """Return the device state attributes.""" + return self._device.state_attributes - def reset_account_icloud(call): - """Reset an iCloud account.""" - accounts = call.data.get(ATTR_ACCOUNTNAME, ICLOUDTRACKERS) - for account in accounts: - if account in ICLOUDTRACKERS: - ICLOUDTRACKERS[account].reset_account_icloud() + @property + def device_info(self) -> Dict[str, any]: + """Return the device information.""" + return { + "identifiers": {(DOMAIN, self._device.unique_id)}, + "name": self._device.name, + "manufacturer": "Apple", + "model": self._device.device_model, + } - hass.services.register( - DOMAIN, "icloud_reset_account", reset_account_icloud, schema=SERVICE_SCHEMA - ) - - def setinterval(call): - """Call the update function of an iCloud account.""" - accounts = call.data.get(ATTR_ACCOUNTNAME, ICLOUDTRACKERS) - interval = call.data.get(ATTR_INTERVAL) - devicename = call.data.get(ATTR_DEVICENAME) - for account in accounts: - if account in ICLOUDTRACKERS: - ICLOUDTRACKERS[account].setinterval(interval, devicename) - - hass.services.register( - DOMAIN, "icloud_set_interval", setinterval, schema=SERVICE_SCHEMA - ) - - # Tells the bootstrapper that the component was successfully initialized - return True - - -class Icloud(DeviceScanner): - """Representation of an iCloud account.""" - - def __init__( - self, hass, username, password, name, max_interval, gps_accuracy_threshold, see - ): - """Initialize an iCloud account.""" - self.hass = hass - self.username = username - self.password = password - self.api = None - self.accountname = name - self.devices = {} - self.seen_devices = {} - self._overridestates = {} - self._intervals = {} - self._max_interval = max_interval - self._gps_accuracy_threshold = gps_accuracy_threshold - self.see = see - - self._trusted_device = None - self._verification_code = None - - self._attrs = {} - self._attrs[ATTR_ACCOUNTNAME] = name - - self.reset_account_icloud() - - randomseconds = random.randint(10, 59) - track_utc_time_change(self.hass, self.keep_alive, second=randomseconds) - - def reset_account_icloud(self): - """Reset an iCloud account.""" - from pyicloud import PyiCloudService - from pyicloud.exceptions import ( - PyiCloudFailedLoginException, - PyiCloudNoDevicesException, + async def async_added_to_hass(self): + """Register state update callback.""" + self._unsub_dispatcher = async_dispatcher_connect( + self.hass, TRACKER_UPDATE, self.async_write_ha_state ) - icloud_dir = self.hass.config.path("icloud") - if not os.path.exists(icloud_dir): - os.makedirs(icloud_dir) + async def async_will_remove_from_hass(self): + """Clean up after entity before removal.""" + self._unsub_dispatcher() - try: - self.api = PyiCloudService( - self.username, self.password, cookie_directory=icloud_dir, verify=True - ) - except PyiCloudFailedLoginException as error: - self.api = None - _LOGGER.error("Error logging into iCloud Service: %s", error) - return - try: - self.devices = {} - self._overridestates = {} - self._intervals = {} - for device in self.api.devices: - status = device.status(DEVICESTATUSSET) - _LOGGER.debug("Device Status is %s", status) - devicename = slugify(status["name"].replace(" ", "", 99)) - _LOGGER.info("Adding icloud device: %s", devicename) - if devicename in self.devices: - _LOGGER.error("Multiple devices with name: %s", devicename) - continue - self.devices[devicename] = device - self._intervals[devicename] = 1 - self._overridestates[devicename] = None - except PyiCloudNoDevicesException: - _LOGGER.error("No iCloud Devices found!") +def icon_for_icloud_device(icloud_device: IcloudDevice) -> str: + """Return a battery icon valid identifier.""" + switcher = { + "iPad": "mdi:tablet-ipad", + "iPhone": "mdi:cellphone-iphone", + "iPod": "mdi:ipod", + "iMac": "mdi:desktop-mac", + "MacBookPro": "mdi:laptop-mac", + } - def icloud_trusted_device_callback(self, callback_data): - """Handle chosen trusted devices.""" - self._trusted_device = int(callback_data.get("trusted_device")) - self._trusted_device = self.api.trusted_devices[self._trusted_device] - - if not self.api.send_verification_code(self._trusted_device): - _LOGGER.error("Failed to send verification code") - self._trusted_device = None - return - - if self.accountname in _CONFIGURING: - request_id = _CONFIGURING.pop(self.accountname) - configurator = self.hass.components.configurator - configurator.request_done(request_id) - - # Trigger the next step immediately - self.icloud_need_verification_code() - - def icloud_need_trusted_device(self): - """We need a trusted device.""" - configurator = self.hass.components.configurator - if self.accountname in _CONFIGURING: - return - - devicesstring = "" - devices = self.api.trusted_devices - for i, device in enumerate(devices): - devicename = device.get( - "deviceName", "SMS to %s" % device.get("phoneNumber") - ) - devicesstring += f"{i}: {devicename};" - - _CONFIGURING[self.accountname] = configurator.request_config( - f"iCloud {self.accountname}", - self.icloud_trusted_device_callback, - description=( - "Please choose your trusted device by entering" - " the index from this list: " + devicesstring - ), - entity_picture="/static/images/config_icloud.png", - submit_caption="Confirm", - fields=[{"id": "trusted_device", "name": "Trusted Device"}], - ) - - def icloud_verification_callback(self, callback_data): - """Handle the chosen trusted device.""" - from pyicloud.exceptions import PyiCloudException - - self._verification_code = callback_data.get("code") - - try: - if not self.api.validate_verification_code( - self._trusted_device, self._verification_code - ): - raise PyiCloudException("Unknown failure") - 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) - self._trusted_device = None - self._verification_code = None - - # Trigger the next step immediately - self.icloud_need_trusted_device() - - if self.accountname in _CONFIGURING: - request_id = _CONFIGURING.pop(self.accountname) - configurator = self.hass.components.configurator - configurator.request_done(request_id) - - def icloud_need_verification_code(self): - """Return the verification code.""" - configurator = self.hass.components.configurator - if self.accountname in _CONFIGURING: - return - - _CONFIGURING[self.accountname] = configurator.request_config( - f"iCloud {self.accountname}", - self.icloud_verification_callback, - description=("Please enter the validation code:"), - entity_picture="/static/images/config_icloud.png", - submit_caption="Confirm", - fields=[{"id": "code", "name": "code"}], - ) - - def keep_alive(self, now): - """Keep the API alive.""" - if self.api is None: - self.reset_account_icloud() - - if self.api is None: - return - - if self.api.requires_2fa: - from pyicloud.exceptions import PyiCloudException - - try: - if self._trusted_device is None: - self.icloud_need_trusted_device() - return - - if self._verification_code is None: - self.icloud_need_verification_code() - return - - self.api.authenticate() - if self.api.requires_2fa: - raise Exception("Unknown failure") - - self._trusted_device = None - self._verification_code = None - except PyiCloudException as error: - _LOGGER.error("Error setting up 2FA: %s", error) - else: - self.api.authenticate() - - currentminutes = dt_util.now().hour * 60 + dt_util.now().minute - try: - for devicename in self.devices: - interval = self._intervals.get(devicename, 1) - if (currentminutes % interval == 0) or ( - interval > 10 and currentminutes % interval in [2, 4] - ): - self.update_device(devicename) - except ValueError: - _LOGGER.debug("iCloud API returned an error") - - def determine_interval(self, devicename, latitude, longitude, battery): - """Calculate new interval.""" - currentzone = run_callback_threadsafe( - self.hass.loop, async_active_zone, self.hass, latitude, longitude - ).result() - - if ( - currentzone is not None - and currentzone == self._overridestates.get(devicename) - ) or (currentzone is None and self._overridestates.get(devicename) == "away"): - return - - zones = ( - self.hass.states.get(entity_id) - for entity_id in sorted(self.hass.states.entity_ids("zone")) - ) - - distances = [] - for zone_state in zones: - zone_state_lat = zone_state.attributes["latitude"] - zone_state_long = zone_state.attributes["longitude"] - zone_distance = distance( - latitude, longitude, zone_state_lat, zone_state_long - ) - distances.append(round(zone_distance / 1000, 1)) - - if distances: - mindistance = min(distances) - else: - mindistance = None - - self._overridestates[devicename] = None - - if currentzone is not None: - self._intervals[devicename] = self._max_interval - return - - if mindistance is None: - return - - # Calculate out how long it would take for the device to drive to the - # nearest zone at 120 km/h: - interval = round(mindistance / 2, 0) - - # Never poll more than once per minute - interval = max(interval, 1) - - if interval > 180: - # Three hour drive? This is far enough that they might be flying - interval = 30 - - if battery is not None and battery <= 33 and mindistance > 3: - # Low battery - let's check half as often - interval = interval * 2 - - self._intervals[devicename] = interval - - def update_device(self, devicename): - """Update the device_tracker entity.""" - from pyicloud.exceptions import PyiCloudNoDevicesException - - # An entity will not be created by see() when track=false in - # 'known_devices.yaml', but we need to see() it at least once - entity = self.hass.states.get(ENTITY_ID_FORMAT.format(devicename)) - if entity is None and devicename in self.seen_devices: - return - attrs = {} - kwargs = {} - - if self.api is None: - return - - try: - for device in self.api.devices: - if str(device) != str(self.devices[devicename]): - continue - - status = device.status(DEVICESTATUSSET) - _LOGGER.debug("Device Status is %s", status) - dev_id = status["name"].replace(" ", "", 99) - dev_id = slugify(dev_id) - attrs[ATTR_DEVICESTATUS] = DEVICESTATUSCODES.get( - status["deviceStatus"], "error" - ) - attrs[ATTR_LOWPOWERMODE] = status["lowPowerMode"] - attrs[ATTR_BATTERYSTATUS] = status["batteryStatus"] - attrs[ATTR_ACCOUNTNAME] = self.accountname - status = device.status(DEVICESTATUSSET) - battery = status.get("batteryLevel", 0) * 100 - location = status["location"] - if location and location["horizontalAccuracy"]: - horizontal_accuracy = int(location["horizontalAccuracy"]) - if horizontal_accuracy < self._gps_accuracy_threshold: - self.determine_interval( - devicename, - location["latitude"], - location["longitude"], - battery, - ) - interval = self._intervals.get(devicename, 1) - attrs[ATTR_INTERVAL] = interval - accuracy = location["horizontalAccuracy"] - kwargs["dev_id"] = dev_id - kwargs["host_name"] = status["name"] - kwargs["gps"] = (location["latitude"], location["longitude"]) - kwargs["battery"] = battery - kwargs["gps_accuracy"] = accuracy - kwargs[ATTR_ATTRIBUTES] = attrs - self.see(**kwargs) - self.seen_devices[devicename] = True - except PyiCloudNoDevicesException: - _LOGGER.error("No iCloud Devices found") - - def lost_iphone(self, devicename): - """Call the lost iPhone function if the device is found.""" - if self.api is None: - return - - self.api.authenticate() - for device in self.api.devices: - if str(device) == str(self.devices[devicename]): - _LOGGER.info("Playing Lost iPhone sound for %s", devicename) - device.play_sound() - - def update_icloud(self, devicename=None): - """Request device information from iCloud and update device_tracker.""" - from pyicloud.exceptions import PyiCloudNoDevicesException - - if self.api is None: - return - - try: - if devicename is not None: - if devicename in self.devices: - self.update_device(devicename) - else: - _LOGGER.error( - "devicename %s unknown for account %s", - devicename, - self._attrs[ATTR_ACCOUNTNAME], - ) - else: - for device in self.devices: - self.update_device(device) - except PyiCloudNoDevicesException: - _LOGGER.error("No iCloud Devices found") - - def setinterval(self, interval=None, devicename=None): - """Set the interval of the given devices.""" - devs = [devicename] if devicename else self.devices - for device in devs: - devid = f"{DOMAIN}.{device}" - devicestate = self.hass.states.get(devid) - if interval is not None: - if devicestate is not None: - self._overridestates[device] = run_callback_threadsafe( - self.hass.loop, - async_active_zone, - self.hass, - float(devicestate.attributes.get("latitude", 0)), - float(devicestate.attributes.get("longitude", 0)), - ).result() - if self._overridestates[device] is None: - self._overridestates[device] = "away" - self._intervals[device] = interval - else: - self._overridestates[device] = None - self.update_device(device) + return switcher.get(icloud_device.device_class, "mdi:cellphone-link") diff --git a/homeassistant/components/icloud/manifest.json b/homeassistant/components/icloud/manifest.json index d3924ee61a8..f7295ceae4d 100644 --- a/homeassistant/components/icloud/manifest.json +++ b/homeassistant/components/icloud/manifest.json @@ -1,10 +1,13 @@ { "domain": "icloud", - "name": "Icloud", - "documentation": "https://www.home-assistant.io/integrations/icloud", + "name": "iCloud", + "config_flow": true, + "documentation": "https://www.home-assistant.io/components/icloud", "requirements": [ "pyicloud==0.9.1" ], - "dependencies": ["configurator"], - "codeowners": [] -} + "dependencies": [], + "codeowners": [ + "@Quentame" + ] +} \ No newline at end of file diff --git a/homeassistant/components/icloud/sensor.py b/homeassistant/components/icloud/sensor.py new file mode 100644 index 00000000000..4351d4ffa19 --- /dev/null +++ b/homeassistant/components/icloud/sensor.py @@ -0,0 +1,85 @@ +"""Support for iCloud sensors.""" +import logging +from typing import Dict + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_USERNAME, DEVICE_CLASS_BATTERY +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.icon import icon_for_battery_level +from homeassistant.helpers.typing import HomeAssistantType + +from . import IcloudDevice +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistantType, entry: ConfigEntry, async_add_entities +) -> None: + """Set up iCloud devices sensors based on a config entry.""" + username = entry.data[CONF_USERNAME] + + entities = [] + for device in hass.data[DOMAIN][username].devices.values(): + if device.battery_level is not None: + _LOGGER.debug("Adding battery sensor for %s", device.name) + entities.append(IcloudDeviceBatterySensor(device)) + + async_add_entities(entities, True) + + +class IcloudDeviceBatterySensor(Entity): + """Representation of a iCloud device battery sensor.""" + + def __init__(self, device: IcloudDevice): + """Initialize the battery sensor.""" + self._device = device + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return f"{self._device.unique_id}_battery" + + @property + def name(self) -> str: + """Sensor name.""" + return f"{self._device.name} battery state" + + @property + def device_class(self) -> str: + """Return the device class of the sensor.""" + return DEVICE_CLASS_BATTERY + + @property + def state(self) -> int: + """Battery state percentage.""" + return self._device.battery_level + + @property + def unit_of_measurement(self) -> str: + """Battery state measured in percentage.""" + return "%" + + @property + def icon(self) -> str: + """Battery state icon handling.""" + return icon_for_battery_level( + battery_level=self._device.battery_level, + charging=self._device.battery_status == "Charging", + ) + + @property + def device_state_attributes(self) -> Dict[str, any]: + """Return default attributes for the iCloud device entity.""" + return self._device.state_attributes + + @property + def device_info(self) -> Dict[str, any]: + """Return the device information.""" + return { + "identifiers": {(DOMAIN, self._device.unique_id)}, + "name": self._device.name, + "manufacturer": "Apple", + "model": self._device.device_model, + } diff --git a/homeassistant/components/icloud/services.yaml b/homeassistant/components/icloud/services.yaml index e69de29bb2d..ce239df7564 100644 --- a/homeassistant/components/icloud/services.yaml +++ b/homeassistant/components/icloud/services.yaml @@ -0,0 +1,49 @@ +update: + description: Update iCloud devices. + fields: + account: + description: Your iCloud account username (email) or account name. + example: 'steve@apple.com' + +play_sound: + description: Play sound on an Apple device. + fields: + account: + description: (required) Your iCloud account username (email) or account name. + example: 'steve@apple.com' + device_name: + description: (required) The name of the Apple device to play a sound. + example: 'stevesiphone' + +display_message: + description: Display a message on an Apple device. + fields: + account: + description: (required) Your iCloud account username (email) or account name. + example: 'steve@apple.com' + device_name: + description: (required) The name of the Apple device to display the message. + example: 'stevesiphone' + message: + description: (required) The content of your message. + example: 'Hey Steve !' + sound: + description: To make a sound when displaying the message (boolean). + example: 'true' + +lost_device: + description: Make an Apple device in lost state. + fields: + account: + description: (required) Your iCloud account username (email) or account name. + example: 'steve@apple.com' + device_name: + description: (required) The name of the Apple device to set lost. + example: 'stevesiphone' + number: + description: (required) The phone number to call in lost mode (must contain country code). + example: '+33450020100' + message: + description: (required) The message to display in lost mode. + example: 'Call me' + diff --git a/homeassistant/components/icloud/strings.json b/homeassistant/components/icloud/strings.json new file mode 100644 index 00000000000..117e26c8830 --- /dev/null +++ b/homeassistant/components/icloud/strings.json @@ -0,0 +1,38 @@ +{ + "config": { + "title": "Apple iCloud", + "step": { + "user": { + "title": "iCloud credentials", + "description": "Enter your credentials", + "data": { + "username": "Email", + "password": "Password" + } + }, + "trusted_device": { + "title": "iCloud trusted device", + "description": "Select your trusted device", + "data": { + "trusted_device": "Trusted device" + } + }, + "verification_code": { + "title": "iCloud verification code", + "description": "Please enter the verification code you just received from iCloud", + "data": { + "verification_code": "Verification code" + } + } + }, + "error": { + "username_exists": "Account already configured", + "login": "Login error: please check your email & password", + "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" + }, + "abort": { + "username_exists": "Account already configured" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/idteck_prox/__init__.py b/homeassistant/components/idteck_prox/__init__.py index 089347a0f73..9cc4f3de9d6 100644 --- a/homeassistant/components/idteck_prox/__init__.py +++ b/homeassistant/components/idteck_prox/__init__.py @@ -1,15 +1,16 @@ """Component for interfacing RFK101 proximity card readers.""" import logging +from rfk101py.rfk101py import rfk101py import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( CONF_HOST, - CONF_PORT, CONF_NAME, + CONF_PORT, EVENT_HOMEASSISTANT_STOP, ) +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -68,7 +69,6 @@ class IdteckReader: def connect(self): """Connect to the reader.""" - from rfk101py.rfk101py import rfk101py self._connection = rfk101py(self._host, self._port, self._callback) diff --git a/homeassistant/components/ifttt/.translations/da.json b/homeassistant/components/ifttt/.translations/da.json index 25c502ed05e..0e0c735eb89 100644 --- a/homeassistant/components/ifttt/.translations/da.json +++ b/homeassistant/components/ifttt/.translations/da.json @@ -1,15 +1,15 @@ { "config": { "abort": { - "not_internet_accessible": "Dit Home Assistant system skal v\u00e6re tilg\u00e6ngeligt fra internettet for at modtage IFTTT meddelelser.", - "one_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning" + "not_internet_accessible": "Din Home Assistant-instans skal v\u00e6re tilg\u00e6ngelig fra internettet for at modtage IFTTT-meddelelser", + "one_instance_allowed": "Kun en enkelt instans er n\u00f8dvendig." }, "create_entry": { - "default": "For at sende begivenheder til Home Assistant skal du bruge handlingen \"Foretag en web foresp\u00f8rgsel\" fra [IFTTT Webhook applet] ({applet_url}).\n\n Udfyld f\u00f8lgende oplysninger: \n\n - URL: `{webhook_url}`\n - Metode: POST\n - Indholdstype: application/json\n\n Se [dokumentationen] ({docs_url}) om hvordan du konfigurerer automatiseringer til at h\u00e5ndtere indg\u00e5ende data." + "default": "For at sende h\u00e6ndelser til Home Assistant skal du bruge handlingen \"Foretag en web-foresp\u00f8rgsel\" fra [IFTTT Webhook-applet] ({applet_url}).\n\n Udfyld f\u00f8lgende oplysninger: \n\n - Webadresse: `{webhook_url}`\n - Metode: POST\n - Indholdstype: application/json\n\nSe [dokumentationen] ({docs_url}) om hvordan du konfigurerer automatiseringer til at h\u00e5ndtere indg\u00e5ende data." }, "step": { "user": { - "description": "Er du sikker p\u00e5 at du vil oprette IFTTT?", + "description": "Er du sikker p\u00e5, at du vil konfigurere IFTTT?", "title": "Konfigurer IFTTT Webhook Applet" } }, diff --git a/homeassistant/components/ifttt/.translations/ko.json b/homeassistant/components/ifttt/.translations/ko.json index 75bdd0d99c8..9c8083a1d94 100644 --- a/homeassistant/components/ifttt/.translations/ko.json +++ b/homeassistant/components/ifttt/.translations/ko.json @@ -9,7 +9,7 @@ }, "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 Webhook \uc560\ud50c\ub9bf \uc124\uc815" } }, diff --git a/homeassistant/components/ifttt/__init__.py b/homeassistant/components/ifttt/__init__.py index 05d773e9fd6..3011f5a2a0a 100644 --- a/homeassistant/components/ifttt/__init__.py +++ b/homeassistant/components/ifttt/__init__.py @@ -9,6 +9,7 @@ import voluptuous as vol from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.helpers import config_entry_flow import homeassistant.helpers.config_validation as cv + from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -23,6 +24,7 @@ ATTR_VALUE3 = "value3" CONF_KEY = "key" +SERVICE_PUSH_ALARM_STATE = "push_alarm_state" SERVICE_TRIGGER = "trigger" SERVICE_TRIGGER_SCHEMA = vol.Schema( diff --git a/homeassistant/components/ifttt/alarm_control_panel.py b/homeassistant/components/ifttt/alarm_control_panel.py index e4d8b6ce654..2c281e58c48 100644 --- a/homeassistant/components/ifttt/alarm_control_panel.py +++ b/homeassistant/components/ifttt/alarm_control_panel.py @@ -4,8 +4,17 @@ import re import voluptuous as vol -import homeassistant.components.alarm_control_panel as alarm -from homeassistant.components.alarm_control_panel import DOMAIN, PLATFORM_SCHEMA +from homeassistant.components.alarm_control_panel import ( + FORMAT_NUMBER, + FORMAT_TEXT, + PLATFORM_SCHEMA, + AlarmControlPanel, +) +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, +) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_STATE, @@ -19,7 +28,7 @@ from homeassistant.const import ( ) import homeassistant.helpers.config_validation as cv -from . import ATTR_EVENT, DOMAIN as IFTTT_DOMAIN, SERVICE_TRIGGER +from . import ATTR_EVENT, DOMAIN, SERVICE_PUSH_ALARM_STATE, SERVICE_TRIGGER _LOGGER = logging.getLogger(__name__) @@ -55,8 +64,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( } ) -SERVICE_PUSH_ALARM_STATE = "ifttt_push_alarm_state" - PUSH_ALARM_STATE_SERVICE_SCHEMA = vol.Schema( {vol.Required(ATTR_ENTITY_ID): cv.entity_ids, vol.Required(ATTR_STATE): cv.string} ) @@ -101,7 +108,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) -class IFTTTAlarmPanel(alarm.AlarmControlPanel): +class IFTTTAlarmPanel(AlarmControlPanel): """Representation of an alarm control panel controlled through IFTTT.""" def __init__( @@ -127,6 +134,11 @@ class IFTTTAlarmPanel(alarm.AlarmControlPanel): """Return the state of the device.""" return self._state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT + @property def assumed_state(self): """Notify that this platform return an assumed state.""" @@ -138,8 +150,8 @@ class IFTTTAlarmPanel(alarm.AlarmControlPanel): if self._code is None: return None if isinstance(self._code, str) and re.search("^\\d+$", self._code): - return alarm.FORMAT_NUMBER - return alarm.FORMAT_TEXT + return FORMAT_NUMBER + return FORMAT_TEXT def alarm_disarm(self, code=None): """Send disarm command.""" @@ -169,7 +181,7 @@ class IFTTTAlarmPanel(alarm.AlarmControlPanel): """Call the IFTTT trigger service to change the alarm state.""" data = {ATTR_EVENT: event} - self.hass.services.call(IFTTT_DOMAIN, SERVICE_TRIGGER, data) + self.hass.services.call(DOMAIN, SERVICE_TRIGGER, data) _LOGGER.debug("Called IFTTT integration to trigger event %s", event) if self._optimistic: self._state = state diff --git a/homeassistant/components/ifttt/config_flow.py b/homeassistant/components/ifttt/config_flow.py index ae9be6b698c..dc28f6bbaa2 100644 --- a/homeassistant/components/ifttt/config_flow.py +++ b/homeassistant/components/ifttt/config_flow.py @@ -1,7 +1,7 @@ """Config flow for IFTTT.""" from homeassistant.helpers import config_entry_flow -from .const import DOMAIN +from .const import DOMAIN config_entry_flow.register_webhook_flow( DOMAIN, diff --git a/homeassistant/components/ifttt/services.yaml b/homeassistant/components/ifttt/services.yaml index 8669bc07fb4..693c654f258 100644 --- a/homeassistant/components/ifttt/services.yaml +++ b/homeassistant/components/ifttt/services.yaml @@ -1,5 +1,14 @@ # Describes the format for available ifttt services +push_alarm_state: + description: Update the alarm state to the specified value. + fields: + entity_id: + description: Name of the alarm control panel which state has to be updated. + example: 'alarm_control_panel.downstairs' + state: + description: The state to which the alarm control panel has to be set. + example: 'armed_night' trigger: description: Triggers the configured IFTTT Webhook. @@ -15,4 +24,4 @@ trigger: example: 'some additional data' value3: description: Generic field to send data via the event. - example: 'even more data' \ No newline at end of file + example: 'even more data' diff --git a/homeassistant/components/iglo/light.py b/homeassistant/components/iglo/light.py index d93ebcb920a..59e6db2a81f 100644 --- a/homeassistant/components/iglo/light.py +++ b/homeassistant/components/iglo/light.py @@ -2,6 +2,7 @@ import logging import math +from iglo import Lamp import voluptuous as vol from homeassistant.components.light import ( @@ -9,11 +10,11 @@ from homeassistant.components.light import ( ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_HS_COLOR, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR_TEMP, - SUPPORT_COLOR, - SUPPORT_EFFECT, PLATFORM_SCHEMA, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, + SUPPORT_COLOR_TEMP, + SUPPORT_EFFECT, Light, ) from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT @@ -47,7 +48,6 @@ class IGloLamp(Light): def __init__(self, name, host, port): """Initialize the light.""" - from iglo import Lamp self._name = name self._lamp = Lamp(0, host, port) diff --git a/homeassistant/components/ign_sismologia/geo_location.py b/homeassistant/components/ign_sismologia/geo_location.py index 8ad045c9f7a..deecc389e7e 100644 --- a/homeassistant/components/ign_sismologia/geo_location.py +++ b/homeassistant/components/ign_sismologia/geo_location.py @@ -3,6 +3,7 @@ from datetime import timedelta import logging from typing import Optional +from georss_ign_sismologia_client import IgnSismologiaFeedManager import voluptuous as vol from homeassistant.components.geo_location import PLATFORM_SCHEMA, GeolocationEvent @@ -87,7 +88,6 @@ class IgnSismologiaFeedEntityManager: minimum_magnitude, ): """Initialize the Feed Entity Manager.""" - from georss_ign_sismologia_client import IgnSismologiaFeedManager self._hass = hass self._feed_manager = IgnSismologiaFeedManager( diff --git a/homeassistant/components/ihc/__init__.py b/homeassistant/components/ihc/__init__.py index a55b94eb26a..b246943b6ad 100644 --- a/homeassistant/components/ihc/__init__.py +++ b/homeassistant/components/ihc/__init__.py @@ -2,6 +2,8 @@ import logging import os.path +from defusedxml import ElementTree +from ihcsdk.ihccontroller import IHCController import voluptuous as vol from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA @@ -214,7 +216,6 @@ def setup(hass, config): def ihc_setup(hass, config, conf, controller_id): """Set up the IHC component.""" - from ihcsdk.ihccontroller import IHCController url = conf[CONF_URL] username = conf[CONF_USERNAME] @@ -272,7 +273,6 @@ def autosetup_ihc_products( hass: HomeAssistantType, config, ihc_controller, controller_id ): """Auto setup of IHC products from the IHC project file.""" - from defusedxml import ElementTree project_xml = ihc_controller.get_project() if not project_xml: diff --git a/homeassistant/components/ihc/manifest.json b/homeassistant/components/ihc/manifest.json index a415b0e3103..cfc86f5e3cb 100644 --- a/homeassistant/components/ihc/manifest.json +++ b/homeassistant/components/ihc/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/ihc", "requirements": [ "defusedxml==0.6.0", - "ihcsdk==2.3.0" + "ihcsdk==2.4.0" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/image_processing/__init__.py b/homeassistant/components/image_processing/__init__.py index 4c90441e7f0..a8f5f0f097e 100644 --- a/homeassistant/components/image_processing/__init__.py +++ b/homeassistant/components/image_processing/__init__.py @@ -2,16 +2,14 @@ import asyncio from datetime import timedelta import logging -from typing import Tuple -from PIL import ImageDraw import voluptuous as vol from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME, CONF_ENTITY_ID, CONF_NAME from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA +from homeassistant.helpers.config_validation import make_entity_service_schema from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.util.async_ import run_callback_threadsafe @@ -65,46 +63,6 @@ PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend( PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE.extend(PLATFORM_SCHEMA.schema) -def draw_box( - draw: ImageDraw, - box: Tuple[float, float, float, float], - img_width: int, - img_height: int, - text: str = "", - color: Tuple[int, int, int] = (255, 255, 0), -) -> None: - """ - Draw a bounding box on and image. - - The bounding box is defined by the tuple (y_min, x_min, y_max, x_max) - where the coordinates are floats in the range [0.0, 1.0] and - relative to the width and height of the image. - - For example, if an image is 100 x 200 pixels (height x width) and the bounding - box is `(0.1, 0.2, 0.5, 0.9)`, the upper-left and bottom-right coordinates of - the bounding box will be `(40, 10)` to `(180, 50)` (in (x,y) coordinates). - """ - - line_width = 3 - font_height = 8 - y_min, x_min, y_max, x_max = box - (left, right, top, bottom) = ( - x_min * img_width, - x_max * img_width, - y_min * img_height, - y_max * img_height, - ) - draw.line( - [(left, top), (left, bottom), (right, bottom), (right, top), (left, top)], - width=line_width, - fill=color, - ) - if text: - draw.text( - (left + line_width, abs(top - line_width - font_height)), text, fill=color - ) - - async def async_setup(hass, config): """Set up the image processing.""" component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL) @@ -124,7 +82,7 @@ async def async_setup(hass, config): await asyncio.wait(update_tasks) hass.services.async_register( - DOMAIN, SERVICE_SCAN, async_scan_service, schema=ENTITY_SERVICE_SCHEMA + DOMAIN, SERVICE_SCAN, async_scan_service, schema=make_entity_service_schema({}) ) return True diff --git a/homeassistant/components/image_processing/manifest.json b/homeassistant/components/image_processing/manifest.json index e986ac6f4ca..1e9f2963a38 100644 --- a/homeassistant/components/image_processing/manifest.json +++ b/homeassistant/components/image_processing/manifest.json @@ -2,11 +2,7 @@ "domain": "image_processing", "name": "Image processing", "documentation": "https://www.home-assistant.io/integrations/image_processing", - "requirements": [ - "pillow==6.2.1" - ], - "dependencies": [ - "camera" - ], + "requirements": [], + "dependencies": ["camera"], "codeowners": [] } diff --git a/homeassistant/components/image_processing/services.yaml b/homeassistant/components/image_processing/services.yaml index 0689c34c1a3..1f1fa347dc9 100644 --- a/homeassistant/components/image_processing/services.yaml +++ b/homeassistant/components/image_processing/services.yaml @@ -6,16 +6,3 @@ scan: entity_id: description: Name(s) of entities to scan immediately. example: 'image_processing.alpr_garage' - -facebox_teach_face: - description: Teach facebox a face using a file. - fields: - entity_id: - description: The facebox entity to teach. - example: 'image_processing.facebox' - name: - description: The name of the face to teach. - example: 'my_name' - file_path: - description: The path to the image file. - example: '/images/my_image.jpg' diff --git a/homeassistant/components/imap/sensor.py b/homeassistant/components/imap/sensor.py index db2f528153b..ceef8acf7c3 100644 --- a/homeassistant/components/imap/sensor.py +++ b/homeassistant/components/imap/sensor.py @@ -162,7 +162,7 @@ class ImapSensor(Entity): self._email_count = len(lines[0].split()) else: _LOGGER.error( - "Can't parse IMAP server response to search " "'%s': %s / %s", + "Can't parse IMAP server response to search '%s': %s / %s", self._search, result, lines[0], diff --git a/homeassistant/components/imap_email_content/sensor.py b/homeassistant/components/imap_email_content/sensor.py index 62dceae0dad..307d5a22c1e 100644 --- a/homeassistant/components/imap_email_content/sensor.py +++ b/homeassistant/components/imap_email_content/sensor.py @@ -1,24 +1,24 @@ """Email sensor support.""" -import logging +from collections import deque import datetime import email -from collections import deque - import imaplib +import logging + import voluptuous as vol -from homeassistant.helpers.entity import Entity from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( + ATTR_DATE, CONF_NAME, + CONF_PASSWORD, CONF_PORT, CONF_USERNAME, - CONF_PASSWORD, CONF_VALUE_TEMPLATE, CONTENT_TYPE_TEXT_PLAIN, - ATTR_DATE, ) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/influxdb/__init__.py b/homeassistant/components/influxdb/__init__.py index 86d489621ea..48852f27910 100644 --- a/homeassistant/components/influxdb/__init__.py +++ b/homeassistant/components/influxdb/__init__.py @@ -1,11 +1,12 @@ """Support for sending data to an Influx database.""" import logging -import re +import math import queue +import re import threading import time -import math +from influxdb import InfluxDBClient, exceptions import requests.exceptions import voluptuous as vol @@ -20,12 +21,12 @@ from homeassistant.const import ( CONF_SSL, CONF_USERNAME, CONF_VERIFY_SSL, - EVENT_STATE_CHANGED, EVENT_HOMEASSISTANT_STOP, + EVENT_STATE_CHANGED, STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.helpers import state as state_helper, event as event_helper +from homeassistant.helpers import event as event_helper, state as state_helper import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_values import EntityValues @@ -118,7 +119,6 @@ RE_DECIMAL = re.compile(r"[^\d.]+") def setup(hass, config): """Set up the InfluxDB component.""" - from influxdb import InfluxDBClient, exceptions conf = config[DOMAIN] @@ -341,7 +341,6 @@ class InfluxThread(threading.Thread): def write_to_influxdb(self, json): """Write preprocessed events to influxdb, with retry.""" - from influxdb import exceptions for retry in range(self.max_tries + 1): try: diff --git a/homeassistant/components/influxdb/sensor.py b/homeassistant/components/influxdb/sensor.py index e94824c9abb..4a169453e35 100644 --- a/homeassistant/components/influxdb/sensor.py +++ b/homeassistant/components/influxdb/sensor.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +from influxdb import InfluxDBClient, exceptions import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -94,7 +95,6 @@ class InfluxSensor(Entity): def __init__(self, hass, influx_conf, query): """Initialize the sensor.""" - from influxdb import InfluxDBClient, exceptions self._name = query.get(CONF_NAME) self._unit_of_measurement = query.get(CONF_UNIT_OF_MEASUREMENT) @@ -205,14 +205,13 @@ class InfluxSensorData: points = list(self.influx.query(self.query).get_points()) if not points: _LOGGER.warning( - "Query returned no points, sensor state set " "to UNKNOWN: %s", - self.query, + "Query returned no points, sensor state set to UNKNOWN: %s", self.query, ) self.value = None else: if len(points) > 1: _LOGGER.warning( - "Query returned multiple points, only first " "one shown: %s", + "Query returned multiple points, only first one shown: %s", self.query, ) self.value = points[0].get("value") diff --git a/homeassistant/components/input_boolean/__init__.py b/homeassistant/components/input_boolean/__init__.py index 6027b0b3da1..326244a7e4c 100644 --- a/homeassistant/components/input_boolean/__init__.py +++ b/homeassistant/components/input_boolean/__init__.py @@ -6,17 +6,18 @@ import voluptuous as vol from homeassistant.const import ( CONF_ICON, CONF_NAME, + SERVICE_RELOAD, + SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, - SERVICE_TOGGLE, STATE_ON, ) -from homeassistant.loader import bind_hass import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity +import homeassistant.helpers.service +from homeassistant.loader import bind_hass DOMAIN = "input_boolean" @@ -42,6 +43,8 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) +RELOAD_SERVICE_SCHEMA = vol.Schema({}) + @bind_hass def is_on(hass, entity_id): @@ -53,6 +56,39 @@ async def async_setup(hass, config): """Set up an input boolean.""" component = EntityComponent(_LOGGER, DOMAIN, hass) + entities = await _async_process_config(config) + + async def reload_service_handler(service_call): + """Remove all input booleans and load new ones from config.""" + conf = await component.async_prepare_reload() + if conf is None: + return + new_entities = await _async_process_config(conf) + if new_entities: + await component.async_add_entities(new_entities) + + homeassistant.helpers.service.async_register_admin_service( + hass, + DOMAIN, + SERVICE_RELOAD, + reload_service_handler, + schema=RELOAD_SERVICE_SCHEMA, + ) + + component.async_register_entity_service(SERVICE_TURN_ON, {}, "async_turn_on") + + component.async_register_entity_service(SERVICE_TURN_OFF, {}, "async_turn_off") + + component.async_register_entity_service(SERVICE_TOGGLE, {}, "async_toggle") + + if entities: + await component.async_add_entities(entities) + + return True + + +async def _async_process_config(config): + """Process config and create list of entities.""" entities = [] for object_id, cfg in config[DOMAIN].items(): @@ -65,23 +101,7 @@ async def async_setup(hass, config): entities.append(InputBoolean(object_id, name, initial, icon)) - if not entities: - return False - - component.async_register_entity_service( - SERVICE_TURN_ON, ENTITY_SERVICE_SCHEMA, "async_turn_on" - ) - - component.async_register_entity_service( - SERVICE_TURN_OFF, ENTITY_SERVICE_SCHEMA, "async_turn_off" - ) - - component.async_register_entity_service( - SERVICE_TOGGLE, ENTITY_SERVICE_SCHEMA, "async_toggle" - ) - - await component.async_add_entities(entities) - return True + return entities class InputBoolean(ToggleEntity, RestoreEntity): diff --git a/homeassistant/components/input_boolean/reproduce_state.py b/homeassistant/components/input_boolean/reproduce_state.py index b8bc18edfac..558d57ae862 100644 --- a/homeassistant/components/input_boolean/reproduce_state.py +++ b/homeassistant/components/input_boolean/reproduce_state.py @@ -4,11 +4,11 @@ import logging from typing import Iterable, Optional from homeassistant.const import ( + ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, - STATE_ON, STATE_OFF, - ATTR_ENTITY_ID, + STATE_ON, ) from homeassistant.core import Context, State from homeassistant.helpers.typing import HomeAssistantType diff --git a/homeassistant/components/input_boolean/services.yaml b/homeassistant/components/input_boolean/services.yaml index e49d46c9b86..e391c15d3a8 100644 --- a/homeassistant/components/input_boolean/services.yaml +++ b/homeassistant/components/input_boolean/services.yaml @@ -10,3 +10,6 @@ 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} +reload: + description: Reload the input_boolean configuration. + diff --git a/homeassistant/components/input_datetime/__init__.py b/homeassistant/components/input_datetime/__init__.py index 85fac4130a3..da684e03ddc 100644 --- a/homeassistant/components/input_datetime/__init__.py +++ b/homeassistant/components/input_datetime/__init__.py @@ -1,17 +1,22 @@ """Support to select a date and/or a time.""" -import logging import datetime +import logging import voluptuous as vol -from homeassistant.const import ATTR_DATE, ATTR_TIME, CONF_ICON, CONF_NAME +from homeassistant.const import ( + ATTR_DATE, + ATTR_TIME, + CONF_ICON, + CONF_NAME, + SERVICE_RELOAD, +) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity +import homeassistant.helpers.service from homeassistant.util import dt as dt_util - _LOGGER = logging.getLogger(__name__) DOMAIN = "input_datetime" @@ -27,14 +32,6 @@ ATTR_DATETIME = "datetime" SERVICE_SET_DATETIME = "set_datetime" -SERVICE_SET_DATETIME_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - { - vol.Optional(ATTR_DATE): cv.date, - vol.Optional(ATTR_TIME): cv.time, - vol.Optional(ATTR_DATETIME): cv.datetime, - } -) - def has_date_or_time(conf): """Check at least date or time is true.""" @@ -61,12 +58,74 @@ CONFIG_SCHEMA = vol.Schema( }, extra=vol.ALLOW_EXTRA, ) +RELOAD_SERVICE_SCHEMA = vol.Schema({}) async def async_setup(hass, config): """Set up an input datetime.""" component = EntityComponent(_LOGGER, DOMAIN, hass) + entities = await _async_process_config(config) + + async def reload_service_handler(service_call): + """Remove all entities and load new ones from config.""" + conf = await component.async_prepare_reload() + if conf is None: + return + new_entities = await _async_process_config(conf) + if new_entities: + await component.async_add_entities(new_entities) + + homeassistant.helpers.service.async_register_admin_service( + hass, + DOMAIN, + SERVICE_RELOAD, + reload_service_handler, + schema=RELOAD_SERVICE_SCHEMA, + ) + + async def async_set_datetime_service(entity, call): + """Handle a call to the input datetime 'set datetime' service.""" + time = call.data.get(ATTR_TIME) + date = call.data.get(ATTR_DATE) + dttm = call.data.get(ATTR_DATETIME) + if ( + dttm + and (date or time) + or entity.has_date + and not (date or dttm) + or entity.has_time + and not (time or dttm) + ): + _LOGGER.error( + "Invalid service data for %s input_datetime.set_datetime: %s", + entity.entity_id, + str(call.data), + ) + return + + if dttm: + date = dttm.date() + time = dttm.time() + entity.async_set_datetime(date, time) + + component.async_register_entity_service( + SERVICE_SET_DATETIME, + { + vol.Optional(ATTR_DATE): cv.date, + vol.Optional(ATTR_TIME): cv.time, + vol.Optional(ATTR_DATETIME): cv.datetime, + }, + async_set_datetime_service, + ) + + if entities: + await component.async_add_entities(entities) + return True + + +async def _async_process_config(config): + """Process config and create list of entities.""" entities = [] for object_id, cfg in config[DOMAIN].items(): @@ -79,41 +138,7 @@ async def async_setup(hass, config): InputDatetime(object_id, name, has_date, has_time, icon, initial) ) - if not entities: - return False - - async def async_set_datetime_service(entity, call): - """Handle a call to the input datetime 'set datetime' service.""" - time = call.data.get(ATTR_TIME) - date = call.data.get(ATTR_DATE) - dttm = call.data.get(ATTR_DATETIME) - # pylint: disable=too-many-boolean-expressions - if ( - dttm - and (date or time) - or entity.has_date - and not (date or dttm) - or entity.has_time - and not (time or dttm) - ): - _LOGGER.error( - "Invalid service data for %s " "input_datetime.set_datetime: %s", - entity.entity_id, - str(call.data), - ) - return - - if dttm: - date = dttm.date() - time = dttm.time() - entity.async_set_datetime(date, time) - - component.async_register_entity_service( - SERVICE_SET_DATETIME, SERVICE_SET_DATETIME_SCHEMA, async_set_datetime_service - ) - - await component.async_add_entities(entities) - return True + return entities class InputDatetime(RestoreEntity): diff --git a/homeassistant/components/input_datetime/services.yaml b/homeassistant/components/input_datetime/services.yaml index 8a40be47acd..472bd1b83b9 100644 --- a/homeassistant/components/input_datetime/services.yaml +++ b/homeassistant/components/input_datetime/services.yaml @@ -9,3 +9,6 @@ set_datetime: example: '"time": "05:30:00"'} datetime: {description: The target date & time the entity should be set to. Do not use with date or time., example: '"datetime": "2019-04-22 05:30:00"'} + +reload: + description: Reload the input_datetime configuration. diff --git a/homeassistant/components/input_number/__init__.py b/homeassistant/components/input_number/__init__.py index 9b4d5a961ba..a4438020886 100644 --- a/homeassistant/components/input_number/__init__.py +++ b/homeassistant/components/input_number/__init__.py @@ -3,17 +3,18 @@ import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, ATTR_MODE, + ATTR_UNIT_OF_MEASUREMENT, CONF_ICON, - CONF_NAME, CONF_MODE, + CONF_NAME, + SERVICE_RELOAD, ) +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 _LOGGER = logging.getLogger(__name__) @@ -38,10 +39,6 @@ SERVICE_SET_VALUE = "set_value" SERVICE_INCREMENT = "increment" SERVICE_DECREMENT = "decrement" -SERVICE_SET_VALUE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_VALUE): vol.Coerce(float)} -) - def _cv_input_number(cfg): """Configure validation helper for input number (voluptuous).""" @@ -82,12 +79,49 @@ CONFIG_SCHEMA = vol.Schema( required=True, extra=vol.ALLOW_EXTRA, ) +RELOAD_SERVICE_SCHEMA = vol.Schema({}) async def async_setup(hass, config): """Set up an input slider.""" component = EntityComponent(_LOGGER, DOMAIN, hass) + entities = await _async_process_config(config) + + async def reload_service_handler(service_call): + """Remove all entities and load new ones from config.""" + conf = await component.async_prepare_reload() + if conf is None: + return + new_entities = await _async_process_config(conf) + if new_entities: + await component.async_add_entities(new_entities) + + homeassistant.helpers.service.async_register_admin_service( + hass, + DOMAIN, + SERVICE_RELOAD, + reload_service_handler, + schema=RELOAD_SERVICE_SCHEMA, + ) + + component.async_register_entity_service( + SERVICE_SET_VALUE, + {vol.Required(ATTR_VALUE): vol.Coerce(float)}, + "async_set_value", + ) + + component.async_register_entity_service(SERVICE_INCREMENT, {}, "async_increment") + + component.async_register_entity_service(SERVICE_DECREMENT, {}, "async_decrement") + + if entities: + await component.async_add_entities(entities) + return True + + +async def _async_process_config(config): + """Process config and create list of entities.""" entities = [] for object_id, cfg in config[DOMAIN].items(): @@ -106,23 +140,7 @@ async def async_setup(hass, config): ) ) - if not entities: - return False - - component.async_register_entity_service( - SERVICE_SET_VALUE, SERVICE_SET_VALUE_SCHEMA, "async_set_value" - ) - - component.async_register_entity_service( - SERVICE_INCREMENT, ENTITY_SERVICE_SCHEMA, "async_increment" - ) - - component.async_register_entity_service( - SERVICE_DECREMENT, ENTITY_SERVICE_SCHEMA, "async_decrement" - ) - - await component.async_add_entities(entities) - return True + return entities class InputNumber(RestoreEntity): diff --git a/homeassistant/components/input_number/reproduce_state.py b/homeassistant/components/input_number/reproduce_state.py index 97a4837d371..22a91f74000 100644 --- a/homeassistant/components/input_number/reproduce_state.py +++ b/homeassistant/components/input_number/reproduce_state.py @@ -7,7 +7,7 @@ from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import Context, State from homeassistant.helpers.typing import HomeAssistantType -from . import DOMAIN, SERVICE_SET_VALUE, ATTR_VALUE +from . import ATTR_VALUE, DOMAIN, SERVICE_SET_VALUE _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/input_number/services.yaml b/homeassistant/components/input_number/services.yaml index 650abc056a9..9cd1b913ccd 100644 --- a/homeassistant/components/input_number/services.yaml +++ b/homeassistant/components/input_number/services.yaml @@ -14,3 +14,5 @@ set_value: entity_id: {description: Entity id of the input number to set the new value., example: input_number.threshold} value: {description: The target value the entity should be set to., example: 42} +reload: + description: Reload the input_number configuration. diff --git a/homeassistant/components/input_select/__init__.py b/homeassistant/components/input_select/__init__.py index 8cb3001c52e..b2b4b2083e8 100644 --- a/homeassistant/components/input_select/__init__.py +++ b/homeassistant/components/input_select/__init__.py @@ -3,11 +3,11 @@ import logging import voluptuous as vol -from homeassistant.const import CONF_ICON, CONF_NAME +from homeassistant.const import CONF_ICON, CONF_NAME, SERVICE_RELOAD import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity +import homeassistant.helpers.service _LOGGER = logging.getLogger(__name__) @@ -22,9 +22,6 @@ ATTR_OPTIONS = "options" SERVICE_SELECT_OPTION = "select_option" -SERVICE_SELECT_OPTION_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_OPTION): cv.string} -) SERVICE_SELECT_NEXT = "select_next" @@ -32,14 +29,6 @@ SERVICE_SELECT_PREVIOUS = "select_previous" SERVICE_SET_OPTIONS = "set_options" -SERVICE_SET_OPTIONS_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - { - vol.Required(ATTR_OPTIONS): vol.All( - cv.ensure_list, vol.Length(min=1), [cv.string] - ) - } -) - def _cv_input_select(cfg): """Configure validation helper for input select (voluptuous).""" @@ -73,12 +62,63 @@ CONFIG_SCHEMA = vol.Schema( required=True, extra=vol.ALLOW_EXTRA, ) +RELOAD_SERVICE_SCHEMA = vol.Schema({}) async def async_setup(hass, config): """Set up an input select.""" component = EntityComponent(_LOGGER, DOMAIN, hass) + entities = await _async_process_config(config) + + async def reload_service_handler(service_call): + """Remove all entities and load new ones from config.""" + conf = await component.async_prepare_reload() + if conf is None: + return + new_entities = await _async_process_config(conf) + if new_entities: + await component.async_add_entities(new_entities) + + homeassistant.helpers.service.async_register_admin_service( + hass, + DOMAIN, + SERVICE_RELOAD, + reload_service_handler, + schema=RELOAD_SERVICE_SCHEMA, + ) + + component.async_register_entity_service( + SERVICE_SELECT_OPTION, + {vol.Required(ATTR_OPTION): cv.string}, + "async_select_option", + ) + + component.async_register_entity_service( + SERVICE_SELECT_NEXT, {}, lambda entity, call: entity.async_offset_index(1), + ) + + component.async_register_entity_service( + SERVICE_SELECT_PREVIOUS, {}, lambda entity, call: entity.async_offset_index(-1), + ) + + component.async_register_entity_service( + SERVICE_SET_OPTIONS, + { + vol.Required(ATTR_OPTIONS): vol.All( + cv.ensure_list, vol.Length(min=1), [cv.string] + ) + }, + "async_set_options", + ) + + if entities: + await component.async_add_entities(entities) + return True + + +async def _async_process_config(config): + """Process config and create list of entities.""" entities = [] for object_id, cfg in config[DOMAIN].items(): @@ -88,31 +128,7 @@ async def async_setup(hass, config): icon = cfg.get(CONF_ICON) entities.append(InputSelect(object_id, name, initial, options, icon)) - if not entities: - return False - - component.async_register_entity_service( - SERVICE_SELECT_OPTION, SERVICE_SELECT_OPTION_SCHEMA, "async_select_option" - ) - - component.async_register_entity_service( - SERVICE_SELECT_NEXT, - ENTITY_SERVICE_SCHEMA, - lambda entity, call: entity.async_offset_index(1), - ) - - component.async_register_entity_service( - SERVICE_SELECT_PREVIOUS, - ENTITY_SERVICE_SCHEMA, - lambda entity, call: entity.async_offset_index(-1), - ) - - component.async_register_entity_service( - SERVICE_SET_OPTIONS, SERVICE_SET_OPTIONS_SCHEMA, "async_set_options" - ) - - await component.async_add_entities(entities) - return True + return entities class InputSelect(RestoreEntity): diff --git a/homeassistant/components/input_select/reproduce_state.py b/homeassistant/components/input_select/reproduce_state.py index 657f518cd3d..818510bee4a 100644 --- a/homeassistant/components/input_select/reproduce_state.py +++ b/homeassistant/components/input_select/reproduce_state.py @@ -9,11 +9,11 @@ from homeassistant.core import Context, State from homeassistant.helpers.typing import HomeAssistantType from . import ( + ATTR_OPTION, + ATTR_OPTIONS, DOMAIN, SERVICE_SELECT_OPTION, SERVICE_SET_OPTIONS, - ATTR_OPTION, - ATTR_OPTIONS, ) ATTR_GROUP = [ATTR_OPTION, ATTR_OPTIONS] diff --git a/homeassistant/components/input_select/services.yaml b/homeassistant/components/input_select/services.yaml index 8084e56b731..2cce496d0b6 100644 --- a/homeassistant/components/input_select/services.yaml +++ b/homeassistant/components/input_select/services.yaml @@ -20,3 +20,5 @@ set_options: for., example: input_select.my_select} options: {description: Options for the input select entity., example: '["Item A", "Item B", "Item C"]'} +reload: + description: Reload the input_select configuration. diff --git a/homeassistant/components/input_text/__init__.py b/homeassistant/components/input_text/__init__.py index 1b4670cf1e6..2049de7ab27 100644 --- a/homeassistant/components/input_text/__init__.py +++ b/homeassistant/components/input_text/__init__.py @@ -3,17 +3,18 @@ import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, ATTR_MODE, + ATTR_UNIT_OF_MEASUREMENT, CONF_ICON, - CONF_NAME, CONF_MODE, + CONF_NAME, + SERVICE_RELOAD, ) +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 _LOGGER = logging.getLogger(__name__) @@ -22,7 +23,9 @@ ENTITY_ID_FORMAT = DOMAIN + ".{}" CONF_INITIAL = "initial" CONF_MIN = "min" +CONF_MIN_VALUE = 0 CONF_MAX = "max" +CONF_MAX_VALUE = 100 MODE_TEXT = "text" MODE_PASSWORD = "password" @@ -34,10 +37,6 @@ ATTR_PATTERN = "pattern" SERVICE_SET_VALUE = "set_value" -SERVICE_SET_VALUE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_VALUE): cv.string} -) - def _cv_input_text(cfg): """Configure validation helper for input box (voluptuous).""" @@ -62,8 +61,8 @@ CONFIG_SCHEMA = vol.Schema( vol.All( { vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_MIN, default=0): vol.Coerce(int), - vol.Optional(CONF_MAX, default=100): vol.Coerce(int), + vol.Optional(CONF_MIN, default=CONF_MIN_VALUE): vol.Coerce(int), + vol.Optional(CONF_MAX, default=CONF_MAX_VALUE): vol.Coerce(int), vol.Optional(CONF_INITIAL, ""): cv.string, vol.Optional(CONF_ICON): cv.icon, vol.Optional(ATTR_UNIT_OF_MEASUREMENT): cv.string, @@ -81,20 +80,51 @@ CONFIG_SCHEMA = vol.Schema( required=True, extra=vol.ALLOW_EXTRA, ) +RELOAD_SERVICE_SCHEMA = vol.Schema({}) async def async_setup(hass, config): """Set up an input text box.""" component = EntityComponent(_LOGGER, DOMAIN, hass) + entities = await _async_process_config(config) + + async def reload_service_handler(service_call): + """Remove all entities and load new ones from config.""" + conf = await component.async_prepare_reload() + if conf is None: + return + new_entities = await _async_process_config(conf) + if new_entities: + await component.async_add_entities(new_entities) + + homeassistant.helpers.service.async_register_admin_service( + hass, + DOMAIN, + SERVICE_RELOAD, + reload_service_handler, + schema=RELOAD_SERVICE_SCHEMA, + ) + + component.async_register_entity_service( + SERVICE_SET_VALUE, {vol.Required(ATTR_VALUE): cv.string}, "async_set_value" + ) + + if entities: + await component.async_add_entities(entities) + return True + + +async def _async_process_config(config): + """Process config and create list of entities.""" entities = [] for object_id, cfg in config[DOMAIN].items(): if cfg is None: cfg = {} name = cfg.get(CONF_NAME) - minimum = cfg.get(CONF_MIN) - maximum = cfg.get(CONF_MAX) + minimum = cfg.get(CONF_MIN, CONF_MIN_VALUE) + maximum = cfg.get(CONF_MAX, CONF_MAX_VALUE) initial = cfg.get(CONF_INITIAL) icon = cfg.get(CONF_ICON) unit = cfg.get(ATTR_UNIT_OF_MEASUREMENT) @@ -107,15 +137,7 @@ async def async_setup(hass, config): ) ) - if not entities: - return False - - component.async_register_entity_service( - SERVICE_SET_VALUE, SERVICE_SET_VALUE_SCHEMA, "async_set_value" - ) - - await component.async_add_entities(entities) - return True + return entities class InputText(RestoreEntity): diff --git a/homeassistant/components/input_text/reproduce_state.py b/homeassistant/components/input_text/reproduce_state.py index f64c5c019f6..28a2f27ee84 100644 --- a/homeassistant/components/input_text/reproduce_state.py +++ b/homeassistant/components/input_text/reproduce_state.py @@ -7,7 +7,7 @@ from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import Context, State from homeassistant.helpers.typing import HomeAssistantType -from . import DOMAIN, SERVICE_SET_VALUE, ATTR_VALUE +from . import ATTR_VALUE, DOMAIN, SERVICE_SET_VALUE _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/input_text/services.yaml b/homeassistant/components/input_text/services.yaml index 219eecf2fd6..e9b709b0c03 100644 --- a/homeassistant/components/input_text/services.yaml +++ b/homeassistant/components/input_text/services.yaml @@ -4,3 +4,5 @@ set_value: entity_id: {description: Entity id of the input text to set the new value., example: input_text.text1} value: {description: The target value the entity should be set to., example: This is an example text} +reload: + description: Reload the input_text configuration. diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index 236a996794a..560a7cbd33c 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -1,22 +1,21 @@ """Numeric integration of data coming from a source sensor over time.""" +from decimal import Decimal, DecimalException import logging -from decimal import Decimal, DecimalException import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, ATTR_UNIT_OF_MEASUREMENT, - STATE_UNKNOWN, + CONF_NAME, STATE_UNAVAILABLE, + STATE_UNKNOWN, ) from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.restore_state import RestoreEntity - # mypy: allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/intent/__init__.py b/homeassistant/components/intent/__init__.py new file mode 100644 index 00000000000..bdf612b2e83 --- /dev/null +++ b/homeassistant/components/intent/__init__.py @@ -0,0 +1,67 @@ +"""The Intent integration.""" +import logging + +import voluptuous as vol + +from homeassistant.components import http +from homeassistant.components.http.data_validator import RequestDataValidator +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_validation as cv, integration_platform, intent + +from .const import DOMAIN + +CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the Intent component.""" + hass.http.register_view(IntentHandleView()) + + await integration_platform.async_process_integration_platforms( + hass, DOMAIN, _async_process_intent + ) + + return True + + +async def _async_process_intent(hass: HomeAssistant, domain: str, platform): + """Process the intents of an integration.""" + await platform.async_setup_intents(hass) + + +class IntentHandleView(http.HomeAssistantView): + """View to handle intents from JSON.""" + + url = "/api/intent/handle" + name = "api:intent:handle" + + @RequestDataValidator( + vol.Schema( + { + vol.Required("name"): cv.string, + vol.Optional("data"): vol.Schema({cv.string: object}), + } + ) + ) + async def post(self, request, data): + """Handle intent with name/data.""" + hass = request.app["hass"] + + try: + intent_name = data["name"] + slots = { + key: {"value": value} for key, value in data.get("data", {}).items() + } + intent_result = await intent.async_handle( + hass, DOMAIN, intent_name, slots, "", self.context(request) + ) + except intent.IntentHandleError as err: + intent_result = intent.IntentResponse() + intent_result.async_set_speech(str(err)) + + if intent_result is None: + intent_result = intent.IntentResponse() + intent_result.async_set_speech("Sorry, I couldn't handle that") + + return self.json(intent_result) diff --git a/homeassistant/components/intent/const.py b/homeassistant/components/intent/const.py new file mode 100644 index 00000000000..61b97c20537 --- /dev/null +++ b/homeassistant/components/intent/const.py @@ -0,0 +1,3 @@ +"""Constants for the Intent integration.""" + +DOMAIN = "intent" diff --git a/homeassistant/components/intent/manifest.json b/homeassistant/components/intent/manifest.json new file mode 100644 index 00000000000..005abde47d6 --- /dev/null +++ b/homeassistant/components/intent/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "intent", + "name": "Intent", + "config_flow": false, + "documentation": "https://www.home-assistant.io/integrations/intent", + "requirements": [], + "ssdp": [], + "homekit": {}, + "dependencies": ["http"], + "codeowners": ["@home-assistant/core"] +} diff --git a/homeassistant/components/intent_script/__init__.py b/homeassistant/components/intent_script/__init__.py index 75a0c0e8f97..38f93ed3506 100644 --- a/homeassistant/components/intent_script/__init__.py +++ b/homeassistant/components/intent_script/__init__.py @@ -4,7 +4,7 @@ import logging import voluptuous as vol -from homeassistant.helpers import intent, template, script, config_validation as cv +from homeassistant.helpers import config_validation as cv, intent, script, template DOMAIN = "intent_script" @@ -80,7 +80,9 @@ class ScriptIntentHandler(intent.IntentHandler): if action is not None: if is_async_action: - intent_obj.hass.async_create_task(action.async_run(slots)) + intent_obj.hass.async_create_task( + action.async_run(slots, intent_obj.context) + ) else: await action.async_run(slots) diff --git a/homeassistant/components/intesishome/__init__.py b/homeassistant/components/intesishome/__init__.py new file mode 100644 index 00000000000..fd4ae1f03e3 --- /dev/null +++ b/homeassistant/components/intesishome/__init__.py @@ -0,0 +1 @@ +"""Intesishome platform.""" diff --git a/homeassistant/components/intesishome/climate.py b/homeassistant/components/intesishome/climate.py new file mode 100644 index 00000000000..669d1155d80 --- /dev/null +++ b/homeassistant/components/intesishome/climate.py @@ -0,0 +1,407 @@ +"""Support for IntesisHome and airconwithme Smart AC Controllers.""" +import logging +from random import randrange + +from pyintesishome import IHAuthenticationError, IHConnectionError, IntesisHome +import voluptuous as vol + +from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice +from homeassistant.components.climate.const import ( + ATTR_HVAC_MODE, + HVAC_MODE_COOL, + HVAC_MODE_DRY, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, + SUPPORT_FAN_MODE, + SUPPORT_SWING_MODE, + SUPPORT_TARGET_TEMPERATURE, + SWING_BOTH, + SWING_HORIZONTAL, + SWING_OFF, + SWING_VERTICAL, +) +from homeassistant.const import ( + ATTR_TEMPERATURE, + CONF_DEVICE, + CONF_PASSWORD, + CONF_USERNAME, + TEMP_CELSIUS, +) +from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import async_call_later + +_LOGGER = logging.getLogger(__name__) + +IH_DEVICE_INTESISHOME = "IntesisHome" +IH_DEVICE_AIRCONWITHME = "airconwithme" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_DEVICE, default=IH_DEVICE_INTESISHOME): vol.In( + [IH_DEVICE_AIRCONWITHME, IH_DEVICE_INTESISHOME] + ), + } +) + +MAP_IH_TO_HVAC_MODE = { + "auto": HVAC_MODE_HEAT_COOL, + "cool": HVAC_MODE_COOL, + "dry": HVAC_MODE_DRY, + "fan": HVAC_MODE_FAN_ONLY, + "heat": HVAC_MODE_HEAT, + "off": HVAC_MODE_OFF, +} + +MAP_HVAC_MODE_TO_IH = {v: k for k, v in MAP_IH_TO_HVAC_MODE.items()} + +MAP_STATE_ICONS = { + HVAC_MODE_COOL: "mdi:snowflake", + HVAC_MODE_DRY: "mdi:water-off", + HVAC_MODE_FAN_ONLY: "mdi:fan", + HVAC_MODE_HEAT: "mdi:white-balance-sunny", + HVAC_MODE_HEAT_COOL: "mdi:cached", +} + +IH_HVAC_MODES = [ + HVAC_MODE_HEAT_COOL, + HVAC_MODE_COOL, + HVAC_MODE_HEAT, + HVAC_MODE_DRY, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_OFF, +] + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Create the IntesisHome climate devices.""" + ih_user = config[CONF_USERNAME] + ih_pass = config[CONF_PASSWORD] + device_type = config[CONF_DEVICE] + + controller = IntesisHome( + ih_user, + ih_pass, + hass.loop, + websession=async_get_clientsession(hass), + device_type=device_type, + ) + try: + await controller.poll_status() + except IHAuthenticationError: + _LOGGER.error("Invalid username or password") + return + except IHConnectionError: + _LOGGER.error("Error connecting to the %s server", device_type) + raise PlatformNotReady + + ih_devices = controller.get_devices() + if ih_devices: + async_add_entities( + [ + IntesisAC(ih_device_id, device, controller) + for ih_device_id, device in ih_devices.items() + ], + True, + ) + else: + _LOGGER.error( + "Error getting device list from %s API: %s", + device_type, + controller.error_message, + ) + await controller.stop() + + +class IntesisAC(ClimateDevice): + """Represents an Intesishome air conditioning device.""" + + def __init__(self, ih_device_id, ih_device, controller): + """Initialize the thermostat.""" + self._controller = controller + self._device_id = ih_device_id + self._ih_device = ih_device + self._device_name = ih_device.get("name") + self._device_type = controller.device_type + self._connected = None + self._setpoint_step = 1 + self._current_temp = None + self._max_temp = None + self._min_temp = None + self._target_temp = None + self._outdoor_temp = None + self._run_hours = None + self._rssi = None + self._swing = None + self._swing_list = None + self._vvane = None + self._hvane = None + self._power = False + self._fan_speed = None + self._hvac_mode = None + self._fan_modes = controller.get_fan_speed_list(ih_device_id) + self._support = SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE + self._swing_list = [SWING_OFF] + + if ih_device.get("config_vertical_vanes"): + self._swing_list.append(SWING_VERTICAL) + if ih_device.get("config_horizontal_vanes"): + self._swing_list.append(SWING_HORIZONTAL) + if len(self._swing_list) == 3: + self._swing_list.append(SWING_BOTH) + self._support |= SUPPORT_SWING_MODE + elif len(self._swing_list) == 2: + self._support |= SUPPORT_SWING_MODE + + async def async_added_to_hass(self): + """Subscribe to event updates.""" + _LOGGER.debug("Added climate device with state: %s", repr(self._ih_device)) + await self._controller.add_update_callback(self.async_update_callback) + try: + await self._controller.connect() + except IHConnectionError as ex: + _LOGGER.error("Exception connecting to IntesisHome: %s", ex) + + @property + def name(self): + """Return the name of the AC device.""" + return self._device_name + + @property + def temperature_unit(self): + """Intesishome API uses celsius on the backend.""" + return TEMP_CELSIUS + + @property + def device_state_attributes(self): + """Return the device specific state attributes.""" + attrs = {} + if len(self._swing_list) > 1: + attrs["vertical_vane"] = self._vvane + attrs["horizontal_vane"] = self._hvane + if self._outdoor_temp: + attrs["outdoor_temp"] = self._outdoor_temp + return attrs + + @property + def unique_id(self): + """Return unique ID for this device.""" + return self._device_id + + @property + def target_temperature_step(self) -> float: + """Return whether setpoint should be whole or half degree precision.""" + return self._setpoint_step + + async def async_set_temperature(self, **kwargs): + """Set new target temperature.""" + temperature = kwargs.get(ATTR_TEMPERATURE) + hvac_mode = kwargs.get(ATTR_HVAC_MODE) + + if hvac_mode: + await self.async_set_hvac_mode(hvac_mode) + + if temperature: + _LOGGER.debug("Setting %s to %s degrees", self._device_type, temperature) + await self._controller.set_temperature(self._device_id, temperature) + self._target_temp = temperature + + # Write updated temperature to HA state to avoid flapping (API confirmation is slow) + self.async_write_ha_state() + + async def async_set_hvac_mode(self, hvac_mode): + """Set operation mode.""" + _LOGGER.debug("Setting %s to %s mode", self._device_type, hvac_mode) + if hvac_mode == HVAC_MODE_OFF: + self._power = False + await self._controller.set_power_off(self._device_id) + # Write changes to HA, API can be slow to push changes + self.async_write_ha_state() + return + + # First check device is turned on + if not self._controller.is_on(self._device_id): + self._power = True + await self._controller.set_power_on(self._device_id) + + # Set the mode + await self._controller.set_mode(self._device_id, MAP_HVAC_MODE_TO_IH[hvac_mode]) + + # Send the temperature again in case changing modes has changed it + if self._target_temp: + await self._controller.set_temperature(self._device_id, self._target_temp) + + # Updates can take longer than 2 seconds, so update locally + self._hvac_mode = hvac_mode + self.async_write_ha_state() + + async def async_set_fan_mode(self, fan_mode): + """Set fan mode (from quiet, low, medium, high, auto).""" + await self._controller.set_fan_speed(self._device_id, fan_mode) + + # Updates can take longer than 2 seconds, so update locally + self._fan_speed = fan_mode + self.async_write_ha_state() + + async def async_set_swing_mode(self, swing_mode): + """Set the vertical vane.""" + if swing_mode == SWING_OFF: + await self._controller.set_vertical_vane(self._device_id, "auto/stop") + await self._controller.set_horizontal_vane(self._device_id, "auto/stop") + elif swing_mode == SWING_BOTH: + await self._controller.set_vertical_vane(self._device_id, "swing") + await self._controller.set_horizontal_vane(self._device_id, "swing") + elif swing_mode == SWING_HORIZONTAL: + await self._controller.set_vertical_vane(self._device_id, "auto/stop") + await self._controller.set_horizontal_vane(self._device_id, "swing") + elif swing_mode == SWING_VERTICAL: + await self._controller.set_vertical_vane(self._device_id, "swing") + await self._controller.set_horizontal_vane(self._device_id, "auto/stop") + self._swing = swing_mode + + async def async_update(self): + """Copy values from controller dictionary to climate device.""" + # Update values from controller's device dictionary + self._connected = self._controller.is_connected + self._current_temp = self._controller.get_temperature(self._device_id) + self._fan_speed = self._controller.get_fan_speed(self._device_id) + self._power = self._controller.is_on(self._device_id) + self._min_temp = self._controller.get_min_setpoint(self._device_id) + self._max_temp = self._controller.get_max_setpoint(self._device_id) + self._rssi = self._controller.get_rssi(self._device_id) + self._run_hours = self._controller.get_run_hours(self._device_id) + self._target_temp = self._controller.get_setpoint(self._device_id) + self._outdoor_temp = self._controller.get_outdoor_temperature(self._device_id) + + # Operation mode + mode = self._controller.get_mode(self._device_id) + self._hvac_mode = MAP_IH_TO_HVAC_MODE.get(mode) + + # Swing mode + # Climate module only supports one swing setting. + self._vvane = self._controller.get_vertical_swing(self._device_id) + self._hvane = self._controller.get_horizontal_swing(self._device_id) + + if self._vvane == "swing" and self._hvane == "swing": + self._swing = SWING_BOTH + elif self._vvane == "swing": + self._swing = SWING_VERTICAL + elif self._hvane == "swing": + self._swing = SWING_HORIZONTAL + else: + self._swing = SWING_OFF + + async def async_will_remove_from_hass(self): + """Shutdown the controller when the device is being removed.""" + await self._controller.stop() + + @property + def icon(self): + """Return the icon for the current state.""" + icon = None + if self._power: + icon = MAP_STATE_ICONS.get(self._hvac_mode) + return icon + + async def async_update_callback(self, device_id=None): + """Let HA know there has been an update from the controller.""" + # Track changes in connection state + if not self._controller.is_connected and self._connected: + # Connection has dropped + self._connected = False + reconnect_minutes = 1 + randrange(10) + _LOGGER.error( + "Connection to %s API was lost. Reconnecting in %i minutes", + self._device_type, + reconnect_minutes, + ) + # Schedule reconnection + async_call_later( + self.hass, reconnect_minutes * 60, self._controller.connect() + ) + + if self._controller.is_connected and not self._connected: + # Connection has been restored + self._connected = True + _LOGGER.debug("Connection to %s API was restored", self._device_type) + + if not device_id or self._device_id == device_id: + # Update all devices if no device_id was specified + _LOGGER.debug( + "%s API sent a status update for device %s", + self._device_type, + device_id, + ) + self.async_schedule_update_ha_state(True) + + @property + def min_temp(self): + """Return the minimum temperature for the current mode of operation.""" + return self._min_temp + + @property + def max_temp(self): + """Return the maximum temperature for the current mode of operation.""" + return self._max_temp + + @property + def should_poll(self): + """Poll for updates if pyIntesisHome doesn't have a socket open.""" + return False + + @property + def hvac_modes(self): + """List of available operation modes.""" + return IH_HVAC_MODES + + @property + def fan_mode(self): + """Return whether the fan is on.""" + return self._fan_speed + + @property + def swing_mode(self): + """Return current swing mode.""" + return self._swing + + @property + def fan_modes(self): + """List of available fan modes.""" + return self._fan_modes + + @property + def swing_modes(self): + """List of available swing positions.""" + return self._swing_list + + @property + def available(self) -> bool: + """If the device hasn't been able to connect, mark as unavailable.""" + return self._connected or self._connected is None + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._current_temp + + @property + def hvac_mode(self): + """Return the current mode of operation if unit is on.""" + if self._power: + return self._hvac_mode + return HVAC_MODE_OFF + + @property + def target_temperature(self): + """Return the current setpoint temperature if unit is on.""" + return self._target_temp + + @property + def supported_features(self): + """Return the list of supported features.""" + return self._support diff --git a/homeassistant/components/intesishome/manifest.json b/homeassistant/components/intesishome/manifest.json new file mode 100644 index 00000000000..025d08ac548 --- /dev/null +++ b/homeassistant/components/intesishome/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "intesishome", + "name": "IntesisHome", + "documentation": "https://www.home-assistant.io/integrations/intesishome", + "dependencies": [], + "codeowners": ["@jnimmo"], + "requirements": ["pyintesishome==1.5"] +} diff --git a/homeassistant/components/ios/.translations/ko.json b/homeassistant/components/ios/.translations/ko.json index 1496dab0555..283594a45b5 100644 --- a/homeassistant/components/ios/.translations/ko.json +++ b/homeassistant/components/ios/.translations/ko.json @@ -5,7 +5,7 @@ }, "step": { "confirm": { - "description": "Home Assistant iOS \ucef4\ud3ec\ub10c\ud2b8\ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "Home Assistant iOS \ucef4\ud3ec\ub10c\ud2b8\ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "Home Assistant iOS" } }, diff --git a/homeassistant/components/ios/config_flow.py b/homeassistant/components/ios/config_flow.py index 511e350aae3..9eaca389ba1 100644 --- a/homeassistant/components/ios/config_flow.py +++ b/homeassistant/components/ios/config_flow.py @@ -1,8 +1,8 @@ """Config flow for iOS.""" -from homeassistant.helpers import config_entry_flow from homeassistant import config_entries -from .const import DOMAIN +from homeassistant.helpers import config_entry_flow +from .const import DOMAIN config_entry_flow.register_discovery_flow( DOMAIN, "Home Assistant iOS", lambda *_: True, config_entries.CONN_CLASS_CLOUD_PUSH diff --git a/homeassistant/components/ios/notify.py b/homeassistant/components/ios/notify.py index ee74b369629..63ed6a6ee26 100644 --- a/homeassistant/components/ios/notify.py +++ b/homeassistant/components/ios/notify.py @@ -48,12 +48,6 @@ def get_service(hass, config, discovery_info=None): hass.config.components.add("notify.ios") if not ios.devices_with_push(hass): - _LOGGER.error( - "The notify.ios platform was loaded but no " - "devices exist! Please check the documentation at " - "https://home-assistant.io/ecosystem/ios/notifications" - "/ for more information" - ) return None return iOSNotificationService() @@ -98,9 +92,9 @@ class iOSNotificationService(BaseNotificationService): if req.status_code != 201: fallback_error = req.json().get("errorMessage", "Unknown error") - fallback_message = ( - "Internal server error, " "please try again later: " "{}" - ).format(fallback_error) + fallback_message = "Internal server error, please try again later: {}".format( + fallback_error + ) message = req.json().get("message", fallback_message) if req.status_code == 429: _LOGGER.warning(message) diff --git a/homeassistant/components/iota/__init__.py b/homeassistant/components/iota/__init__.py index 900dd028e6a..497e94a08d6 100644 --- a/homeassistant/components/iota/__init__.py +++ b/homeassistant/components/iota/__init__.py @@ -1,7 +1,8 @@ """Support for IOTA wallets.""" -import logging from datetime import timedelta +import logging +from iota import Iota import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -77,6 +78,5 @@ class IotaDevice(Entity): @property def api(self): """Construct API object for interaction with the IRI node.""" - from iota import Iota return Iota(adapter=self.iri, seed=self._seed) diff --git a/homeassistant/components/iperf3/__init__.py b/homeassistant/components/iperf3/__init__.py index 753ea60efa4..9272a725bb7 100644 --- a/homeassistant/components/iperf3/__init__.py +++ b/homeassistant/components/iperf3/__init__.py @@ -85,17 +85,7 @@ async def async_setup(hass, config): conf = config[DOMAIN] for host in conf[CONF_HOSTS]: - host_name = host[CONF_HOST] - - client = iperf3.Client() - client.duration = host[CONF_DURATION] - client.server_hostname = host_name - client.port = host[CONF_PORT] - client.num_streams = host[CONF_PARALLEL] - client.protocol = host[CONF_PROTOCOL] - client.verbose = False - - data = hass.data[DOMAIN][host_name] = Iperf3Data(hass, client) + data = hass.data[DOMAIN][host[CONF_HOST]] = Iperf3Data(hass, host) if not conf[CONF_MANUAL]: async_track_time_interval(hass, data.update, conf[CONF_SCAN_INTERVAL]) @@ -123,26 +113,37 @@ async def async_setup(hass, config): class Iperf3Data: """Get the latest data from iperf3.""" - def __init__(self, hass, client): + def __init__(self, hass, host): """Initialize the data object.""" self._hass = hass - self._client = client + self._host = host self.data = {ATTR_DOWNLOAD: None, ATTR_UPLOAD: None, ATTR_VERSION: None} + def create_client(self): + """Create a new iperf3 client to use for measurement.""" + client = iperf3.Client() + client.duration = self._host[CONF_DURATION] + client.server_hostname = self._host[CONF_HOST] + client.port = self._host[CONF_PORT] + client.num_streams = self._host[CONF_PARALLEL] + client.protocol = self._host[CONF_PROTOCOL] + client.verbose = False + return client + @property def protocol(self): """Return the protocol used for this connection.""" - return self._client.protocol + return self._host[CONF_PROTOCOL] @property def host(self): """Return the host connected to.""" - return self._client.server_hostname + return self._host[CONF_HOST] @property def port(self): """Return the port on the host connected to.""" - return self._client.port + return self._host[CONF_PORT] def update(self, now=None): """Get the latest data from iperf3.""" @@ -165,9 +166,10 @@ class Iperf3Data: def _run_test(self, test_type): """Run and return the iperf3 data.""" - self._client.reverse = test_type == ATTR_DOWNLOAD + client = self.create_client() + client.reverse = test_type == ATTR_DOWNLOAD try: - result = self._client.run() + result = client.run() except (AttributeError, OSError, ValueError) as error: _LOGGER.error("Iperf3 error: %s", error) return None diff --git a/homeassistant/components/iperf3/manifest.json b/homeassistant/components/iperf3/manifest.json index c3b1e27c77a..6b7cadfd5de 100644 --- a/homeassistant/components/iperf3/manifest.json +++ b/homeassistant/components/iperf3/manifest.json @@ -6,5 +6,7 @@ "iperf3==0.1.11" ], "dependencies": [], - "codeowners": [] + "codeowners": [ + "@rohankapoorcom" + ] } diff --git a/homeassistant/components/ipma/.translations/da.json b/homeassistant/components/ipma/.translations/da.json index 080c41429ba..017aff4d0ec 100644 --- a/homeassistant/components/ipma/.translations/da.json +++ b/homeassistant/components/ipma/.translations/da.json @@ -11,7 +11,7 @@ "name": "Navn" }, "description": "Instituto Portugu\u00eas do Mar e Atmosfera", - "title": "Beliggenhed" + "title": "Lokalitet" } }, "title": "Portugisisk vejrservice (IPMA)" diff --git a/homeassistant/components/ipma/__init__.py b/homeassistant/components/ipma/__init__.py index 702e12a8a63..a00941624f5 100644 --- a/homeassistant/components/ipma/__init__.py +++ b/homeassistant/components/ipma/__init__.py @@ -1,5 +1,6 @@ """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 diff --git a/homeassistant/components/ipma/weather.py b/homeassistant/components/ipma/weather.py index 9f1836c7389..c088d76d165 100644 --- a/homeassistant/components/ipma/weather.py +++ b/homeassistant/components/ipma/weather.py @@ -1,22 +1,23 @@ """Support for IPMA weather service.""" -import logging from datetime import timedelta +import logging import async_timeout +from pyipma import Station import voluptuous as vol from homeassistant.components.weather import ( - WeatherEntity, - PLATFORM_SCHEMA, ATTR_FORECAST_CONDITION, ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, + PLATFORM_SCHEMA, + WeatherEntity, ) -from homeassistant.const import CONF_NAME, TEMP_CELSIUS, CONF_LATITUDE, CONF_LONGITUDE -from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_CELSIUS from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -84,7 +85,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async def async_get_station(hass, latitude, longitude): """Retrieve weather station, station name to be used as the entity name.""" - from pyipma import Station websession = async_get_clientsession(hass) with async_timeout.timeout(10): diff --git a/homeassistant/components/iqvia/__init__.py b/homeassistant/components/iqvia/__init__.py index e3add21c3a4..397cffe6d8c 100644 --- a/homeassistant/components/iqvia/__init__.py +++ b/homeassistant/components/iqvia/__init__.py @@ -4,8 +4,7 @@ from datetime import timedelta import logging from pyiqvia import Client -from pyiqvia.errors import IQVIAError, InvalidZipError - +from pyiqvia.errors import InvalidZipError, IQVIAError import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT diff --git a/homeassistant/components/iqvia/config_flow.py b/homeassistant/components/iqvia/config_flow.py index f6776a833ee..abec8eff09a 100644 --- a/homeassistant/components/iqvia/config_flow.py +++ b/homeassistant/components/iqvia/config_flow.py @@ -1,10 +1,10 @@ """Config flow to configure the IQVIA component.""" from collections import OrderedDict -import voluptuous as vol from pyiqvia import Client from pyiqvia.errors import InvalidZipError +import voluptuous as vol from homeassistant import config_entries from homeassistant.core import callback diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json index 04723a6a1f6..7a5eb7e56df 100644 --- a/homeassistant/components/iqvia/manifest.json +++ b/homeassistant/components/iqvia/manifest.json @@ -3,12 +3,7 @@ "name": "IQVIA", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/iqvia", - "requirements": [ - "numpy==1.17.3", - "pyiqvia==0.2.1" - ], + "requirements": ["numpy==1.17.4", "pyiqvia==0.2.1"], "dependencies": [], - "codeowners": [ - "@bachya" - ] -} \ No newline at end of file + "codeowners": ["@bachya"] +} diff --git a/homeassistant/components/iqvia/sensor.py b/homeassistant/components/iqvia/sensor.py index 90aa89f06d1..21c31bbff08 100644 --- a/homeassistant/components/iqvia/sensor.py +++ b/homeassistant/components/iqvia/sensor.py @@ -9,8 +9,8 @@ from homeassistant.components.iqvia import ( DOMAIN, SENSORS, TYPE_ALLERGY_FORECAST, - TYPE_ALLERGY_OUTLOOK, TYPE_ALLERGY_INDEX, + TYPE_ALLERGY_OUTLOOK, TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW, TYPE_ASTHMA_FORECAST, diff --git a/homeassistant/components/irish_rail_transport/sensor.py b/homeassistant/components/irish_rail_transport/sensor.py index 6f2bbae5eba..883f4ed7b39 100644 --- a/homeassistant/components/irish_rail_transport/sensor.py +++ b/homeassistant/components/irish_rail_transport/sensor.py @@ -1,12 +1,13 @@ """Support for Irish Rail RTPI information.""" -import logging from datetime import timedelta +import logging +from pyirishrail.pyirishrail import IrishRailRTPI import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION +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__) @@ -47,7 +48,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Irish Rail transport sensor.""" - from pyirishrail.pyirishrail import IrishRailRTPI station = config.get(CONF_STATION) direction = config.get(CONF_DIRECTION) diff --git a/homeassistant/components/islamic_prayer_times/sensor.py b/homeassistant/components/islamic_prayer_times/sensor.py index 88cbd2cb431..3f7de535407 100644 --- a/homeassistant/components/islamic_prayer_times/sensor.py +++ b/homeassistant/components/islamic_prayer_times/sensor.py @@ -1,15 +1,16 @@ """Platform to retrieve Islamic prayer times information for Home Assistant.""" -import logging from datetime import datetime, timedelta +import logging +from prayer_times_calculator import PrayerTimesCalculator import voluptuous as vol -import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt_util from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import DEVICE_CLASS_TIMESTAMP +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_point_in_time +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -148,7 +149,6 @@ class IslamicPrayerTimesData: def get_new_prayer_times(self): """Fetch prayer times for today.""" - from prayer_times_calculator import PrayerTimesCalculator today = datetime.today().strftime("%Y-%m-%d") diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index 96796e37a6a..6c5a668c51a 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -62,7 +62,7 @@ NODE_FILTERS = { "binary_sensor": { "uom": [], "states": [], - "node_def_id": ["BinaryAlarm"], + "node_def_id": ["BinaryAlarm", "BinaryAlarm_ADV"], "insteon_type": ["16."], # Does a startswith() match; include the dot }, "sensor": { @@ -112,6 +112,8 @@ NODE_FILTERS = { "BallastRelayLampSwitch_ADV", "RemoteLinc2", "RemoteLinc2_ADV", + "KeypadDimmer", + "KeypadDimmer_ADV", ], "insteon_type": ["1."], }, diff --git a/homeassistant/components/isy994/binary_sensor.py b/homeassistant/components/isy994/binary_sensor.py index 0c4fb80be8c..9cf1332c4f4 100644 --- a/homeassistant/components/isy994/binary_sensor.py +++ b/homeassistant/components/isy994/binary_sensor.py @@ -110,7 +110,6 @@ class ISYBinarySensorDevice(ISYDevice, BinarySensorDevice): self._negative_node = None self._heartbeat_device = None self._device_class_from_type = _detect_device_type(self._node) - # pylint: disable=protected-access if _is_val_unknown(self._node.status._val): self._computed_state = None self._status_was_unknown = True @@ -166,7 +165,7 @@ class ISYBinarySensorDevice(ISYDevice, BinarySensorDevice): """Handle an "On" control event from the "negative" node.""" if event == "DON": _LOGGER.debug( - "Sensor %s turning Off via the Negative node " "sending a DON command", + "Sensor %s turning Off via the Negative node sending a DON command", self.name, ) self._computed_state = False @@ -182,7 +181,7 @@ class ISYBinarySensorDevice(ISYDevice, BinarySensorDevice): """ if event == "DON": _LOGGER.debug( - "Sensor %s turning On via the Primary node " "sending a DON command", + "Sensor %s turning On via the Primary node sending a DON command", self.name, ) self._computed_state = True @@ -190,7 +189,7 @@ class ISYBinarySensorDevice(ISYDevice, BinarySensorDevice): self._heartbeat() if event == "DOF": _LOGGER.debug( - "Sensor %s turning Off via the Primary node " "sending a DOF command", + "Sensor %s turning Off via the Primary node sending a DOF command", self.name, ) self._computed_state = False diff --git a/homeassistant/components/itunes/media_player.py b/homeassistant/components/itunes/media_player.py index aebe16ffa26..112a9c609d8 100644 --- a/homeassistant/components/itunes/media_player.py +++ b/homeassistant/components/itunes/media_player.py @@ -4,7 +4,7 @@ import logging import requests import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, @@ -14,11 +14,11 @@ from homeassistant.components.media_player.const import ( SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, + SUPPORT_SHUFFLE_SET, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - SUPPORT_SHUFFLE_SET, ) from homeassistant.const import ( CONF_HOST, diff --git a/homeassistant/components/izone/.translations/ko.json b/homeassistant/components/izone/.translations/ko.json index 69b8ce8a31e..91593b26511 100644 --- a/homeassistant/components/izone/.translations/ko.json +++ b/homeassistant/components/izone/.translations/ko.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "iZone \uc744 \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "iZone \uc744 \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "iZone" } }, diff --git a/homeassistant/components/izone/__init__.py b/homeassistant/components/izone/__init__.py index 6fecbc1f3a6..0e5dcddbc48 100644 --- a/homeassistant/components/izone/__init__.py +++ b/homeassistant/components/izone/__init__.py @@ -13,7 +13,7 @@ from homeassistant.const import CONF_EXCLUDE import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from .const import IZONE, DATA_CONFIG +from .const import DATA_CONFIG, IZONE from .discovery import async_start_discovery_service, async_stop_discovery_service _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/izone/climate.py b/homeassistant/components/izone/climate.py index c932c66627b..b80dfc2542f 100644 --- a/homeassistant/components/izone/climate.py +++ b/homeassistant/components/izone/climate.py @@ -1,22 +1,21 @@ """Support for the iZone HVAC.""" import logging -from typing import Optional, List +from typing import List, Optional -from pizone import Zone, Controller +from pizone import Controller, Zone -from homeassistant.core import callback from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( - HVAC_MODE_HEAT_COOL, + FAN_AUTO, + FAN_HIGH, + FAN_LOW, + FAN_MEDIUM, HVAC_MODE_COOL, HVAC_MODE_DRY, HVAC_MODE_FAN_ONLY, HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, - FAN_LOW, - FAN_MEDIUM, - FAN_HIGH, - FAN_AUTO, PRESET_ECO, PRESET_NONE, SUPPORT_FAN_MODE, @@ -25,23 +24,24 @@ from homeassistant.components.climate.const import ( ) from homeassistant.const import ( ATTR_TEMPERATURE, + CONF_EXCLUDE, PRECISION_HALVES, TEMP_CELSIUS, - CONF_EXCLUDE, ) +from homeassistant.core import callback +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 -from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import ( + DATA_CONFIG, DATA_DISCOVERY_SERVICE, - IZONE, - DISPATCH_CONTROLLER_DISCOVERED, DISPATCH_CONTROLLER_DISCONNECTED, + DISPATCH_CONTROLLER_DISCOVERED, DISPATCH_CONTROLLER_RECONNECTED, DISPATCH_CONTROLLER_UPDATE, DISPATCH_ZONE_UPDATE, - DATA_CONFIG, + IZONE, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/izone/config_flow.py b/homeassistant/components/izone/config_flow.py index eb57a36a2bb..add1bb47a54 100644 --- a/homeassistant/components/izone/config_flow.py +++ b/homeassistant/components/izone/config_flow.py @@ -1,7 +1,7 @@ """Config flow for izone.""" -import logging import asyncio +import logging from async_timeout import timeout @@ -9,14 +9,13 @@ from homeassistant import config_entries from homeassistant.helpers import config_entry_flow from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .const import IZONE, TIMEOUT_DISCOVERY, DISPATCH_CONTROLLER_DISCOVERED - +from .const import DISPATCH_CONTROLLER_DISCOVERED, IZONE, TIMEOUT_DISCOVERY +from .discovery import async_start_discovery_service, async_stop_discovery_service _LOGGER = logging.getLogger(__name__) async def _async_has_devices(hass): - from .discovery import async_start_discovery_service, async_stop_discovery_service controller_ready = asyncio.Event() async_dispatcher_connect( diff --git a/homeassistant/components/izone/discovery.py b/homeassistant/components/izone/discovery.py index 3630c28605b..c49144f1db9 100644 --- a/homeassistant/components/izone/discovery.py +++ b/homeassistant/components/izone/discovery.py @@ -1,17 +1,18 @@ """Internal discovery service for iZone AC.""" import logging + import pizone from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.helpers import aiohttp_client -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.typing import HomeAssistantType from .const import ( DATA_DISCOVERY_SERVICE, - DISPATCH_CONTROLLER_DISCOVERED, DISPATCH_CONTROLLER_DISCONNECTED, + DISPATCH_CONTROLLER_DISCOVERED, DISPATCH_CONTROLLER_RECONNECTED, DISPATCH_CONTROLLER_UPDATE, DISPATCH_ZONE_UPDATE, diff --git a/homeassistant/components/jewish_calendar/__init__.py b/homeassistant/components/jewish_calendar/__init__.py index bbe0c1d24fd..21c19da7b35 100644 --- a/homeassistant/components/jewish_calendar/__init__.py +++ b/homeassistant/components/jewish_calendar/__init__.py @@ -1,13 +1,12 @@ """The jewish_calendar component.""" import logging -import voluptuous as vol import hdate +import voluptuous as vol from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME -from homeassistant.helpers.discovery import async_load_platform import homeassistant.helpers.config_validation as cv - +from homeassistant.helpers.discovery import async_load_platform _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/jewish_calendar/sensor.py b/homeassistant/components/jewish_calendar/sensor.py index 54a3d1497aa..d0376694a44 100644 --- a/homeassistant/components/jewish_calendar/sensor.py +++ b/homeassistant/components/jewish_calendar/sensor.py @@ -122,8 +122,9 @@ class JewishCalendarSensor(Entity): # Compute the weekly portion based on the upcoming shabbat. return after_tzais_date.upcoming_shabbat.parasha if self._type == "holiday": - self._holiday_attrs["type"] = after_shkia_date.holiday_type.name self._holiday_attrs["id"] = after_shkia_date.holiday_name + self._holiday_attrs["type"] = after_shkia_date.holiday_type.name + self._holiday_attrs["type_id"] = after_shkia_date.holiday_type.value return after_shkia_date.holiday_description if self._type == "omer_count": return after_shkia_date.omer_day diff --git a/homeassistant/components/joaoapps_join/__init__.py b/homeassistant/components/joaoapps_join/__init__.py index b988411762e..10cbcf6b5c0 100644 --- a/homeassistant/components/joaoapps_join/__init__.py +++ b/homeassistant/components/joaoapps_join/__init__.py @@ -1,10 +1,19 @@ """Support for Joaoapps Join services.""" import logging +from pyjoin import ( + get_devices, + ring_device, + send_file, + send_notification, + send_sms, + send_url, + set_wallpaper, +) import voluptuous as vol +from homeassistant.const import CONF_API_KEY, CONF_NAME import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_NAME, CONF_API_KEY _LOGGER = logging.getLogger(__name__) @@ -35,14 +44,6 @@ CONFIG_SCHEMA = vol.Schema( def register_device(hass, api_key, name, device_id, device_ids, device_names): """Register services for each join device listed.""" - from pyjoin import ( - ring_device, - set_wallpaper, - send_sms, - send_file, - send_url, - send_notification, - ) def ring_service(service): """Service to ring devices.""" @@ -114,7 +115,6 @@ def register_device(hass, api_key, name, device_id, device_ids, device_names): def setup(hass, config): """Set up the Join services.""" - from pyjoin import get_devices for device in config[DOMAIN]: api_key = device.get(CONF_API_KEY) diff --git a/homeassistant/components/joaoapps_join/notify.py b/homeassistant/components/joaoapps_join/notify.py index 2e6b3d1c67a..14b8fe1a814 100644 --- a/homeassistant/components/joaoapps_join/notify.py +++ b/homeassistant/components/joaoapps_join/notify.py @@ -1,6 +1,9 @@ """Support for Join notifications.""" import logging + +from pyjoin import get_devices, send_notification import voluptuous as vol + from homeassistant.components.notify import ( ATTR_DATA, ATTR_TITLE, @@ -34,8 +37,6 @@ def get_service(hass, config, discovery_info=None): device_ids = config.get(CONF_DEVICE_IDS) device_names = config.get(CONF_DEVICE_NAMES) if api_key: - from pyjoin import get_devices - if not get_devices(api_key): _LOGGER.error("Error connecting to Join. Check the API key") return False @@ -60,7 +61,6 @@ class JoinNotificationService(BaseNotificationService): def send_message(self, message="", **kwargs): """Send a message to a user.""" - from pyjoin import send_notification title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) data = kwargs.get(ATTR_DATA) or {} diff --git a/homeassistant/components/juicenet/manifest.json b/homeassistant/components/juicenet/manifest.json index 076567573c7..fac59cb3b8d 100644 --- a/homeassistant/components/juicenet/manifest.json +++ b/homeassistant/components/juicenet/manifest.json @@ -3,7 +3,7 @@ "name": "Juicenet", "documentation": "https://www.home-assistant.io/integrations/juicenet", "requirements": [ - "python-juicenet==0.1.5" + "python-juicenet==0.1.6" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/kankun/switch.py b/homeassistant/components/kankun/switch.py index 63f289862f6..4f7ba5c8b06 100644 --- a/homeassistant/components/kankun/switch.py +++ b/homeassistant/components/kankun/switch.py @@ -4,15 +4,15 @@ import logging import requests import voluptuous as vol -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import ( CONF_HOST, CONF_NAME, - CONF_PORT, - CONF_PATH, - CONF_USERNAME, CONF_PASSWORD, + CONF_PATH, + CONF_PORT, CONF_SWITCHES, + CONF_USERNAME, ) import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/keba/__init__.py b/homeassistant/components/keba/__init__.py index 5a9a49a005a..a261311be76 100644 --- a/homeassistant/components/keba/__init__.py +++ b/homeassistant/components/keba/__init__.py @@ -106,13 +106,14 @@ class KebaHandler(KebaKeContact): """Representation of a KEBA charging station connection.""" def __init__(self, hass, host, rfid, refresh_interval): - """Constructor.""" + """Initialize charging station connection.""" super().__init__(host, self.hass_callback) self._update_listeners = [] self._hass = hass self.rfid = rfid - self.device_name = "keba_wallbox_" + self.device_name = "keba" # correct device name will be set in setup() + self.device_id = "keba_wallbox_" # correct device id will be set in setup() # Ensure at least MAX_POLLING_INTERVAL seconds delay self._refresh_interval = max(MAX_POLLING_INTERVAL, refresh_interval) @@ -147,8 +148,12 @@ class KebaHandler(KebaKeContact): # Request initial values and extract serial number await self.request_data() - if self.get_value("Serial") is not None: - self.device_name = f"keba_wallbox_{self.get_value('Serial')}" + if ( + self.get_value("Serial") is not None + and self.get_value("Product") is not None + ): + self.device_id = f"keba_wallbox_{self.get_value('Serial')}" + self.device_name = self.get_value("Product") return True return False @@ -179,7 +184,7 @@ class KebaHandler(KebaKeContact): """Set energy target in async way.""" try: energy = param["energy"] - await self.set_energy(energy) + await self.set_energy(float(energy)) self._set_fast_polling() except (KeyError, ValueError) as ex: _LOGGER.warning("Energy value is not correct. %s", ex) @@ -188,7 +193,7 @@ class KebaHandler(KebaKeContact): """Set current maximum in async way.""" try: current = param["current"] - await self.set_current(current) + await self.set_current(float(current)) # No fast polling as this function might be called regularly except (KeyError, ValueError) as ex: _LOGGER.warning("Current value is not correct. %s", ex) @@ -216,10 +221,10 @@ class KebaHandler(KebaKeContact): async def async_set_failsafe(self, param=None): """Set failsafe mode in async way.""" try: - timout = param[CONF_FS_TIMEOUT] + timeout = param[CONF_FS_TIMEOUT] fallback = param[CONF_FS_FALLBACK] persist = param[CONF_FS_PERSIST] - await self.set_failsafe(timout, fallback, persist) + await self.set_failsafe(int(timeout), float(fallback), bool(persist)) self._set_fast_polling() except (KeyError, ValueError) as ex: _LOGGER.warning( diff --git a/homeassistant/components/keba/binary_sensor.py b/homeassistant/components/keba/binary_sensor.py index 8c0503a2020..5cced416bc3 100644 --- a/homeassistant/components/keba/binary_sensor.py +++ b/homeassistant/components/keba/binary_sensor.py @@ -1,12 +1,12 @@ """Support for KEBA charging station binary sensors.""" import logging -from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_PLUG, DEVICE_CLASS_CONNECTIVITY, + DEVICE_CLASS_PLUG, DEVICE_CLASS_POWER, DEVICE_CLASS_SAFETY, + BinarySensorDevice, ) from . import DOMAIN @@ -22,10 +22,16 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= keba = hass.data[DOMAIN] sensors = [ - KebaBinarySensor(keba, "Online", "Wallbox", DEVICE_CLASS_CONNECTIVITY), - KebaBinarySensor(keba, "Plug", "Plug", DEVICE_CLASS_PLUG), - KebaBinarySensor(keba, "State", "Charging state", DEVICE_CLASS_POWER), - KebaBinarySensor(keba, "Tmo FS", "Failsafe Mode", DEVICE_CLASS_SAFETY), + KebaBinarySensor( + keba, "Online", "Status", "device_state", DEVICE_CLASS_CONNECTIVITY + ), + KebaBinarySensor(keba, "Plug", "Plug", "plug_state", DEVICE_CLASS_PLUG), + KebaBinarySensor( + keba, "State", "Charging State", "charging_state", DEVICE_CLASS_POWER + ), + KebaBinarySensor( + keba, "Tmo FS", "Failsafe Mode", "failsafe_mode_state", DEVICE_CLASS_SAFETY + ), ] async_add_entities(sensors) @@ -33,11 +39,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class KebaBinarySensor(BinarySensorDevice): """Representation of a binary sensor of a KEBA charging station.""" - def __init__(self, keba, key, sensor_name, device_class): + def __init__(self, keba, key, name, entity_type, device_class): """Initialize the KEBA Sensor.""" self._key = key self._keba = keba - self._name = sensor_name + self._name = name + self._entity_type = entity_type self._device_class = device_class self._is_on = None self._attributes = {} @@ -50,12 +57,12 @@ class KebaBinarySensor(BinarySensorDevice): @property def unique_id(self): """Return the unique ID of the binary sensor.""" - return f"{self._keba.device_name}_{self._name}" + return f"{self._keba.device_id}_{self._entity_type}" @property def name(self): """Return the name of the device.""" - return self._name + return f"{self._keba.device_name} {self._name}" @property def device_class(self): diff --git a/homeassistant/components/keba/lock.py b/homeassistant/components/keba/lock.py index 3a65e44cd6f..f69fbdddf20 100644 --- a/homeassistant/components/keba/lock.py +++ b/homeassistant/components/keba/lock.py @@ -15,17 +15,18 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= keba = hass.data[DOMAIN] - sensors = [KebaLock(keba, "Authentication")] + sensors = [KebaLock(keba, "Authentication", "authentication")] async_add_entities(sensors) class KebaLock(LockDevice): """The entity class for KEBA charging stations switch.""" - def __init__(self, keba, name): + def __init__(self, keba, name, entity_type): """Initialize the KEBA switch.""" self._keba = keba self._name = name + self._entity_type = entity_type self._state = True @property @@ -35,13 +36,13 @@ class KebaLock(LockDevice): @property def unique_id(self): - """Return the unique ID of the binary sensor.""" - return f"{self._keba.device_name}_{self._name}" + """Return the unique ID of the lock.""" + return f"{self._keba.device_id}_{self._entity_type}" @property def name(self): """Return the name of the device.""" - return self._name + return f"{self._keba.device_name} {self._name}" @property def is_locked(self): diff --git a/homeassistant/components/keba/manifest.json b/homeassistant/components/keba/manifest.json index 422a79cd0be..0f3d21fc783 100644 --- a/homeassistant/components/keba/manifest.json +++ b/homeassistant/components/keba/manifest.json @@ -2,7 +2,7 @@ "domain": "keba", "name": "Keba Charging Station", "documentation": "https://www.home-assistant.io/integrations/keba", - "requirements": ["keba-kecontact==0.2.0"], + "requirements": ["keba-kecontact==1.0.0"], "dependencies": [], "codeowners": [ "@dannerph" diff --git a/homeassistant/components/keba/sensor.py b/homeassistant/components/keba/sensor.py index f46b2f0cf90..d9e6118ff32 100644 --- a/homeassistant/components/keba/sensor.py +++ b/homeassistant/components/keba/sensor.py @@ -1,9 +1,8 @@ """Support for KEBA charging station sensors.""" import logging -from homeassistant.const import ENERGY_KILO_WATT_HOUR +from homeassistant.const import DEVICE_CLASS_POWER, ENERGY_KILO_WATT_HOUR from homeassistant.helpers.entity import Entity -from homeassistant.const import DEVICE_CLASS_POWER from . import DOMAIN @@ -18,15 +17,40 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= keba = hass.data[DOMAIN] sensors = [ - KebaSensor(keba, "Curr user", "Max current", "mdi:flash", "A"), + KebaSensor(keba, "Curr user", "Max Current", "max_current", "mdi:flash", "A"), KebaSensor( - keba, "Setenergy", "Energy target", "mdi:gauge", ENERGY_KILO_WATT_HOUR + keba, + "Setenergy", + "Energy Target", + "energy_target", + "mdi:gauge", + ENERGY_KILO_WATT_HOUR, ), - KebaSensor(keba, "P", "Charging power", "mdi:flash", "kW", DEVICE_CLASS_POWER), KebaSensor( - keba, "E pres", "Session energy", "mdi:gauge", ENERGY_KILO_WATT_HOUR + keba, + "P", + "Charging Power", + "charging_power", + "mdi:flash", + "kW", + DEVICE_CLASS_POWER, + ), + KebaSensor( + keba, + "E pres", + "Session Energy", + "session_energy", + "mdi:gauge", + ENERGY_KILO_WATT_HOUR, + ), + KebaSensor( + keba, + "E total", + "Total Energy", + "total_energy", + "mdi:gauge", + ENERGY_KILO_WATT_HOUR, ), - KebaSensor(keba, "E total", "Total Energy", "mdi:gauge", ENERGY_KILO_WATT_HOUR), ] async_add_entities(sensors) @@ -34,14 +58,16 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class KebaSensor(Entity): """The entity class for KEBA charging stations sensors.""" - def __init__(self, keba, key, name, icon, unit, device_class=None): + def __init__(self, keba, key, name, entity_type, icon, unit, device_class=None): """Initialize the KEBA Sensor.""" - self._key = key self._keba = keba + self._key = key self._name = name - self._device_class = device_class + self._entity_type = entity_type self._icon = icon self._unit = unit + self._device_class = device_class + self._state = None self._attributes = {} @@ -53,12 +79,12 @@ class KebaSensor(Entity): @property def unique_id(self): """Return the unique ID of the binary sensor.""" - return f"{self._keba.device_name}_{self._name}" + return f"{self._keba.device_id}_{self._entity_type}" @property def name(self): """Return the name of the device.""" - return self._name + return f"{self._keba.device_name} {self._name}" @property def device_class(self): diff --git a/homeassistant/components/keenetic_ndms2/device_tracker.py b/homeassistant/components/keenetic_ndms2/device_tracker.py index faab0dde62d..598e29cf583 100644 --- a/homeassistant/components/keenetic_ndms2/device_tracker.py +++ b/homeassistant/components/keenetic_ndms2/device_tracker.py @@ -1,15 +1,16 @@ """Support for Zyxel Keenetic NDMS2 based routers.""" import logging +from ndms2_client import Client, ConnectionException, TelnetConnection import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner, ) -from homeassistant.const import CONF_HOST, CONF_PORT, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -45,7 +46,6 @@ class KeeneticNDMS2DeviceScanner(DeviceScanner): def __init__(self, config): """Initialize the scanner.""" - from ndms2_client import Client, TelnetConnection self.last_results = [] @@ -88,8 +88,6 @@ class KeeneticNDMS2DeviceScanner(DeviceScanner): """Get ARP from keenetic router.""" _LOGGER.debug("Fetching devices from router...") - from ndms2_client import ConnectionException - try: self.last_results = [ dev diff --git a/homeassistant/components/keenetic_ndms2/manifest.json b/homeassistant/components/keenetic_ndms2/manifest.json index 4613d2d9608..df78c98aa3c 100644 --- a/homeassistant/components/keenetic_ndms2/manifest.json +++ b/homeassistant/components/keenetic_ndms2/manifest.json @@ -3,7 +3,7 @@ "name": "Keenetic ndms2", "documentation": "https://www.home-assistant.io/integrations/keenetic_ndms2", "requirements": [ - "ndms2_client==0.0.10" + "ndms2_client==0.0.11" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/keyboard/services.yaml b/homeassistant/components/keyboard/services.yaml index e69de29bb2d..a9896c3d6cf 100644 --- a/homeassistant/components/keyboard/services.yaml +++ b/homeassistant/components/keyboard/services.yaml @@ -0,0 +1,17 @@ +volume_up: + description: Simulates a key press of the "Volume Up" button on HomeAssistant's host machine. + +volume_down: + description: Simulates a key press of the "Volume Down" button on HomeAssistant's host machine. + +volume_mute: + description: Simulates a key press of the "Volume Mute" button on HomeAssistant's host machine. + +media_play_pause: + description: Simulates a key press of the "Media Play/Pause" button on HomeAssistant's host machine. + +media_next_track: + description: Simulates a key press of the "Media Next Track" button on HomeAssistant's host machine. + +media_prev_track: + description: Simulates a key press of the "Media Previous Track" button on HomeAssistant's host machine. diff --git a/homeassistant/components/keyboard_remote/__init__.py b/homeassistant/components/keyboard_remote/__init__.py index d4ed6128cbe..310bd0189bd 100644 --- a/homeassistant/components/keyboard_remote/__init__.py +++ b/homeassistant/components/keyboard_remote/__init__.py @@ -1,13 +1,15 @@ """Receive signals from a keyboard and use it as a remote control.""" # pylint: disable=import-error -import logging import asyncio +import logging +import os -from evdev import InputDevice, categorize, ecodes, list_devices import aionotify +from evdev import InputDevice, categorize, ecodes, list_devices import voluptuous as vol -import homeassistant.helpers.config_validation as cv + from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -118,9 +120,11 @@ class KeyboardRemote: # add initial devices (do this AFTER starting watcher in order to # avoid race conditions leading to missing device connections) initial_start_monitoring = set() - descriptors = list_devices(DEVINPUT) + descriptors = await self.hass.async_add_executor_job(list_devices, DEVINPUT) for descriptor in descriptors: - dev, handler = self.get_device_handler(descriptor) + dev, handler = await self.hass.async_add_executor_job( + self.get_device_handler, descriptor + ) if handler is None: continue @@ -164,6 +168,15 @@ class KeyboardRemote: handler = self.handlers_by_descriptor[descriptor] elif dev.name in self.handlers_by_name: handler = self.handlers_by_name[dev.name] + else: + # check for symlinked paths matching descriptor + for test_descriptor, test_handler in self.handlers_by_descriptor.items(): + if test_handler.dev is not None: + fullpath = test_handler.dev.path + else: + fullpath = os.path.realpath(test_descriptor) + if fullpath == descriptor: + handler = test_handler return (dev, handler) @@ -185,7 +198,9 @@ class KeyboardRemote: (event.flags & aionotify.Flags.CREATE) or (event.flags & aionotify.Flags.ATTRIB) ) and not descriptor_active: - dev, handler = self.get_device_handler(descriptor) + dev, handler = await self.hass.async_add_executor_job( + self.get_device_handler, descriptor + ) if handler is None: continue self.active_handlers_by_descriptor[descriptor] = handler @@ -241,7 +256,7 @@ class KeyboardRemote: """Stop event monitoring task and issue event.""" if self.monitor_task is not None: try: - self.dev.ungrab() + 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 @@ -271,7 +286,7 @@ class KeyboardRemote: try: _LOGGER.debug("Start device monitoring") - dev.grab() + await self.hass.async_add_executor_job(dev.grab) async for event in dev.async_read_loop(): if event.type is ecodes.EV_KEY: if event.value in self.key_values: diff --git a/homeassistant/components/kiwi/lock.py b/homeassistant/components/kiwi/lock.py index 4949ceeb1d8..b13906b44f5 100644 --- a/homeassistant/components/kiwi/lock.py +++ b/homeassistant/components/kiwi/lock.py @@ -1,21 +1,22 @@ """Support for the KIWI.KI lock platform.""" import logging +from kiwiki import KiwiClient, KiwiException import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.components.lock import LockDevice, PLATFORM_SCHEMA +from homeassistant.components.lock import PLATFORM_SCHEMA, LockDevice from homeassistant.const import ( + ATTR_ID, + ATTR_LATITUDE, + ATTR_LONGITUDE, CONF_PASSWORD, CONF_USERNAME, - ATTR_ID, - ATTR_LONGITUDE, - ATTR_LATITUDE, STATE_LOCKED, STATE_UNLOCKED, ) -from homeassistant.helpers.event import async_call_later from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import async_call_later _LOGGER = logging.getLogger(__name__) @@ -32,7 +33,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the KIWI lock platform.""" - from kiwiki import KiwiClient, KiwiException try: kiwi = KiwiClient(config[CONF_USERNAME], config[CONF_PASSWORD]) @@ -98,7 +98,6 @@ class KiwiLock(LockDevice): def unlock(self, **kwargs): """Unlock the device.""" - from kiwiki import KiwiException try: self._client.open_door(self.lock_id) diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index 00d5d18f013..61a497e938a 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -102,7 +102,7 @@ async def async_setup(hass, config): except XKNXException as ex: _LOGGER.warning("Can't connect to KNX interface: %s", ex) hass.components.persistent_notification.async_create( - "Can't connect to KNX interface:
" "{0}".format(ex), title="KNX" + "Can't connect to KNX interface:
{0}".format(ex), title="KNX" ) for component, discovery_type in ( diff --git a/homeassistant/components/kodi/__init__.py b/homeassistant/components/kodi/__init__.py index 5bbffc5df1d..1f2d3cb5cd0 100644 --- a/homeassistant/components/kodi/__init__.py +++ b/homeassistant/components/kodi/__init__.py @@ -1,13 +1,13 @@ """The kodi component.""" import asyncio + import voluptuous as vol -from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM -from homeassistant.helpers import config_validation as cv from homeassistant.components.kodi.const import DOMAIN from homeassistant.components.media_player.const import DOMAIN as MP_DOMAIN - +from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM +from homeassistant.helpers import config_validation as cv SERVICE_ADD_MEDIA = "add_to_playlist" SERVICE_CALL_METHOD = "call_method" diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index 9b2ba01e90a..71418927ed2 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -7,15 +7,14 @@ import socket import urllib import aiohttp -import jsonrpc_base import jsonrpc_async +import jsonrpc_base import jsonrpc_websocket - import voluptuous as vol from homeassistant.components.kodi import SERVICE_CALL_METHOD from homeassistant.components.kodi.const import DOMAIN -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, MEDIA_TYPE_MOVIE, @@ -52,12 +51,11 @@ from homeassistant.const import ( STATE_PLAYING, ) from homeassistant.core import callback -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers import script +from homeassistant.helpers import config_validation as cv, script from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.template import Template -from homeassistant.util.yaml import dump import homeassistant.util.dt as dt_util +from homeassistant.util.yaml import dump _LOGGER = logging.getLogger(__name__) @@ -813,7 +811,7 @@ class KodiDevice(MediaPlayerDevice): except jsonrpc_base.jsonrpc.TransportError: result = None _LOGGER.warning( - "TransportError trying to run API method " "%s.%s(%s)", + "TransportError trying to run API method %s.%s(%s)", self.entity_id, method, kwargs, diff --git a/homeassistant/components/kodi/notify.py b/homeassistant/components/kodi/notify.py index 1072cf1b732..6f370ffad98 100644 --- a/homeassistant/components/kodi/notify.py +++ b/homeassistant/components/kodi/notify.py @@ -3,9 +3,15 @@ import logging import aiohttp import jsonrpc_async - import voluptuous as vol +from homeassistant.components.notify import ( + ATTR_DATA, + ATTR_TITLE, + ATTR_TITLE_DEFAULT, + PLATFORM_SCHEMA, + BaseNotificationService, +) from homeassistant.const import ( ATTR_ICON, CONF_HOST, @@ -17,14 +23,6 @@ from homeassistant.const import ( from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.components.notify import ( - ATTR_DATA, - ATTR_TITLE, - ATTR_TITLE_DEFAULT, - PLATFORM_SCHEMA, - BaseNotificationService, -) - _LOGGER = logging.getLogger(__name__) DEFAULT_PORT = 8080 diff --git a/homeassistant/components/kwb/sensor.py b/homeassistant/components/kwb/sensor.py index 49815faf7ae..b7872ca1ab4 100644 --- a/homeassistant/components/kwb/sensor.py +++ b/homeassistant/components/kwb/sensor.py @@ -1,18 +1,19 @@ """Support for KWB Easyfire.""" import logging +from pykwb import kwb import voluptuous as vol +from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_HOST, - CONF_PORT, CONF_DEVICE, + CONF_HOST, CONF_NAME, + CONF_PORT, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.helpers.entity import Entity -from homeassistant.components.sensor import PLATFORM_SCHEMA import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -56,8 +57,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): raw = config.get(CONF_RAW) client_name = config.get(CONF_NAME) - from pykwb import kwb - if connection_type == "serial": easyfire = kwb.KWBEasyfire(MODE_SERIAL, "", 0, device) elif connection_type == "tcp": diff --git a/homeassistant/components/lacrosse/sensor.py b/homeassistant/components/lacrosse/sensor.py index ccfd647d746..b8bde797b39 100644 --- a/homeassistant/components/lacrosse/sensor.py +++ b/homeassistant/components/lacrosse/sensor.py @@ -2,6 +2,8 @@ from datetime import timedelta import logging +import pylacrosse +from serial import SerialException import voluptuous as vol from homeassistant.components.sensor import ENTITY_ID_FORMAT, PLATFORM_SCHEMA @@ -61,8 +63,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the LaCrosse sensors.""" - import pylacrosse - from serial import SerialException usb_device = config.get(CONF_DEVICE) baud = int(config.get(CONF_BAUD)) diff --git a/homeassistant/components/lametric/__init__.py b/homeassistant/components/lametric/__init__.py index a24ad79104d..9281affa492 100644 --- a/homeassistant/components/lametric/__init__.py +++ b/homeassistant/components/lametric/__init__.py @@ -1,6 +1,7 @@ """Support for LaMetric time.""" import logging +from lmnotify import LaMetricManager import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -50,7 +51,6 @@ class HassLaMetricManager: def __init__(self, client_id, client_secret): """Initialize HassLaMetricManager and connect to LaMetric.""" - from lmnotify import LaMetricManager _LOGGER.debug("Connecting to LaMetric") self.manager = LaMetricManager(client_id, client_secret) diff --git a/homeassistant/components/lametric/notify.py b/homeassistant/components/lametric/notify.py index 901fb07fc55..052eb3bceac 100644 --- a/homeassistant/components/lametric/notify.py +++ b/homeassistant/components/lametric/notify.py @@ -1,6 +1,8 @@ """Support for LaMetric notifications.""" import logging +from lmnotify import Model, SimpleFrame, Sound +from oauthlib.oauth2 import TokenExpiredError from requests.exceptions import ConnectionError as RequestsConnectionError import voluptuous as vol @@ -59,8 +61,6 @@ class LaMetricNotificationService(BaseNotificationService): def send_message(self, message="", **kwargs): """Send a message to some LaMetric device.""" - from lmnotify import SimpleFrame, Sound, Model - from oauthlib.oauth2 import TokenExpiredError targets = kwargs.get(ATTR_TARGET) data = kwargs.get(ATTR_DATA) @@ -113,7 +113,7 @@ class LaMetricNotificationService(BaseNotificationService): self._devices = lmn.get_devices() except RequestsConnectionError: _LOGGER.warning( - "Problem connecting to LaMetric, " "using cached devices instead" + "Problem connecting to LaMetric, using cached devices instead" ) for dev in self._devices: if targets is None or dev["name"] in targets: diff --git a/homeassistant/components/lannouncer/notify.py b/homeassistant/components/lannouncer/notify.py index 9512a75047b..9421eb16f51 100644 --- a/homeassistant/components/lannouncer/notify.py +++ b/homeassistant/components/lannouncer/notify.py @@ -5,14 +5,13 @@ from urllib.parse import urlencode import voluptuous as vol -from homeassistant.const import CONF_HOST, CONF_PORT -import homeassistant.helpers.config_validation as cv - from homeassistant.components.notify import ( ATTR_DATA, PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.const import CONF_HOST, CONF_PORT +import homeassistant.helpers.config_validation as cv ATTR_METHOD = "method" ATTR_METHOD_DEFAULT = "speak" diff --git a/homeassistant/components/launch_library/sensor.py b/homeassistant/components/launch_library/sensor.py index 14a75704312..32335526194 100644 --- a/homeassistant/components/launch_library/sensor.py +++ b/homeassistant/components/launch_library/sensor.py @@ -2,13 +2,14 @@ from datetime import timedelta import logging +from pylaunches.api import Launches import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME -from homeassistant.helpers.entity import Entity 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__) @@ -25,7 +26,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Create the launch sensor.""" - from pylaunches.api import Launches name = config[CONF_NAME] diff --git a/homeassistant/components/lcn/__init__.py b/homeassistant/components/lcn/__init__.py index f7170340f1b..14f25be70b0 100644 --- a/homeassistant/components/lcn/__init__.py +++ b/homeassistant/components/lcn/__init__.py @@ -4,7 +4,6 @@ import logging import pypck import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.climate import DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP from homeassistant.const import ( CONF_ADDRESS, @@ -22,6 +21,7 @@ from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.entity import Entity diff --git a/homeassistant/components/lcn/helpers.py b/homeassistant/components/lcn/helpers.py index 236035b0400..f4545817c9f 100644 --- a/homeassistant/components/lcn/helpers.py +++ b/homeassistant/components/lcn/helpers.py @@ -9,7 +9,7 @@ from .const import DEFAULT_NAME # Regex for address validation PATTERN_ADDRESS = re.compile( - "^((?P\\w+)\\.)?s?(?P\\d+)" "\\.(?Pm|g)?(?P\\d+)$" + "^((?P\\w+)\\.)?s?(?P\\d+)\\.(?Pm|g)?(?P\\d+)$" ) diff --git a/homeassistant/components/lcn/services.py b/homeassistant/components/lcn/services.py index aba29e55176..c35a0cc00bf 100644 --- a/homeassistant/components/lcn/services.py +++ b/homeassistant/components/lcn/services.py @@ -2,13 +2,13 @@ import pypck import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( CONF_ADDRESS, CONF_BRIGHTNESS, CONF_STATE, CONF_UNIT_OF_MEASUREMENT, ) +import homeassistant.helpers.config_validation as cv from .const import ( CONF_CONNECTIONS, @@ -305,7 +305,7 @@ class SendKeys(LcnServiceCall): hit = pypck.lcn_defs.SendKeyCommand.HIT if pypck.lcn_defs.SendKeyCommand[call.data[CONF_STATE]] != hit: raise ValueError( - "Only hit command is allowed when sending" " deferred keys." + "Only hit command is allowed when sending deferred keys." ) delay_unit = pypck.lcn_defs.TimeUnit.parse(call.data[CONF_TIME_UNIT]) address_connection.send_keys_hit_deferred(keys, delay_time, delay_unit) @@ -344,7 +344,7 @@ class LockKeys(LcnServiceCall): if delay_time != 0: if table_id != 0: raise ValueError( - "Only table A is allowed when locking keys" " for a specific time." + "Only table A is allowed when locking keys for a specific time." ) delay_unit = pypck.lcn_defs.TimeUnit.parse(call.data[CONF_TIME_UNIT]) address_connection.lock_keys_tab_a_temporary(delay_time, delay_unit, states) diff --git a/homeassistant/components/lg_netcast/media_player.py b/homeassistant/components/lg_netcast/media_player.py index d11887d8a8a..0be51c337e8 100644 --- a/homeassistant/components/lg_netcast/media_player.py +++ b/homeassistant/components/lg_netcast/media_player.py @@ -2,11 +2,12 @@ from datetime import timedelta import logging +from pylgnetcast import LgNetCastClient, LgNetCastError from requests import RequestException import voluptuous as vol from homeassistant import util -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, SUPPORT_NEXT_TRACK, @@ -57,7 +58,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the LG TV platform.""" - from pylgnetcast import LgNetCastClient host = config.get(CONF_HOST) access_token = config.get(CONF_ACCESS_TOKEN) @@ -87,7 +87,6 @@ class LgTVDevice(MediaPlayerDevice): def send_command(self, command): """Send remote control commands to the TV.""" - from pylgnetcast import LgNetCastError try: with self._client as client: @@ -98,7 +97,6 @@ class LgTVDevice(MediaPlayerDevice): @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) def update(self): """Retrieve the latest data from the LG TV.""" - from pylgnetcast import LgNetCastError try: with self._client as client: diff --git a/homeassistant/components/life360/.translations/da.json b/homeassistant/components/life360/.translations/da.json index 933fce4a4e8..32acc488dc6 100644 --- a/homeassistant/components/life360/.translations/da.json +++ b/homeassistant/components/life360/.translations/da.json @@ -20,7 +20,7 @@ "username": "Brugernavn" }, "description": "Hvis du vil angive avancerede indstillinger skal du se [Life360 dokumentation]({docs_url}).\nDu \u00f8nsker m\u00e5ske at g\u00f8re dette f\u00f8r du tilf\u00f8jer konti.", - "title": "Life360 kontooplysninger" + "title": "Life360-kontooplysninger" } }, "title": "Life360" diff --git a/homeassistant/components/life360/.translations/ru.json b/homeassistant/components/life360/.translations/ru.json index eba3a47ead8..c3f2601eb99 100644 --- a/homeassistant/components/life360/.translations/ru.json +++ b/homeassistant/components/life360/.translations/ru.json @@ -1,17 +1,17 @@ { "config": { "abort": { - "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", - "user_already_configured": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430." + "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", + "user_already_configured": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430." }, "create_entry": { "default": "\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." }, "error": { - "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", + "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", "invalid_username": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d.", "unexpected": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u0441\u0432\u044f\u0437\u0438 \u0441 \u0441\u0435\u0440\u0432\u0435\u0440\u043e\u043c Life360.", - "user_already_configured": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430." + "user_already_configured": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430." }, "step": { "user": { diff --git a/homeassistant/components/lifx/.translations/da.json b/homeassistant/components/lifx/.translations/da.json index ffd8e20ce42..99143f38c98 100644 --- a/homeassistant/components/lifx/.translations/da.json +++ b/homeassistant/components/lifx/.translations/da.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "no_devices_found": "Ingen LIFX enheder kunne findes p\u00e5 netv\u00e6rket.", - "single_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning af LIFX." + "no_devices_found": "Der blev ikke fundet nogen LIFX-enheder p\u00e5 netv\u00e6rket.", + "single_instance_allowed": "Kun en enkelt konfiguration af LIFX er mulig." }, "step": { "confirm": { diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index 50e36e8db0a..aa63be04f0d 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -35,7 +35,12 @@ from homeassistant.components.light import ( Light, preprocess_turn_on_alternatives, ) -from homeassistant.const import ATTR_ENTITY_ID, ATTR_MODE, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_MODE, + ENTITY_MATCH_ALL, + EVENT_HOMEASSISTANT_STOP, +) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv import homeassistant.helpers.device_registry as dr @@ -60,23 +65,24 @@ MESSAGE_TIMEOUT = 1.0 MESSAGE_RETRIES = 8 UNAVAILABLE_GRACE = 90 -SERVICE_LIFX_SET_STATE = "lifx_set_state" +SERVICE_LIFX_SET_STATE = "set_state" ATTR_INFRARED = "infrared" ATTR_ZONES = "zones" ATTR_POWER = "power" -LIFX_SET_STATE_SCHEMA = LIGHT_TURN_ON_SCHEMA.extend( +LIFX_SET_STATE_SCHEMA = cv.make_entity_service_schema( { + **LIGHT_TURN_ON_SCHEMA, ATTR_INFRARED: vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255)), ATTR_ZONES: vol.All(cv.ensure_list, [cv.positive_int]), ATTR_POWER: cv.boolean, } ) -SERVICE_EFFECT_PULSE = "lifx_effect_pulse" -SERVICE_EFFECT_COLORLOOP = "lifx_effect_colorloop" -SERVICE_EFFECT_STOP = "lifx_effect_stop" +SERVICE_EFFECT_PULSE = "effect_pulse" +SERVICE_EFFECT_COLORLOOP = "effect_colorloop" +SERVICE_EFFECT_STOP = "effect_stop" ATTR_POWER_ON = "power_on" ATTR_PERIOD = "period" @@ -282,7 +288,7 @@ class LIFXManager: SERVICE_EFFECT_PULSE, SERVICE_EFFECT_COLORLOOP, ]: - self.hass.services.async_remove(DOMAIN, service) + self.hass.services.async_remove(LIFX_DOMAIN, service) def register_set_state(self): """Register the LIFX set_state service call.""" @@ -298,7 +304,7 @@ class LIFXManager: await asyncio.wait(tasks) self.hass.services.async_register( - DOMAIN, + LIFX_DOMAIN, SERVICE_LIFX_SET_STATE, service_handler, schema=LIFX_SET_STATE_SCHEMA, @@ -314,21 +320,24 @@ class LIFXManager: await self.start_effect(entities, service.service, **service.data) self.hass.services.async_register( - DOMAIN, + LIFX_DOMAIN, SERVICE_EFFECT_PULSE, service_handler, schema=LIFX_EFFECT_PULSE_SCHEMA, ) self.hass.services.async_register( - DOMAIN, + LIFX_DOMAIN, SERVICE_EFFECT_COLORLOOP, service_handler, schema=LIFX_EFFECT_COLORLOOP_SCHEMA, ) self.hass.services.async_register( - DOMAIN, SERVICE_EFFECT_STOP, service_handler, schema=LIFX_EFFECT_STOP_SCHEMA + LIFX_DOMAIN, + SERVICE_EFFECT_STOP, + service_handler, + schema=LIFX_EFFECT_STOP_SCHEMA, ) async def start_effect(self, entities, service, **kwargs): @@ -365,17 +374,15 @@ class LIFXManager: async def async_service_to_entities(self, service): """Return the known entities that a service call mentions.""" - entity_ids = await async_extract_entity_ids(self.hass, service) - if entity_ids: - entities = [ - entity - for entity in self.entities.values() - if entity.entity_id in entity_ids - ] - else: - entities = list(self.entities.values()) + if service.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_ALL: + return self.entities.values() - return entities + entity_ids = await async_extract_entity_ids(self.hass, service) + return [ + entity + for entity in self.entities.values() + if entity.entity_id in entity_ids + ] @callback def register(self, bulb): @@ -652,7 +659,7 @@ class LIFXLight(Light): """Start an effect with default parameters.""" service = kwargs[ATTR_EFFECT] data = {ATTR_ENTITY_ID: self.entity_id} - await self.hass.services.async_call(DOMAIN, service, data) + await self.hass.services.async_call(LIFX_DOMAIN, service, data) async def async_update(self): """Update bulb status.""" diff --git a/homeassistant/components/lifx/services.yaml b/homeassistant/components/lifx/services.yaml index e69de29bb2d..ebf2032a9a5 100644 --- a/homeassistant/components/lifx/services.yaml +++ b/homeassistant/components/lifx/services.yaml @@ -0,0 +1,77 @@ +set_state: + description: Set a color/brightness and possibliy turn the light on/off. + fields: + entity_id: + description: Name(s) of entities to set a state on. + example: "light.garage" + "...": + description: All turn_on parameters can be used to specify a color. + infrared: + description: Automatic infrared level (0..255) when light brightness is low. + example: 255 + zones: + description: List of zone numbers to affect (8 per LIFX Z, starts at 0). + example: "[0,5]" + transition: + description: Duration in seconds it takes to get to the final state. + example: 10 + power: + description: Turn the light on (True) or off (False). Leave out to keep the power as it is. + example: True + +effect_pulse: + description: Run a flash effect by changing to a color and back. + fields: + entity_id: + description: Name(s) of entities to run the effect on. + example: "light.kitchen" + mode: + description: "Decides how colors are changed. Possible values: blink, breathe, ping, strobe, solid." + example: strobe + brightness: + description: Number between 0..255 indicating brightness of the temporary color. + example: 120 + color_name: + description: A human readable color name. + example: "red" + rgb_color: + description: The temporary color in RGB-format. + example: "[255, 100, 100]" + period: + description: Duration of the effect in seconds (default 1.0). + example: 3 + cycles: + description: Number of times the effect should run (default 1.0). + example: 2 + power_on: + description: Powered off lights are temporarily turned on during the effect (default True). + example: False + +effect_colorloop: + description: Run an effect with looping colors. + fields: + entity_id: + description: Name(s) of entities to run the effect on. + example: "light.disco1, light.disco2, light.disco3" + brightness: + description: Number between 0 and 255 indicating brightness of the effect. Leave this out to maintain the current brightness of each participating light. + example: 120 + period: + description: Duration (in seconds) between color changes (default 60). + example: 180 + change: + description: Hue movement per period, in degrees on a color wheel (ranges from 0 to 360, default 20). + example: 45 + spread: + description: Maximum hue difference between participating lights, in degrees on a color wheel (ranges from 0 to 360, default 30). + example: 0 + power_on: + description: Powered off lights are temporarily turned on during the effect (default True). + example: False + +effect_stop: + description: Stop a running effect. + fields: + entity_id: + description: Name(s) of entities to stop effects on. Leave out to stop effects everywhere. + example: "light.bedroom" diff --git a/homeassistant/components/lifx_cloud/scene.py b/homeassistant/components/lifx_cloud/scene.py index ac4e0201fb8..4068ff20fe2 100644 --- a/homeassistant/components/lifx_cloud/scene.py +++ b/homeassistant/components/lifx_cloud/scene.py @@ -8,7 +8,7 @@ import async_timeout import voluptuous as vol from homeassistant.components.scene import Scene -from homeassistant.const import CONF_TOKEN, CONF_TIMEOUT, CONF_PLATFORM +from homeassistant.const import CONF_PLATFORM, CONF_TIMEOUT, CONF_TOKEN from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/light/.translations/da.json b/homeassistant/components/light/.translations/da.json index 14a747f6eff..eefa1e8bb6e 100644 --- a/homeassistant/components/light/.translations/da.json +++ b/homeassistant/components/light/.translations/da.json @@ -1,8 +1,17 @@ { "device_automation": { + "action_type": { + "toggle": "Skift {entity_name}", + "turn_off": "Sluk {entity_name}", + "turn_on": "T\u00e6nd for {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} er fra", + "is_on": "{entity_name} er til" + }, "trigger_type": { - "turned_off": "{entity_name} slukket", - "turned_on": "{entity_name} t\u00e6ndt" + "turned_off": "{entity_name} slukkede", + "turned_on": "{entity_name} t\u00e6ndte" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/ko.json b/homeassistant/components/light/.translations/ko.json index e055f67421e..b923fdb210e 100644 --- a/homeassistant/components/light/.translations/ko.json +++ b/homeassistant/components/light/.translations/ko.json @@ -6,12 +6,12 @@ "turn_on": "{entity_name} \ucf1c\uae30" }, "condition_type": { - "is_off": "{entity_name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4", - "is_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4" + "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\uc84c\uc2b5\ub2c8\ub2e4", - "turned_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4" + "turned_off": "{entity_name} \uc774(\uac00) \uaebc\uc9c8 \ub54c", + "turned_on": "{entity_name} \uc774(\uac00) \ucf1c\uc9c8 \ub54c" } } } \ No newline at end of file diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 2ca5e496b10..8a2a61d0421 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -17,20 +17,18 @@ from homeassistant.const import ( SERVICE_TURN_ON, STATE_ON, ) -from homeassistant.exceptions import UnknownUser, Unauthorized +from homeassistant.exceptions import Unauthorized, UnknownUser import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, - ENTITY_SERVICE_SCHEMA, + make_entity_service_schema, ) from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers import intent from homeassistant.loader import bind_hass import homeassistant.util.color as color_util - # mypy: allow-untyped-defs, no-check-untyped-defs DOMAIN = "light" @@ -94,55 +92,41 @@ VALID_TRANSITION = vol.All(vol.Coerce(float), vol.Clamp(min=0, max=6553)) VALID_BRIGHTNESS = vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255)) VALID_BRIGHTNESS_PCT = vol.All(vol.Coerce(float), vol.Range(min=0, max=100)) -LIGHT_TURN_ON_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - { - vol.Exclusive(ATTR_PROFILE, COLOR_GROUP): cv.string, - ATTR_TRANSITION: VALID_TRANSITION, - ATTR_BRIGHTNESS: VALID_BRIGHTNESS, - ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_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.Exclusive(ATTR_XY_COLOR, COLOR_GROUP): vol.All( - vol.ExactSequence((cv.small_float, cv.small_float)), vol.Coerce(tuple) - ), - vol.Exclusive(ATTR_HS_COLOR, COLOR_GROUP): vol.All( - vol.ExactSequence( - ( - vol.All(vol.Coerce(float), vol.Range(min=0, max=360)), - vol.All(vol.Coerce(float), vol.Range(min=0, max=100)), - ) - ), - vol.Coerce(tuple), - ), - vol.Exclusive(ATTR_COLOR_TEMP, COLOR_GROUP): vol.All( - vol.Coerce(int), vol.Range(min=1) - ), - vol.Exclusive(ATTR_KELVIN, COLOR_GROUP): vol.All( - vol.Coerce(int), vol.Range(min=0) - ), - ATTR_WHITE_VALUE: vol.All(vol.Coerce(int), vol.Range(min=0, max=255)), - ATTR_FLASH: vol.In([FLASH_SHORT, FLASH_LONG]), - ATTR_EFFECT: cv.string, - } -) - - -LIGHT_TURN_OFF_SCHEMA = { +LIGHT_TURN_ON_SCHEMA = { + vol.Exclusive(ATTR_PROFILE, COLOR_GROUP): cv.string, ATTR_TRANSITION: VALID_TRANSITION, + ATTR_BRIGHTNESS: VALID_BRIGHTNESS, + ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_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.Exclusive(ATTR_XY_COLOR, COLOR_GROUP): vol.All( + vol.ExactSequence((cv.small_float, cv.small_float)), vol.Coerce(tuple) + ), + vol.Exclusive(ATTR_HS_COLOR, COLOR_GROUP): vol.All( + vol.ExactSequence( + ( + vol.All(vol.Coerce(float), vol.Range(min=0, max=360)), + vol.All(vol.Coerce(float), vol.Range(min=0, max=100)), + ) + ), + vol.Coerce(tuple), + ), + vol.Exclusive(ATTR_COLOR_TEMP, COLOR_GROUP): vol.All( + vol.Coerce(int), vol.Range(min=1) + ), + vol.Exclusive(ATTR_KELVIN, COLOR_GROUP): vol.All(vol.Coerce(int), vol.Range(min=0)), + ATTR_WHITE_VALUE: vol.All(vol.Coerce(int), vol.Range(min=0, max=255)), ATTR_FLASH: vol.In([FLASH_SHORT, FLASH_LONG]), + ATTR_EFFECT: cv.string, } -LIGHT_TOGGLE_SCHEMA = LIGHT_TURN_ON_SCHEMA - PROFILE_SCHEMA = vol.Schema( vol.ExactSequence((str, cv.small_float, cv.small_float, cv.byte)) ) -INTENT_SET = "HassLightSet" - _LOGGER = logging.getLogger(__name__) @@ -196,63 +180,6 @@ def preprocess_turn_off(params): return (False, None) # Light should be turned on -class SetIntentHandler(intent.IntentHandler): - """Handle set color intents.""" - - intent_type = INTENT_SET - slot_schema = { - vol.Required("name"): cv.string, - vol.Optional("color"): color_util.color_name_to_rgb, - vol.Optional("brightness"): vol.All(vol.Coerce(int), vol.Range(0, 100)), - } - - async def async_handle(self, intent_obj): - """Handle the hass intent.""" - hass = intent_obj.hass - slots = self.async_validate_slots(intent_obj.slots) - state = hass.helpers.intent.async_match_state( - slots["name"]["value"], - [state for state in hass.states.async_all() if state.domain == DOMAIN], - ) - - service_data = {ATTR_ENTITY_ID: state.entity_id} - speech_parts = [] - - if "color" in slots: - intent.async_test_feature(state, SUPPORT_COLOR, "changing colors") - service_data[ATTR_RGB_COLOR] = slots["color"]["value"] - # Use original passed in value of the color because we don't have - # human readable names for that internally. - speech_parts.append( - "the color {}".format(intent_obj.slots["color"]["value"]) - ) - - if "brightness" in slots: - intent.async_test_feature(state, SUPPORT_BRIGHTNESS, "changing brightness") - service_data[ATTR_BRIGHTNESS_PCT] = slots["brightness"]["value"] - speech_parts.append("{}% brightness".format(slots["brightness"]["value"])) - - await hass.services.async_call(DOMAIN, SERVICE_TURN_ON, service_data) - - response = intent_obj.create_response() - - if not speech_parts: # No attributes changed - speech = f"Turned on {state.name}" - else: - parts = [f"Changed {state.name} to"] - for index, part in enumerate(speech_parts): - if index == 0: - parts.append(f" {part}") - elif index != len(speech_parts) - 1: - parts.append(f", {part}") - else: - parts.append(f" and {part}") - speech = "".join(parts) - - response.async_set_speech(speech) - return response - - async def async_setup(hass, config): """Expose light control via state machine and services.""" component = hass.data[DOMAIN] = EntityComponent( @@ -328,19 +255,22 @@ async def async_setup(hass, config): DOMAIN, SERVICE_TURN_ON, async_handle_light_on_service, - schema=LIGHT_TURN_ON_SCHEMA, + schema=cv.make_entity_service_schema(LIGHT_TURN_ON_SCHEMA), ) component.async_register_entity_service( - SERVICE_TURN_OFF, LIGHT_TURN_OFF_SCHEMA, "async_turn_off" + SERVICE_TURN_OFF, + { + ATTR_TRANSITION: VALID_TRANSITION, + ATTR_FLASH: vol.In([FLASH_SHORT, FLASH_LONG]), + }, + "async_turn_off", ) component.async_register_entity_service( - SERVICE_TOGGLE, LIGHT_TOGGLE_SCHEMA, "async_toggle" + SERVICE_TOGGLE, LIGHT_TURN_ON_SCHEMA, "async_toggle" ) - hass.helpers.intent.async_register(SetIntentHandler()) - return True @@ -460,8 +390,8 @@ class Light(ToggleEntity): return None @property - def state_attributes(self): - """Return optional state attributes.""" + def capability_attributes(self): + """Return capability attributes.""" data = {} supported_features = self.supported_features @@ -472,25 +402,35 @@ class Light(ToggleEntity): if supported_features & SUPPORT_EFFECT: data[ATTR_EFFECT_LIST] = self.effect_list - if self.is_on: - if supported_features & SUPPORT_BRIGHTNESS: - data[ATTR_BRIGHTNESS] = self.brightness + return data - if supported_features & SUPPORT_COLOR_TEMP: - data[ATTR_COLOR_TEMP] = self.color_temp + @property + def state_attributes(self): + """Return state attributes.""" + if not self.is_on: + return None - if supported_features & SUPPORT_COLOR and self.hs_color: - # pylint: disable=unsubscriptable-object,not-an-iterable - 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) + data = {} + supported_features = self.supported_features - if supported_features & SUPPORT_WHITE_VALUE: - data[ATTR_WHITE_VALUE] = self.white_value + if supported_features & SUPPORT_BRIGHTNESS: + data[ATTR_BRIGHTNESS] = self.brightness - if supported_features & SUPPORT_EFFECT: - data[ATTR_EFFECT] = self.effect + if supported_features & SUPPORT_COLOR_TEMP: + data[ATTR_COLOR_TEMP] = self.color_temp + + if supported_features & SUPPORT_COLOR and self.hs_color: + # pylint: disable=unsubscriptable-object,not-an-iterable + 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 supported_features & SUPPORT_WHITE_VALUE: + data[ATTR_WHITE_VALUE] = self.white_value + + if supported_features & SUPPORT_EFFECT: + data[ATTR_EFFECT] = self.effect return {key: val for key, val in data.items() if val is not None} diff --git a/homeassistant/components/light/device_action.py b/homeassistant/components/light/device_action.py index 9d8ef6bceaf..c436ce7886a 100644 --- a/homeassistant/components/light/device_action.py +++ b/homeassistant/components/light/device_action.py @@ -1,13 +1,14 @@ """Provides device actions for lights.""" from typing import List + import voluptuous as vol -from homeassistant.core import HomeAssistant, Context from homeassistant.components.device_automation import toggle_entity from homeassistant.const import CONF_DOMAIN -from homeassistant.helpers.typing import TemplateVarsType, ConfigType -from . import DOMAIN +from homeassistant.core import Context, HomeAssistant +from homeassistant.helpers.typing import ConfigType, TemplateVarsType +from . import DOMAIN ACTION_SCHEMA = toggle_entity.ACTION_SCHEMA.extend({vol.Required(CONF_DOMAIN): DOMAIN}) diff --git a/homeassistant/components/light/device_condition.py b/homeassistant/components/light/device_condition.py index e87ae3bf945..d27953749f6 100644 --- a/homeassistant/components/light/device_condition.py +++ b/homeassistant/components/light/device_condition.py @@ -1,14 +1,15 @@ """Provides device conditions for lights.""" from typing import Dict, List + import voluptuous as vol -from homeassistant.core import HomeAssistant from homeassistant.components.device_automation import toggle_entity from homeassistant.const import CONF_DOMAIN -from homeassistant.helpers.typing import ConfigType +from homeassistant.core import HomeAssistant from homeassistant.helpers.condition import ConditionCheckerType -from . import DOMAIN +from homeassistant.helpers.typing import ConfigType +from . import DOMAIN CONDITION_SCHEMA = toggle_entity.CONDITION_SCHEMA.extend( {vol.Required(CONF_DOMAIN): DOMAIN} diff --git a/homeassistant/components/light/device_trigger.py b/homeassistant/components/light/device_trigger.py index 432d24d3c14..066d1f4c020 100644 --- a/homeassistant/components/light/device_trigger.py +++ b/homeassistant/components/light/device_trigger.py @@ -1,14 +1,15 @@ """Provides device trigger for lights.""" from typing import List + import voluptuous as vol -from homeassistant.core import HomeAssistant, CALLBACK_TYPE from homeassistant.components.automation import AutomationActionType from homeassistant.components.device_automation import toggle_entity from homeassistant.const import CONF_DOMAIN +from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers.typing import ConfigType -from . import DOMAIN +from . import DOMAIN TRIGGER_SCHEMA = toggle_entity.TRIGGER_SCHEMA.extend( {vol.Required(CONF_DOMAIN): DOMAIN} diff --git a/homeassistant/components/light/intent.py b/homeassistant/components/light/intent.py new file mode 100644 index 00000000000..ea8899c44fc --- /dev/null +++ b/homeassistant/components/light/intent.py @@ -0,0 +1,83 @@ +"""Intents for the light integration.""" +import voluptuous as vol + +from homeassistant.core import HomeAssistant +from homeassistant.helpers import intent +import homeassistant.helpers.config_validation as cv +import homeassistant.util.color as color_util + +from . import ( + ATTR_BRIGHTNESS_PCT, + ATTR_ENTITY_ID, + ATTR_RGB_COLOR, + DOMAIN, + SERVICE_TURN_ON, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, +) + +INTENT_SET = "HassLightSet" + + +async def async_setup_intents(hass: HomeAssistant) -> None: + """Set up the light intents.""" + hass.helpers.intent.async_register(SetIntentHandler()) + + +class SetIntentHandler(intent.IntentHandler): + """Handle set color intents.""" + + intent_type = INTENT_SET + slot_schema = { + vol.Required("name"): cv.string, + vol.Optional("color"): color_util.color_name_to_rgb, + vol.Optional("brightness"): vol.All(vol.Coerce(int), vol.Range(0, 100)), + } + + async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse: + """Handle the hass intent.""" + hass = intent_obj.hass + slots = self.async_validate_slots(intent_obj.slots) + state = hass.helpers.intent.async_match_state( + slots["name"]["value"], + [state for state in hass.states.async_all() if state.domain == DOMAIN], + ) + + service_data = {ATTR_ENTITY_ID: state.entity_id} + speech_parts = [] + + if "color" in slots: + intent.async_test_feature(state, SUPPORT_COLOR, "changing colors") + service_data[ATTR_RGB_COLOR] = slots["color"]["value"] + # Use original passed in value of the color because we don't have + # human readable names for that internally. + speech_parts.append( + "the color {}".format(intent_obj.slots["color"]["value"]) + ) + + if "brightness" in slots: + intent.async_test_feature(state, SUPPORT_BRIGHTNESS, "changing brightness") + service_data[ATTR_BRIGHTNESS_PCT] = slots["brightness"]["value"] + speech_parts.append("{}% brightness".format(slots["brightness"]["value"])) + + await hass.services.async_call( + DOMAIN, SERVICE_TURN_ON, service_data, context=intent_obj.context + ) + + response = intent_obj.create_response() + + if not speech_parts: # No attributes changed + speech = f"Turned on {state.name}" + else: + parts = [f"Changed {state.name} to"] + for index, part in enumerate(speech_parts): + if index == 0: + parts.append(f" {part}") + elif index != len(speech_parts) - 1: + parts.append(f", {part}") + else: + parts.append(f" and {part}") + speech = "".join(parts) + + response.async_set_speech(speech) + return response diff --git a/homeassistant/components/light/reproduce_state.py b/homeassistant/components/light/reproduce_state.py index c84b3627bed..59a4b0306d0 100644 --- a/homeassistant/components/light/reproduce_state.py +++ b/homeassistant/components/light/reproduce_state.py @@ -6,16 +6,15 @@ from typing import Iterable, Optional from homeassistant.const import ( ATTR_ENTITY_ID, - STATE_ON, - STATE_OFF, SERVICE_TURN_OFF, SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, ) from homeassistant.core import Context, State from homeassistant.helpers.typing import HomeAssistantType from . import ( - DOMAIN, ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_NAME, @@ -29,6 +28,7 @@ from . import ( ATTR_TRANSITION, ATTR_WHITE_VALUE, ATTR_XY_COLOR, + DOMAIN, ) _LOGGER = logging.getLogger(__name__) @@ -45,13 +45,14 @@ ATTR_GROUP = [ ] COLOR_GROUP = [ - ATTR_COLOR_NAME, - ATTR_COLOR_TEMP, ATTR_HS_COLOR, - ATTR_KELVIN, - ATTR_PROFILE, + ATTR_COLOR_TEMP, ATTR_RGB_COLOR, ATTR_XY_COLOR, + # The following color attributes are deprecated + ATTR_PROFILE, + ATTR_COLOR_NAME, + ATTR_KELVIN, ] DEPRECATED_GROUP = [ diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index 9173f79f964..449e5ea5aaf 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -119,101 +119,3 @@ toggle: values: - colorloop - random - -lifx_set_state: - description: Set a color/brightness and possibliy turn the light on/off. - fields: - entity_id: - description: Name(s) of entities to set a state on. - example: "light.garage" - "...": - description: All turn_on parameters can be used to specify a color. - infrared: - description: Automatic infrared level (0..255) when light brightness is low. - example: 255 - zones: - description: List of zone numbers to affect (8 per LIFX Z, starts at 0). - example: "[0,5]" - transition: - description: Duration in seconds it takes to get to the final state. - example: 10 - power: - description: Turn the light on (True) or off (False). Leave out to keep the power as it is. - example: True - -lifx_effect_pulse: - description: Run a flash effect by changing to a color and back. - fields: - entity_id: - description: Name(s) of entities to run the effect on. - example: "light.kitchen" - mode: - description: "Decides how colors are changed. Possible values: blink, breathe, ping, strobe, solid." - example: strobe - brightness: - description: Number between 0..255 indicating brightness of the temporary color. - example: 120 - color_name: - description: A human readable color name. - example: "red" - rgb_color: - description: The temporary color in RGB-format. - example: "[255, 100, 100]" - period: - description: Duration of the effect in seconds (default 1.0). - example: 3 - cycles: - description: Number of times the effect should run (default 1.0). - example: 2 - power_on: - description: Powered off lights are temporarily turned on during the effect (default True). - example: False - -lifx_effect_colorloop: - description: Run an effect with looping colors. - fields: - entity_id: - description: Name(s) of entities to run the effect on. - example: "light.disco1, light.disco2, light.disco3" - brightness: - description: Number between 0 and 255 indicating brightness of the effect. Leave this out to maintain the current brightness of each participating light. - example: 120 - period: - description: Duration (in seconds) between color changes (default 60). - example: 180 - change: - description: Hue movement per period, in degrees on a color wheel (ranges from 0 to 360, default 20). - example: 45 - spread: - description: Maximum hue difference between participating lights, in degrees on a color wheel (ranges from 0 to 360, default 30). - example: 0 - power_on: - description: Powered off lights are temporarily turned on during the effect (default True). - example: False - -lifx_effect_stop: - description: Stop a running effect. - fields: - entity_id: - description: Name(s) of entities to stop effects on. Leave out to stop effects everywhere. - example: "light.bedroom" - -xiaomi_miio_set_scene: - description: Set a fixed scene. - fields: - entity_id: - description: Name of the light entity. - example: "light.xiaomi_miio" - scene: - description: Number of the fixed scene, between 1 and 4. - example: 1 - -xiaomi_miio_set_delayed_turn_off: - description: Delayed turn off. - fields: - entity_id: - description: Name of the light entity. - example: "light.xiaomi_miio" - time_period: - description: Time period for the delayed turn off. - example: "5, '0:05', {'minutes': 5}" diff --git a/homeassistant/components/lightwave/__init__.py b/homeassistant/components/lightwave/__init__.py index f3445a3c94a..4a27d4a7f4a 100644 --- a/homeassistant/components/lightwave/__init__.py +++ b/homeassistant/components/lightwave/__init__.py @@ -1,7 +1,9 @@ """Support for device connected via Lightwave WiFi-link hub.""" +from lightwave.lightwave import LWLink import voluptuous as vol -import homeassistant.helpers.config_validation as cv + from homeassistant.const import CONF_HOST, CONF_LIGHTS, CONF_NAME, CONF_SWITCHES +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import async_load_platform LIGHTWAVE_LINK = "lightwave_link" @@ -32,7 +34,6 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass, config): """Try to start embedded Lightwave broker.""" - from lightwave.lightwave import LWLink host = config[DOMAIN][CONF_HOST] hass.data[LIGHTWAVE_LINK] = LWLink(host) diff --git a/homeassistant/components/lightwave/manifest.json b/homeassistant/components/lightwave/manifest.json index 4b2456f0df5..c39a8a6bae8 100644 --- a/homeassistant/components/lightwave/manifest.json +++ b/homeassistant/components/lightwave/manifest.json @@ -3,7 +3,7 @@ "name": "Lightwave", "documentation": "https://www.home-assistant.io/integrations/lightwave", "requirements": [ - "lightwave==0.15" + "lightwave==0.17" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/limitlessled/light.py b/homeassistant/components/limitlessled/light.py index be320ee4307..e0ef635ae87 100644 --- a/homeassistant/components/limitlessled/light.py +++ b/homeassistant/components/limitlessled/light.py @@ -1,9 +1,16 @@ """Support for LimitlessLED bulbs.""" import logging +from limitlessled import Color +from limitlessled.bridge import Bridge +from limitlessled.group.dimmer import DimmerGroup +from limitlessled.group.rgbw import RgbwGroup +from limitlessled.group.rgbww import RgbwwGroup +from limitlessled.group.white import WhiteGroup +from limitlessled.pipeline import Pipeline +from limitlessled.presets import COLORLOOP import voluptuous as vol -from homeassistant.const import CONF_NAME, CONF_HOST, CONF_PORT, CONF_TYPE, STATE_ON from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, @@ -14,18 +21,19 @@ from homeassistant.components.light import ( EFFECT_COLORLOOP, EFFECT_WHITE, FLASH_LONG, + PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, - SUPPORT_COLOR, SUPPORT_TRANSITION, Light, - PLATFORM_SCHEMA, ) +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, CONF_TYPE, STATE_ON import homeassistant.helpers.config_validation as cv -from homeassistant.util.color import color_temperature_mired_to_kelvin, color_hs_to_RGB from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.util.color import color_hs_to_RGB, color_temperature_mired_to_kelvin _LOGGER = logging.getLogger(__name__) @@ -137,7 +145,6 @@ def rewrite_legacy(config): def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the LimitlessLED lights.""" - from limitlessled.bridge import Bridge # Two legacy configuration formats are supported to maintain backwards # compatibility. @@ -172,7 +179,6 @@ def state(new_state): # pylint: disable=protected-access def wrapper(self, **kwargs): """Wrap a group state change.""" - from limitlessled.pipeline import Pipeline pipeline = Pipeline() transition_time = DEFAULT_TRANSITION @@ -199,10 +205,6 @@ class LimitlessLEDGroup(Light, RestoreEntity): def __init__(self, group, config): """Initialize a group.""" - from limitlessled.group.rgbw import RgbwGroup - from limitlessled.group.white import WhiteGroup - from limitlessled.group.dimmer import DimmerGroup - from limitlessled.group.rgbww import RgbwwGroup if isinstance(group, WhiteGroup): self._supported = SUPPORT_LIMITLESSLED_WHITE @@ -366,8 +368,6 @@ class LimitlessLEDGroup(Light, RestoreEntity): # Add effects. if ATTR_EFFECT in kwargs and self._effect_list: if kwargs[ATTR_EFFECT] == EFFECT_COLORLOOP: - from limitlessled.presets import COLORLOOP - self._effect = EFFECT_COLORLOOP pipeline.append(COLORLOOP) if kwargs[ATTR_EFFECT] == EFFECT_WHITE: @@ -389,6 +389,5 @@ class LimitlessLEDGroup(Light, RestoreEntity): def limitlessled_color(self): """Convert Home Assistant HS list to RGB Color tuple.""" - from limitlessled import Color return Color(*color_hs_to_RGB(*tuple(self._color))) diff --git a/homeassistant/components/linksys_smart/device_tracker.py b/homeassistant/components/linksys_smart/device_tracker.py index 1af84a4c4ab..a2a8e317133 100644 --- a/homeassistant/components/linksys_smart/device_tracker.py +++ b/homeassistant/components/linksys_smart/device_tracker.py @@ -4,13 +4,13 @@ import logging import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST +import homeassistant.helpers.config_validation as cv DEFAULT_TIMEOUT = 10 diff --git a/homeassistant/components/linky/.translations/fr.json b/homeassistant/components/linky/.translations/fr.json index af12c2b654d..6ff99c41a16 100644 --- a/homeassistant/components/linky/.translations/fr.json +++ b/homeassistant/components/linky/.translations/fr.json @@ -8,7 +8,7 @@ "enedis": "Erreur d'Enedis.fr: merci de r\u00e9essayer plus tard (pas entre 23h et 2h)", "unknown": "Erreur inconnue: merci de r\u00e9essayer plus tard (pas entre 23h et 2h)", "username_exists": "Compte d\u00e9j\u00e0 configur\u00e9", - "wrong_login": "Impossible de vous identifier: merci de v\u00e9rifier vos identifiants" + "wrong_login": "Erreur de connexion: veuillez v\u00e9rifier votre e-mail et votre mot de passe" }, "step": { "user": { diff --git a/homeassistant/components/linky/.translations/pt.json b/homeassistant/components/linky/.translations/pt.json index daf1ce75181..67e742c5813 100644 --- a/homeassistant/components/linky/.translations/pt.json +++ b/homeassistant/components/linky/.translations/pt.json @@ -10,7 +10,7 @@ "user": { "data": { "password": "Palavra-passe", - "username": "" + "username": "O email" } } } diff --git a/homeassistant/components/linky/.translations/ru.json b/homeassistant/components/linky/.translations/ru.json index 463343490a7..da34fbbdb62 100644 --- a/homeassistant/components/linky/.translations/ru.json +++ b/homeassistant/components/linky/.translations/ru.json @@ -1,13 +1,13 @@ { "config": { "abort": { - "username_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430." + "username_exists": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430." }, "error": { "access": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f \u043a Enedis.fr, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443.", "enedis": "Enedis.fr \u043e\u0442\u043f\u0440\u0430\u0432\u0438\u043b \u043e\u0442\u0432\u0435\u0442 \u0441 \u043e\u0448\u0438\u0431\u043a\u043e\u0439: \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435 (\u043d\u0435 \u0432 \u043f\u0440\u043e\u043c\u0435\u0436\u0443\u0442\u043a\u0435 \u0441 23:00 \u043f\u043e 2:00).", "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430: \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435 (\u043d\u0435 \u0432 \u043f\u0440\u043e\u043c\u0435\u0436\u0443\u0442\u043a\u0435 \u0441 23:00 \u043f\u043e 2:00).", - "username_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430.", + "username_exists": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430.", "wrong_login": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0432\u0445\u043e\u0434\u0430: \u043f\u0440\u043e\u0432\u0435\u0440\u044c\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." }, "step": { @@ -16,7 +16,7 @@ "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0412\u0430\u0448\u0438 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", + "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": "Linky" } }, diff --git a/homeassistant/components/linky/config_flow.py b/homeassistant/components/linky/config_flow.py index 3b882eed2ad..8a2d307ceab 100644 --- a/homeassistant/components/linky/config_flow.py +++ b/homeassistant/components/linky/config_flow.py @@ -1,7 +1,6 @@ """Config flow to configure the Linky integration.""" import logging -import voluptuous as vol from pylinky.client import LinkyClient from pylinky.exceptions import ( PyLinkyAccessException, @@ -9,6 +8,7 @@ from pylinky.exceptions import ( PyLinkyException, PyLinkyWrongLoginException, ) +import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME diff --git a/homeassistant/components/linky/sensor.py b/homeassistant/components/linky/sensor.py index 489e66c2b12..4b5f9ab6cad 100644 --- a/homeassistant/components/linky/sensor.py +++ b/homeassistant/components/linky/sensor.py @@ -1,10 +1,9 @@ """Support for Linky.""" +from datetime import timedelta import json import logging -from datetime import timedelta -from pylinky.client import DAILY, MONTHLY, YEARLY, LinkyClient -from pylinky.client import PyLinkyException +from pylinky.client import DAILY, MONTHLY, YEARLY, LinkyClient, PyLinkyException from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( diff --git a/homeassistant/components/litejet/__init__.py b/homeassistant/components/litejet/__init__.py index 6df79c7c968..1d3ab4f6b19 100644 --- a/homeassistant/components/litejet/__init__.py +++ b/homeassistant/components/litejet/__init__.py @@ -1,11 +1,12 @@ """Support for the LiteJet lighting system.""" import logging +from pylitejet import LiteJet import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers import discovery from homeassistant.const import CONF_PORT +from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -30,7 +31,6 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): """Set up the LiteJet component.""" - from pylitejet import LiteJet url = config[DOMAIN].get(CONF_PORT) diff --git a/homeassistant/components/liveboxplaytv/manifest.json b/homeassistant/components/liveboxplaytv/manifest.json index bcb2b53f081..39692b1e282 100644 --- a/homeassistant/components/liveboxplaytv/manifest.json +++ b/homeassistant/components/liveboxplaytv/manifest.json @@ -3,8 +3,8 @@ "name": "Liveboxplaytv", "documentation": "https://www.home-assistant.io/integrations/liveboxplaytv", "requirements": [ - "liveboxplaytv==2.0.2", - "pyteleloisirs==3.5" + "liveboxplaytv==2.0.3", + "pyteleloisirs==3.6" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/liveboxplaytv/media_player.py b/homeassistant/components/liveboxplaytv/media_player.py index 996b4f33b50..66fb383d677 100644 --- a/homeassistant/components/liveboxplaytv/media_player.py +++ b/homeassistant/components/liveboxplaytv/media_player.py @@ -7,7 +7,7 @@ import pyteleloisirs import requests import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, SUPPORT_NEXT_TRACK, diff --git a/homeassistant/components/llamalab_automate/notify.py b/homeassistant/components/llamalab_automate/notify.py index ab6a7032208..5a3d4e0df38 100644 --- a/homeassistant/components/llamalab_automate/notify.py +++ b/homeassistant/components/llamalab_automate/notify.py @@ -4,11 +4,10 @@ import logging import requests import voluptuous as vol +from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService from homeassistant.const import CONF_API_KEY, CONF_DEVICE from homeassistant.helpers import config_validation as cv -from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService - _LOGGER = logging.getLogger(__name__) _RESOURCE = "https://llamalab.com/automate/cloud/message" diff --git a/homeassistant/components/local_file/camera.py b/homeassistant/components/local_file/camera.py index d705cefc3fc..1d06efeb708 100644 --- a/homeassistant/components/local_file/camera.py +++ b/homeassistant/components/local_file/camera.py @@ -5,21 +5,23 @@ import os import voluptuous as vol -from homeassistant.const import CONF_NAME, ATTR_ENTITY_ID from homeassistant.components.camera import ( - Camera, CAMERA_SERVICE_SCHEMA, PLATFORM_SCHEMA, + Camera, ) -from homeassistant.components.camera.const import DOMAIN +from homeassistant.const import ATTR_ENTITY_ID, CONF_NAME from homeassistant.helpers import config_validation as cv -_LOGGER = logging.getLogger(__name__) +from .const import ( + CONF_FILE_PATH, + DATA_LOCAL_FILE, + DEFAULT_NAME, + DOMAIN, + SERVICE_UPDATE_FILE_PATH, +) -CONF_FILE_PATH = "file_path" -DATA_LOCAL_FILE = "local_file_cameras" -DEFAULT_NAME = "Local File" -SERVICE_UPDATE_FILE_PATH = "local_file_update_file_path" +_LOGGER = logging.getLogger(__name__) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { diff --git a/homeassistant/components/local_file/const.py b/homeassistant/components/local_file/const.py new file mode 100644 index 00000000000..5225a70daed --- /dev/null +++ b/homeassistant/components/local_file/const.py @@ -0,0 +1,6 @@ +"""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_file/services.yaml b/homeassistant/components/local_file/services.yaml index b359b411b6a..b8c615f3335 100644 --- a/homeassistant/components/local_file/services.yaml +++ b/homeassistant/components/local_file/services.yaml @@ -1,4 +1,4 @@ -local_file_update_file_path: +update_file_path: description: Use this service to change the file displayed by the camera. fields: entity_id: diff --git a/homeassistant/components/local_ip/.translations/ca.json b/homeassistant/components/local_ip/.translations/ca.json new file mode 100644 index 00000000000..b2b7ee89c16 --- /dev/null +++ b/homeassistant/components/local_ip/.translations/ca.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Integraci\u00f3 ja configurada amb un sensor amb aquest nom" + }, + "step": { + "user": { + "data": { + "name": "Nom del sensor" + }, + "title": "Adre\u00e7a IP local" + } + }, + "title": "Adre\u00e7a IP local" + } +} \ No newline at end of file diff --git a/homeassistant/components/local_ip/.translations/da.json b/homeassistant/components/local_ip/.translations/da.json new file mode 100644 index 00000000000..c0396ccb182 --- /dev/null +++ b/homeassistant/components/local_ip/.translations/da.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Integration er allerede konfigureret med en eksisterende sensor med det navn" + }, + "step": { + "user": { + "data": { + "name": "Sensornavn" + }, + "title": "Lokal IP-adresse" + } + }, + "title": "Lokal IP-adresse" + } +} \ No newline at end of file diff --git a/homeassistant/components/local_ip/.translations/en.json b/homeassistant/components/local_ip/.translations/en.json new file mode 100644 index 00000000000..869bb5a23d5 --- /dev/null +++ b/homeassistant/components/local_ip/.translations/en.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Integration is already configured with an existing sensor with that name" + }, + "step": { + "user": { + "data": { + "name": "Sensor Name" + }, + "title": "Local IP Address" + } + }, + "title": "Local IP Address" + } +} \ No newline at end of file diff --git a/homeassistant/components/local_ip/.translations/ko.json b/homeassistant/components/local_ip/.translations/ko.json new file mode 100644 index 00000000000..a00a130bfca --- /dev/null +++ b/homeassistant/components/local_ip/.translations/ko.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uac00 \uc774\ubbf8 \ud574\ub2f9 \uc774\ub984\uc758 \uc13c\uc11c\ub85c \uad6c\uc131\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "name": "\uc13c\uc11c \uc774\ub984" + }, + "title": "\ub85c\uceec IP \uc8fc\uc18c" + } + }, + "title": "\ub85c\uceec IP \uc8fc\uc18c" + } +} \ No newline at end of file diff --git a/homeassistant/components/local_ip/.translations/ru.json b/homeassistant/components/local_ip/.translations/ru.json new file mode 100644 index 00000000000..de92b9680f0 --- /dev/null +++ b/homeassistant/components/local_ip/.translations/ru.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430 \u0441 \u0442\u0430\u043a\u0438\u043c \u0436\u0435 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435\u043c \u0434\u0430\u0442\u0447\u0438\u043a\u0430." + }, + "step": { + "user": { + "data": { + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" + }, + "title": "\u041b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441" + } + }, + "title": "\u041b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441" + } +} \ No newline at end of file diff --git a/homeassistant/components/local_ip/__init__.py b/homeassistant/components/local_ip/__init__.py new file mode 100644 index 00000000000..c93b7a5a81b --- /dev/null +++ b/homeassistant/components/local_ip/__init__.py @@ -0,0 +1,42 @@ +"""Get the local IP address of the Home Assistant instance.""" +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant +import homeassistant.helpers.config_validation as cv + +DOMAIN = "local_ip" +PLATFORM = "sensor" + +CONFIG_SCHEMA = vol.Schema( + {DOMAIN: vol.Schema({vol.Optional(CONF_NAME, default=DOMAIN): cv.string})}, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up local_ip from configuration.yaml.""" + conf = config.get(DOMAIN) + if conf: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, data=conf, context={"source": config_entries.SOURCE_IMPORT} + ) + ) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: config_entries.ConfigEntry): + """Set up local_ip from a config entry.""" + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, PLATFORM) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: config_entries.ConfigEntry): + """Unload a config entry.""" + return await hass.config_entries.async_forward_entry_unload(entry, PLATFORM) diff --git a/homeassistant/components/local_ip/config_flow.py b/homeassistant/components/local_ip/config_flow.py new file mode 100644 index 00000000000..58a666a68f3 --- /dev/null +++ b/homeassistant/components/local_ip/config_flow.py @@ -0,0 +1,34 @@ +"""Config flow for local_ip.""" +import voluptuous as vol + +from homeassistant import config_entries + +from . import DOMAIN + + +class SimpleConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for local_ip.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + if user_input is not None: + if any( + user_input["name"] == entry.data["name"] + for entry in self._async_current_entries() + ): + return self.async_abort(reason="already_configured") + + return self.async_create_entry(title=user_input["name"], data=user_input) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema({vol.Required("name", default=DOMAIN): str}), + errors={}, + ) + + async def async_step_import(self, import_info): + """Handle import from config file.""" + return await self.async_step_user(import_info) diff --git a/homeassistant/components/local_ip/manifest.json b/homeassistant/components/local_ip/manifest.json new file mode 100644 index 00000000000..4e97c32afa0 --- /dev/null +++ b/homeassistant/components/local_ip/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "local_ip", + "name": "Local IP Address", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/local_ip", + "dependencies": [], + "codeowners": ["@issacg"], + "requirements": [] +} diff --git a/homeassistant/components/local_ip/sensor.py b/homeassistant/components/local_ip/sensor.py new file mode 100644 index 00000000000..274a11faec6 --- /dev/null +++ b/homeassistant/components/local_ip/sensor.py @@ -0,0 +1,34 @@ +"""Sensor platform for local_ip.""" + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import Entity +from homeassistant.util import get_local_ip + + +async def async_setup_entry(hass: HomeAssistant, config_entry, async_add_entities): + """Set up the platform from config_entry.""" + name = config_entry.data["name"] + async_add_entities([IPSensor(name)], True) + + +class IPSensor(Entity): + """A simple sensor.""" + + def __init__(self, name: str): + """Initialize the sensor.""" + self._state = None + self._name = name + + @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 + + def update(self): + """Fetch new state data for the sensor.""" + self._state = get_local_ip() diff --git a/homeassistant/components/local_ip/strings.json b/homeassistant/components/local_ip/strings.json new file mode 100644 index 00000000000..43a88be3325 --- /dev/null +++ b/homeassistant/components/local_ip/strings.json @@ -0,0 +1,16 @@ +{ + "config": { + "title": "Local IP Address", + "step": { + "user": { + "title": "Local IP Address", + "data": { + "name": "Sensor Name" + } + } + }, + "abort": { + "already_configured": "Integration is already configured with an existing sensor with that name" + } + } +} diff --git a/homeassistant/components/locative/.translations/da.json b/homeassistant/components/locative/.translations/da.json index 8211d52fa5d..3752b23bbe3 100644 --- a/homeassistant/components/locative/.translations/da.json +++ b/homeassistant/components/locative/.translations/da.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "not_internet_accessible": "Dit Home Assistant system skal v\u00e6re tilg\u00e6ngeligt fra internettet for at modtage Geofency meddelelser.", + "not_internet_accessible": "Din Home Assistant-instans skal v\u00e6re tilg\u00e6ngelig fra internettet for at modtage Geofency-meddelelser.", "one_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning" }, "create_entry": { diff --git a/homeassistant/components/locative/.translations/ko.json b/homeassistant/components/locative/.translations/ko.json index 0649ed557c4..c53f538799f 100644 --- a/homeassistant/components/locative/.translations/ko.json +++ b/homeassistant/components/locative/.translations/ko.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "Locative Webhook \uc744 \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "Locative Webhook \uc744 \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "Locative Webhook \uc124\uc815" } }, diff --git a/homeassistant/components/locative/.translations/zh-Hant.json b/homeassistant/components/locative/.translations/zh-Hant.json index 7dd598c8fc2..5135eb33c9f 100644 --- a/homeassistant/components/locative/.translations/zh-Hant.json +++ b/homeassistant/components/locative/.translations/zh-Hant.json @@ -5,7 +5,7 @@ "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002" }, "create_entry": { - "default": "\u6b32\u50b3\u9001\u4f4d\u7f6e\u81f3 Home Assistant\uff0c\u5c07\u9700\u65bc Locative App \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": "\u6b32\u50b3\u9001\u5ea7\u6a19\u81f3 Home Assistant\uff0c\u5c07\u9700\u65bc Locative App \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" }, "step": { "user": { diff --git a/homeassistant/components/locative/__init__.py b/homeassistant/components/locative/__init__.py index ed8bcb6e7e5..ea36aa9f7fb 100644 --- a/homeassistant/components/locative/__init__.py +++ b/homeassistant/components/locative/__init__.py @@ -2,21 +2,21 @@ import logging from typing import Dict -import voluptuous as vol from aiohttp import web +import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER from homeassistant.const import ( - HTTP_UNPROCESSABLE_ENTITY, + ATTR_ID, ATTR_LATITUDE, ATTR_LONGITUDE, - STATE_NOT_HOME, CONF_WEBHOOK_ID, - ATTR_ID, HTTP_OK, + HTTP_UNPROCESSABLE_ENTITY, + STATE_NOT_HOME, ) from homeassistant.helpers import config_entry_flow +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/locative/config_flow.py b/homeassistant/components/locative/config_flow.py index b4fb43d5e4e..a1ac8263416 100644 --- a/homeassistant/components/locative/config_flow.py +++ b/homeassistant/components/locative/config_flow.py @@ -1,7 +1,7 @@ """Config flow for Locative.""" from homeassistant.helpers import config_entry_flow -from .const import DOMAIN +from .const import DOMAIN config_entry_flow.register_webhook_flow( DOMAIN, diff --git a/homeassistant/components/locative/device_tracker.py b/homeassistant/components/locative/device_tracker.py index c92847930a0..ef247954171 100644 --- a/homeassistant/components/locative/device_tracker.py +++ b/homeassistant/components/locative/device_tracker.py @@ -1,9 +1,9 @@ """Support for the Locative platform.""" import logging -from homeassistant.core import callback from homeassistant.components.device_tracker import SOURCE_TYPE_GPS from homeassistant.components.device_tracker.config_entry import TrackerEntity +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import DOMAIN as LT_DOMAIN, TRACKER_UPDATE diff --git a/homeassistant/components/lock/.translations/bg.json b/homeassistant/components/lock/.translations/bg.json index 0e77bcf1033..54b80842f4f 100644 --- a/homeassistant/components/lock/.translations/bg.json +++ b/homeassistant/components/lock/.translations/bg.json @@ -8,6 +8,10 @@ "condition_type": { "is_locked": "{entity_name} \u0435 \u0437\u0430\u043a\u043b\u044e\u0447\u0435\u043d", "is_unlocked": "{entity_name} \u0435 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d" + }, + "trigger_type": { + "locked": "{entity_name} \u0437\u0430\u043a\u043b\u044e\u0447\u0435\u043d", + "unlocked": "{entity_name} \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d" } } } \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/da.json b/homeassistant/components/lock/.translations/da.json index de4f603ac43..e2f2588349c 100644 --- a/homeassistant/components/lock/.translations/da.json +++ b/homeassistant/components/lock/.translations/da.json @@ -2,11 +2,16 @@ "device_automation": { "action_type": { "lock": "L\u00e5s {entity_name}", - "open": "\u00c5ben {entity_name}" + "open": "\u00c5bn {entity_name}", + "unlock": "L\u00e5s {entity_name} op" }, "condition_type": { "is_locked": "{entity_name} er l\u00e5st", "is_unlocked": "{entity_name} er l\u00e5st op" + }, + "trigger_type": { + "locked": "{entity_name} blev l\u00e5st", + "unlocked": "{entity_name} l\u00e5st op" } } } \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/hu.json b/homeassistant/components/lock/.translations/hu.json new file mode 100644 index 00000000000..20b1663558b --- /dev/null +++ b/homeassistant/components/lock/.translations/hu.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "lock": "{entity_name} z\u00e1r\u00e1sa", + "open": "{entity_name} nyit\u00e1sa", + "unlock": "{entity_name} nyit\u00e1sa" + }, + "condition_type": { + "is_locked": "{entity_name} z\u00e1rva", + "is_unlocked": "{entity_name} nyitva" + }, + "trigger_type": { + "locked": "{entity_name} be lett z\u00e1rva", + "unlocked": "{entity_name} ki lett nyitva" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/ko.json b/homeassistant/components/lock/.translations/ko.json index 6abd9cd60e6..fb202f73b37 100644 --- a/homeassistant/components/lock/.translations/ko.json +++ b/homeassistant/components/lock/.translations/ko.json @@ -6,8 +6,12 @@ "unlock": "{entity_name} \uc7a0\uae08 \ud574\uc81c" }, "condition_type": { - "is_locked": "{entity_name} \uc774(\uac00) \uc7a0\uacbc\uc2b5\ub2c8\ub2e4", - "is_unlocked": "{entity_name} \uc758 \uc7a0\uae08\uc774 \ud574\uc81c\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + "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" } } } \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/nl.json b/homeassistant/components/lock/.translations/nl.json index 099b7308334..61370236a97 100644 --- a/homeassistant/components/lock/.translations/nl.json +++ b/homeassistant/components/lock/.translations/nl.json @@ -8,6 +8,10 @@ "condition_type": { "is_locked": "{entity_name} is vergrendeld", "is_unlocked": "{entity_name} is ontgrendeld" + }, + "trigger_type": { + "locked": "{entity_name} vergrendeld", + "unlocked": "{entity_name} ontgrendeld" } } } \ No newline at end of file diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index 341df1bb28a..a50f687238f 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -5,26 +5,25 @@ import logging import voluptuous as vol -from homeassistant.loader import bind_hass -from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.config_validation import ( # noqa: F401 - ENTITY_SERVICE_SCHEMA, - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) -import homeassistant.helpers.config_validation as cv +from homeassistant.components import group from homeassistant.const import ( ATTR_CODE, ATTR_CODE_FORMAT, + SERVICE_LOCK, + SERVICE_OPEN, + SERVICE_UNLOCK, STATE_LOCKED, STATE_UNLOCKED, - SERVICE_LOCK, - SERVICE_UNLOCK, - SERVICE_OPEN, ) -from homeassistant.components import group - +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.config_validation import ( # noqa: F401 + PLATFORM_SCHEMA, + PLATFORM_SCHEMA_BASE, + make_entity_service_schema, +) +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.loader import bind_hass # mypy: allow-untyped-defs, no-check-untyped-defs @@ -40,7 +39,7 @@ GROUP_NAME_ALL_LOCKS = "all locks" MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) -LOCK_SERVICE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend({vol.Optional(ATTR_CODE): cv.string}) +LOCK_SERVICE_SCHEMA = make_entity_service_schema({vol.Optional(ATTR_CODE): cv.string}) # Bitfield of features supported by the lock entity SUPPORT_OPEN = 1 diff --git a/homeassistant/components/lock/device_action.py b/homeassistant/components/lock/device_action.py index c678bfc17cf..efdb5e352cf 100644 --- a/homeassistant/components/lock/device_action.py +++ b/homeassistant/components/lock/device_action.py @@ -1,21 +1,23 @@ """Provides device automations for Lock.""" -from typing import Optional, List +from typing import List, Optional + import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, - CONF_DOMAIN, - CONF_TYPE, CONF_DEVICE_ID, + CONF_DOMAIN, CONF_ENTITY_ID, + CONF_TYPE, SERVICE_LOCK, SERVICE_OPEN, SERVICE_UNLOCK, ) -from homeassistant.core import HomeAssistant, Context +from homeassistant.core import Context, HomeAssistant from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv + from . import DOMAIN, SUPPORT_OPEN ACTION_TYPES = {"lock", "unlock", "open"} diff --git a/homeassistant/components/lock/device_condition.py b/homeassistant/components/lock/device_condition.py index 328da6ad450..44791320669 100644 --- a/homeassistant/components/lock/device_condition.py +++ b/homeassistant/components/lock/device_condition.py @@ -1,21 +1,23 @@ """Provides device automations for Lock.""" from typing import List + import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, CONF_CONDITION, - CONF_DOMAIN, - CONF_TYPE, CONF_DEVICE_ID, + CONF_DOMAIN, CONF_ENTITY_ID, + CONF_TYPE, STATE_LOCKED, STATE_UNLOCKED, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import condition, config_validation as cv, entity_registry -from homeassistant.helpers.typing import ConfigType, TemplateVarsType from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA +from homeassistant.helpers.typing import ConfigType, TemplateVarsType + from . import DOMAIN CONDITION_TYPES = {"is_locked", "is_unlocked"} diff --git a/homeassistant/components/lock/device_trigger.py b/homeassistant/components/lock/device_trigger.py index 8732cca29f0..9db2822a591 100644 --- a/homeassistant/components/lock/device_trigger.py +++ b/homeassistant/components/lock/device_trigger.py @@ -1,21 +1,23 @@ """Provides device automations for Lock.""" from typing import List + import voluptuous as vol +from homeassistant.components.automation import AutomationActionType, state +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA from homeassistant.const import ( - CONF_DOMAIN, - CONF_TYPE, - CONF_PLATFORM, CONF_DEVICE_ID, + CONF_DOMAIN, CONF_ENTITY_ID, + CONF_PLATFORM, + CONF_TYPE, STATE_LOCKED, STATE_UNLOCKED, ) -from homeassistant.core import HomeAssistant, CALLBACK_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 homeassistant.components.automation import state, AutomationActionType -from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA + from . import DOMAIN TRIGGER_TYPES = {"locked", "unlocked"} diff --git a/homeassistant/components/lock/reproduce_state.py b/homeassistant/components/lock/reproduce_state.py index dc1bee85839..b8b469f943f 100644 --- a/homeassistant/components/lock/reproduce_state.py +++ b/homeassistant/components/lock/reproduce_state.py @@ -5,10 +5,10 @@ from typing import Iterable, Optional from homeassistant.const import ( ATTR_ENTITY_ID, - STATE_LOCKED, - STATE_UNLOCKED, SERVICE_LOCK, SERVICE_UNLOCK, + STATE_LOCKED, + STATE_UNLOCKED, ) from homeassistant.core import Context, State from homeassistant.helpers.typing import HomeAssistantType diff --git a/homeassistant/components/lock/services.yaml b/homeassistant/components/lock/services.yaml index d17e00addd1..fea02bb025e 100644 --- a/homeassistant/components/lock/services.yaml +++ b/homeassistant/components/lock/services.yaml @@ -20,23 +20,6 @@ get_usercode: description: Code slot to retrieve a code from. example: 1 -nuki_lock_n_go: - description: "Nuki Lock 'n' Go" - fields: - entity_id: - description: Entity id of the Nuki lock. - example: 'lock.front_door' - unlatch: - description: Whether to unlatch the lock. - example: false - -nuki_unlatch: - description: Nuki unlatch. - fields: - entity_id: - description: Entity id of the Nuki lock. - example: 'lock.front_door' - lock: description: Lock all or specified locks. fields: @@ -79,66 +62,3 @@ unlock: code: description: An optional code to unlock the lock with. example: 1234 - -wink_set_lock_vacation_mode: - description: Set vacation mode for all or specified locks. Disables all user codes. - fields: - entity_id: - description: Name of lock to unlock. - example: 'lock.front_door' - enabled: - description: enable or disable. true or false. - example: true - -wink_set_lock_alarm_mode: - description: Set alarm mode for all or specified locks. - fields: - entity_id: - description: Name of lock to unlock. - example: 'lock.front_door' - mode: - description: One of tamper, activity, or forced_entry. - example: tamper - -wink_set_lock_alarm_sensitivity: - description: Set alarm sensitivity for all or specified locks. - fields: - entity_id: - description: Name of lock to unlock. - example: 'lock.front_door' - sensitivity: - description: One of low, medium_low, medium, medium_high, high. - example: medium - -wink_set_lock_alarm_state: - description: Set alarm state. - fields: - entity_id: - description: Name of lock to unlock. - example: 'lock.front_door' - enabled: - description: enable or disable. true or false. - example: true - -wink_set_lock_beeper_state: - description: Set beeper state. - fields: - entity_id: - description: Name of lock to unlock. - example: 'lock.front_door' - enabled: - description: enable or disable. true or false. - example: true - -wink_add_new_lock_key_code: - description: Add a new user key code. - fields: - entity_id: - description: Name of lock to unlock. - example: 'lock.front_door' - name: - description: name of the new key code. - example: Bob - code: - description: new key code, length must match length of other codes. Default length is 4. - example: 1234 diff --git a/homeassistant/components/lockitron/lock.py b/homeassistant/components/lockitron/lock.py index b993f644ecd..5840c7f5537 100644 --- a/homeassistant/components/lockitron/lock.py +++ b/homeassistant/components/lockitron/lock.py @@ -4,9 +4,9 @@ import logging import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.components.lock import LockDevice, PLATFORM_SCHEMA +from homeassistant.components.lock import PLATFORM_SCHEMA, LockDevice from homeassistant.const import CONF_ACCESS_TOKEN, CONF_ID +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/logbook/manifest.json b/homeassistant/components/logbook/manifest.json index e8e3ad8ac2e..9d5c78dc34d 100644 --- a/homeassistant/components/logbook/manifest.json +++ b/homeassistant/components/logbook/manifest.json @@ -3,9 +3,7 @@ "name": "Logbook", "documentation": "https://www.home-assistant.io/integrations/logbook", "requirements": [], - "dependencies": [ - "frontend", - "recorder" - ], + "dependencies": ["frontend", "http", "recorder"], + "after_dependencies": ["homekit"], "codeowners": [] } diff --git a/homeassistant/components/logentries/__init__.py b/homeassistant/components/logentries/__init__.py index 3601ee275b8..55d1ab7aae6 100644 --- a/homeassistant/components/logentries/__init__.py +++ b/homeassistant/components/logentries/__init__.py @@ -1,13 +1,13 @@ """Support for sending data to Logentries webhook endpoint.""" import json import logging -import requests +import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_TOKEN, EVENT_STATE_CHANGED from homeassistant.helpers import state as state_helper +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/logger/__init__.py b/homeassistant/components/logger/__init__.py index a2ed19f92b1..8043469d43b 100644 --- a/homeassistant/components/logger/__init__.py +++ b/homeassistant/components/logger/__init__.py @@ -1,6 +1,6 @@ """Support for settting the level of logging for components.""" -import logging from collections import OrderedDict +import logging import voluptuous as vol diff --git a/homeassistant/components/logi_circle/.translations/da.json b/homeassistant/components/logi_circle/.translations/da.json index 9de8d707ad4..1f2a96fe5b4 100644 --- a/homeassistant/components/logi_circle/.translations/da.json +++ b/homeassistant/components/logi_circle/.translations/da.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_setup": "Du kan kun konfigurere en enkelt Logi Circle konto.", - "external_error": "Der opstod en undtagelse fra et andet flow.", + "already_setup": "Du kan kun konfigurere en enkelt Logi Circle-konto.", + "external_error": "Undtagelse skete fra et andet flow.", "external_setup": "Logi Circle er konfigureret med succes fra et andet flow.", "no_flows": "Du skal konfigurere Logi Circle f\u00f8r du kan godkende med det. [L\u00e6s venligst vejledningen](https://www.home-assistant.io/components/logi_circle/)." }, @@ -24,7 +24,7 @@ "flow_impl": "Udbyder" }, "description": "V\u00e6lg via hvilken godkendelsesudbyder du vil godkende med Logi Circle.", - "title": "Godkendelses udbyder" + "title": "Godkendelsesudbyder" } }, "title": "Logi Circle" diff --git a/homeassistant/components/logi_circle/.translations/es.json b/homeassistant/components/logi_circle/.translations/es.json index 4819ff5cdd7..7209bdfefd5 100644 --- a/homeassistant/components/logi_circle/.translations/es.json +++ b/homeassistant/components/logi_circle/.translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_setup": "Solo puedes configurar una cuenta de Logi Circle.", - "external_error": "La excepci\u00f3n se produjo a partir de otro flujo.", + "external_error": "Se produjo una excepci\u00f3n de otro flujo.", "external_setup": "Logi Circle se ha configurado correctamente a partir de otro flujo.", "no_flows": "Es necesario configurar Logi Circle antes de poder autenticarse con \u00e9l. [Echa un vistazo a las instrucciones] (https://www.home-assistant.io/components/logi_circle/)." }, diff --git a/homeassistant/components/logi_circle/.translations/ru.json b/homeassistant/components/logi_circle/.translations/ru.json index 40c7c8853da..9cecf3081b6 100644 --- a/homeassistant/components/logi_circle/.translations/ru.json +++ b/homeassistant/components/logi_circle/.translations/ru.json @@ -16,7 +16,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\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Logi Circle, \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.", + "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 Logi Circle, \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.", "title": "Logi Circle" }, "user": { diff --git a/homeassistant/components/logi_circle/__init__.py b/homeassistant/components/logi_circle/__init__.py index f7ed3a73fce..b77f17101a8 100644 --- a/homeassistant/components/logi_circle/__init__.py +++ b/homeassistant/components/logi_circle/__init__.py @@ -2,7 +2,10 @@ import asyncio import logging +from aiohttp.client_exceptions import ClientResponseError import async_timeout +from logi_circle import LogiCircle +from logi_circle.exception import AuthorizationFailed import voluptuous as vol from homeassistant import config_entries @@ -116,9 +119,6 @@ async def async_setup(hass, config): async def async_setup_entry(hass, entry): """Set up Logi Circle from a config entry.""" - from logi_circle import LogiCircle - from logi_circle.exception import AuthorizationFailed - from aiohttp.client_exceptions import ClientResponseError logi_circle = LogiCircle( client_id=entry.data[CONF_CLIENT_ID], diff --git a/homeassistant/components/logi_circle/config_flow.py b/homeassistant/components/logi_circle/config_flow.py index 2a25c5f00a4..bc585153b64 100644 --- a/homeassistant/components/logi_circle/config_flow.py +++ b/homeassistant/components/logi_circle/config_flow.py @@ -3,6 +3,8 @@ import asyncio from collections import OrderedDict import async_timeout +from logi_circle import LogiCircle +from logi_circle.exception import AuthorizationFailed import voluptuous as vol from homeassistant import config_entries @@ -120,7 +122,6 @@ class LogiCircleFlowHandler(config_entries.ConfigFlow): def _get_authorization_url(self): """Create temporary Circle session and generate authorization url.""" - from logi_circle import LogiCircle flow = self.hass.data[DATA_FLOW_IMPL][self.flow_impl] client_id = flow[CONF_CLIENT_ID] @@ -148,8 +149,6 @@ class LogiCircleFlowHandler(config_entries.ConfigFlow): async def _async_create_session(self, code): """Create Logi Circle session and entries.""" - from logi_circle import LogiCircle - from logi_circle.exception import AuthorizationFailed flow = self.hass.data[DATA_FLOW_IMPL][DOMAIN] client_id = flow[CONF_CLIENT_ID] @@ -208,5 +207,5 @@ class LogiCircleAuthCallbackView(HomeAssistantView): ) return self.json_message("Authorisation code saved") return self.json_message( - "Authorisation code missing " "from query string", status_code=400 + "Authorisation code missing from query string", status_code=400 ) diff --git a/homeassistant/components/logi_circle/manifest.json b/homeassistant/components/logi_circle/manifest.json index 22502956e06..bd6dc8a8d27 100644 --- a/homeassistant/components/logi_circle/manifest.json +++ b/homeassistant/components/logi_circle/manifest.json @@ -4,6 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/logi_circle", "requirements": ["logi_circle==0.2.2"], - "dependencies": ["ffmpeg"], + "dependencies": ["ffmpeg", "http"], "codeowners": ["@evanjd"] } diff --git a/homeassistant/components/london_underground/sensor.py b/homeassistant/components/london_underground/sensor.py index 07881fce40f..12f40f7b461 100644 --- a/homeassistant/components/london_underground/sensor.py +++ b/homeassistant/components/london_underground/sensor.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +from london_tube_status import TubeData import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -43,7 +44,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Tube sensor.""" - from london_tube_status import TubeData data = TubeData() data.update() diff --git a/homeassistant/components/luci/device_tracker.py b/homeassistant/components/luci/device_tracker.py index 59c3251a437..9d71b3d263a 100644 --- a/homeassistant/components/luci/device_tracker.py +++ b/homeassistant/components/luci/device_tracker.py @@ -1,6 +1,7 @@ """Support for OpenWRT (luci) routers.""" import logging +from openwrt_luci_rpc import OpenWrtRpc import voluptuous as vol from homeassistant.components.device_tracker import ( @@ -45,7 +46,6 @@ class LuciDeviceScanner(DeviceScanner): def __init__(self, config): """Initialize the scanner.""" - from openwrt_luci_rpc import OpenWrtRpc self.router = OpenWrtRpc( config[CONF_HOST], diff --git a/homeassistant/components/luftdaten/.translations/da.json b/homeassistant/components/luftdaten/.translations/da.json index d43fc1128ae..3a5f5e7b409 100644 --- a/homeassistant/components/luftdaten/.translations/da.json +++ b/homeassistant/components/luftdaten/.translations/da.json @@ -9,7 +9,7 @@ "user": { "data": { "show_on_map": "Vis p\u00e5 kort", - "station_id": "Luftdaten Sensor ID" + "station_id": "Luftdaten sensor-id" }, "title": "Definer Luftdaten" } diff --git a/homeassistant/components/luftdaten/__init__.py b/homeassistant/components/luftdaten/__init__.py index 3dca82404c0..d797fbbf4ba 100644 --- a/homeassistant/components/luftdaten/__init__.py +++ b/homeassistant/components/luftdaten/__init__.py @@ -34,6 +34,7 @@ SENSOR_HUMIDITY = "humidity" SENSOR_PM10 = "P1" SENSOR_PM2_5 = "P2" SENSOR_PRESSURE = "pressure" +SENSOR_PRESSURE_AT_SEALEVEL = "pressure_at_sealevel" SENSOR_TEMPERATURE = "temperature" TOPIC_UPDATE = f"{DOMAIN}_data_update" @@ -44,6 +45,7 @@ SENSORS = { SENSOR_TEMPERATURE: ["Temperature", "mdi:thermometer", TEMP_CELSIUS], SENSOR_HUMIDITY: ["Humidity", "mdi:water-percent", "%"], SENSOR_PRESSURE: ["Pressure", "mdi:arrow-down-bold", "Pa"], + SENSOR_PRESSURE_AT_SEALEVEL: ["Pressure at sealevel", "mdi:mdi-download", "Pa"], SENSOR_PM10: ["PM10", "mdi:thought-bubble", VOLUME_MICROGRAMS_PER_CUBIC_METER], SENSOR_PM2_5: [ "PM2.5", diff --git a/homeassistant/components/lupusec/alarm_control_panel.py b/homeassistant/components/lupusec/alarm_control_panel.py index 245743d0f65..c6ad817bfbf 100644 --- a/homeassistant/components/lupusec/alarm_control_panel.py +++ b/homeassistant/components/lupusec/alarm_control_panel.py @@ -2,6 +2,10 @@ from datetime import timedelta from homeassistant.components.alarm_control_panel import AlarmControlPanel +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, @@ -51,6 +55,11 @@ class LupusecAlarm(LupusecDevice, AlarmControlPanel): state = None return state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY + def alarm_arm_away(self, code=None): """Send arm away command.""" self._device.set_away() diff --git a/homeassistant/components/lupusec/manifest.json b/homeassistant/components/lupusec/manifest.json index bb6b18243ec..2fea92afd0e 100644 --- a/homeassistant/components/lupusec/manifest.json +++ b/homeassistant/components/lupusec/manifest.json @@ -3,8 +3,8 @@ "name": "Lupusec", "documentation": "https://www.home-assistant.io/integrations/lupusec", "requirements": [ - "lupupy==0.0.17" + "lupupy==0.0.18" ], "dependencies": [], - "codeowners": [] + "codeowners": ["@majuss"] } diff --git a/homeassistant/components/lutron/__init__.py b/homeassistant/components/lutron/__init__.py index ac9a4eab417..ac6ffa46b27 100644 --- a/homeassistant/components/lutron/__init__.py +++ b/homeassistant/components/lutron/__init__.py @@ -1,11 +1,12 @@ """Component for interacting with a Lutron RadioRA 2 system.""" import logging +from pylutron import Button, Lutron import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import ATTR_ID, CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import slugify @@ -36,7 +37,6 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, base_config): """Set up the Lutron component.""" - from pylutron import Lutron hass.data[LUTRON_BUTTONS] = [] hass.data[LUTRON_CONTROLLER] = None @@ -147,7 +147,6 @@ class LutronButton: def button_callback(self, button, context, event, params): """Fire an event about a button being pressed or released.""" - from pylutron import Button # Events per button type: # RaiseLower -> pressed/released diff --git a/homeassistant/components/lutron/binary_sensor.py b/homeassistant/components/lutron/binary_sensor.py index a86d56c325f..866c82a7b2a 100644 --- a/homeassistant/components/lutron/binary_sensor.py +++ b/homeassistant/components/lutron/binary_sensor.py @@ -2,8 +2,8 @@ from pylutron import OccupancyGroup from homeassistant.components.binary_sensor import ( - BinarySensorDevice, DEVICE_CLASS_OCCUPANCY, + BinarySensorDevice, ) from . import LUTRON_CONTROLLER, LUTRON_DEVICES, LutronDevice diff --git a/homeassistant/components/lutron_caseta/.translations/bg.json b/homeassistant/components/lutron_caseta/.translations/bg.json new file mode 100644 index 00000000000..cfc3c290afe --- /dev/null +++ b/homeassistant/components/lutron_caseta/.translations/bg.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Lutron Cas\u00e9ta" + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/.translations/ca.json b/homeassistant/components/lutron_caseta/.translations/ca.json new file mode 100644 index 00000000000..cfc3c290afe --- /dev/null +++ b/homeassistant/components/lutron_caseta/.translations/ca.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Lutron Cas\u00e9ta" + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/.translations/da.json b/homeassistant/components/lutron_caseta/.translations/da.json new file mode 100644 index 00000000000..cfc3c290afe --- /dev/null +++ b/homeassistant/components/lutron_caseta/.translations/da.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Lutron Cas\u00e9ta" + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/.translations/en.json b/homeassistant/components/lutron_caseta/.translations/en.json new file mode 100644 index 00000000000..cfc3c290afe --- /dev/null +++ b/homeassistant/components/lutron_caseta/.translations/en.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Lutron Cas\u00e9ta" + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/.translations/es.json b/homeassistant/components/lutron_caseta/.translations/es.json new file mode 100644 index 00000000000..cfc3c290afe --- /dev/null +++ b/homeassistant/components/lutron_caseta/.translations/es.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Lutron Cas\u00e9ta" + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/.translations/fr.json b/homeassistant/components/lutron_caseta/.translations/fr.json new file mode 100644 index 00000000000..cfc3c290afe --- /dev/null +++ b/homeassistant/components/lutron_caseta/.translations/fr.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Lutron Cas\u00e9ta" + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/.translations/it.json b/homeassistant/components/lutron_caseta/.translations/it.json new file mode 100644 index 00000000000..cfc3c290afe --- /dev/null +++ b/homeassistant/components/lutron_caseta/.translations/it.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Lutron Cas\u00e9ta" + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/.translations/ko.json b/homeassistant/components/lutron_caseta/.translations/ko.json new file mode 100644 index 00000000000..cfc3c290afe --- /dev/null +++ b/homeassistant/components/lutron_caseta/.translations/ko.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Lutron Cas\u00e9ta" + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/.translations/lb.json b/homeassistant/components/lutron_caseta/.translations/lb.json new file mode 100644 index 00000000000..cfc3c290afe --- /dev/null +++ b/homeassistant/components/lutron_caseta/.translations/lb.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Lutron Cas\u00e9ta" + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/.translations/nl.json b/homeassistant/components/lutron_caseta/.translations/nl.json new file mode 100644 index 00000000000..cfc3c290afe --- /dev/null +++ b/homeassistant/components/lutron_caseta/.translations/nl.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Lutron Cas\u00e9ta" + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/.translations/no.json b/homeassistant/components/lutron_caseta/.translations/no.json new file mode 100644 index 00000000000..cfc3c290afe --- /dev/null +++ b/homeassistant/components/lutron_caseta/.translations/no.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Lutron Cas\u00e9ta" + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/.translations/pl.json b/homeassistant/components/lutron_caseta/.translations/pl.json new file mode 100644 index 00000000000..cfc3c290afe --- /dev/null +++ b/homeassistant/components/lutron_caseta/.translations/pl.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Lutron Cas\u00e9ta" + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/.translations/ru.json b/homeassistant/components/lutron_caseta/.translations/ru.json new file mode 100644 index 00000000000..cfc3c290afe --- /dev/null +++ b/homeassistant/components/lutron_caseta/.translations/ru.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Lutron Cas\u00e9ta" + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/.translations/sl.json b/homeassistant/components/lutron_caseta/.translations/sl.json new file mode 100644 index 00000000000..cfc3c290afe --- /dev/null +++ b/homeassistant/components/lutron_caseta/.translations/sl.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Lutron Cas\u00e9ta" + } +} \ 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 new file mode 100644 index 00000000000..cfc3c290afe --- /dev/null +++ b/homeassistant/components/lutron_caseta/.translations/zh-Hant.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Lutron Cas\u00e9ta" + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index 4d4b2e90fd6..aaac06a6bd5 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -1,11 +1,12 @@ """Component for interacting with a Lutron Caseta system.""" import logging +from pylutron_caseta.smartbridge import Smartbridge import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_HOST from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -32,12 +33,11 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -LUTRON_CASETA_COMPONENTS = ["light", "switch", "cover", "scene"] +LUTRON_CASETA_COMPONENTS = ["light", "switch", "cover", "scene", "fan"] async def async_setup(hass, base_config): """Set up the Lutron component.""" - from pylutron_caseta.smartbridge import Smartbridge config = base_config.get(DOMAIN) keyfile = hass.config.path(config[CONF_KEYFILE]) @@ -76,28 +76,39 @@ class LutronCasetaDevice(Entity): [:param]device the device metadata [:param]bridge the smartbridge object """ - self._device_id = device["device_id"] - self._device_type = device["type"] - self._device_name = device["name"] - self._device_zone = device["zone"] - self._state = None + self._device = device self._smartbridge = bridge async def async_added_to_hass(self): """Register callbacks.""" self._smartbridge.add_subscriber( - self._device_id, self.async_schedule_update_ha_state + self.device_id, self.async_schedule_update_ha_state ) + @property + def device_id(self): + """Return the device ID used for calling pylutron_caseta.""" + return self._device["device_id"] + @property def name(self): """Return the name of the device.""" - return self._device_name + return self._device["name"] + + @property + def serial(self): + """Return the serial number of the device.""" + return self._device["serial"] + + @property + def unique_id(self): + """Return the unique ID of the device (serial).""" + return str(self.serial) @property def device_state_attributes(self): """Return the state attributes.""" - attr = {"Device ID": self._device_id, "Zone ID": self._device_zone} + attr = {"Device ID": self.device_id, "Zone ID": self._device["zone"]} return attr @property diff --git a/homeassistant/components/lutron_caseta/cover.py b/homeassistant/components/lutron_caseta/cover.py index 786e569da32..afd669153e0 100644 --- a/homeassistant/components/lutron_caseta/cover.py +++ b/homeassistant/components/lutron_caseta/cover.py @@ -38,28 +38,28 @@ class LutronCasetaCover(LutronCasetaDevice, CoverDevice): @property def is_closed(self): """Return if the cover is closed.""" - return self._state["current_state"] < 1 + return self._device["current_state"] < 1 @property def current_cover_position(self): """Return the current position of cover.""" - return self._state["current_state"] + return self._device["current_state"] async def async_close_cover(self, **kwargs): """Close the cover.""" - self._smartbridge.set_value(self._device_id, 0) + self._smartbridge.set_value(self.device_id, 0) async def async_open_cover(self, **kwargs): """Open the cover.""" - self._smartbridge.set_value(self._device_id, 100) + self._smartbridge.set_value(self.device_id, 100) async def async_set_cover_position(self, **kwargs): """Move the shade to a specific position.""" if ATTR_POSITION in kwargs: position = kwargs[ATTR_POSITION] - self._smartbridge.set_value(self._device_id, position) + self._smartbridge.set_value(self.device_id, position) async def async_update(self): """Call when forcing a refresh of the device.""" - self._state = self._smartbridge.get_device_by_id(self._device_id) - _LOGGER.debug(self._state) + self._device = self._smartbridge.get_device_by_id(self.device_id) + _LOGGER.debug(self._device) diff --git a/homeassistant/components/lutron_caseta/fan.py b/homeassistant/components/lutron_caseta/fan.py new file mode 100644 index 00000000000..1227371ac07 --- /dev/null +++ b/homeassistant/components/lutron_caseta/fan.py @@ -0,0 +1,96 @@ +"""Support for Lutron Caseta fans.""" +import logging + +from pylutron_caseta import FAN_HIGH, FAN_LOW, FAN_MEDIUM, FAN_MEDIUM_HIGH, FAN_OFF + +from homeassistant.components.fan import ( + DOMAIN, + SPEED_HIGH, + SPEED_LOW, + SPEED_MEDIUM, + SPEED_OFF, + SUPPORT_SET_SPEED, + FanEntity, +) + +from . import LUTRON_CASETA_SMARTBRIDGE, LutronCasetaDevice + +_LOGGER = logging.getLogger(__name__) + +VALUE_TO_SPEED = { + None: SPEED_OFF, + FAN_OFF: SPEED_OFF, + FAN_LOW: SPEED_LOW, + FAN_MEDIUM: SPEED_MEDIUM, + FAN_MEDIUM_HIGH: SPEED_MEDIUM, + FAN_HIGH: SPEED_HIGH, +} + +SPEED_TO_VALUE = { + SPEED_OFF: FAN_OFF, + SPEED_LOW: FAN_LOW, + SPEED_MEDIUM: FAN_MEDIUM, + SPEED_HIGH: FAN_HIGH, +} + +FAN_SPEEDS = [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up Lutron fan.""" + entities = [] + bridge = hass.data[LUTRON_CASETA_SMARTBRIDGE] + fan_devices = bridge.get_devices_by_domain(DOMAIN) + + for fan_device in fan_devices: + entity = LutronCasetaFan(fan_device, bridge) + entities.append(entity) + + async_add_entities(entities, True) + + +class LutronCasetaFan(LutronCasetaDevice, FanEntity): + """Representation of a Lutron Caseta fan. Including Fan Speed.""" + + @property + def speed(self) -> str: + """Return the current speed.""" + return VALUE_TO_SPEED[self._device["fan_speed"]] + + @property + def speed_list(self) -> list: + """Get the list of available speeds.""" + return FAN_SPEEDS + + @property + def supported_features(self) -> int: + """Flag supported features. Speed Only.""" + return SUPPORT_SET_SPEED + + async def async_turn_on(self, speed: str = None, **kwargs): + """Turn the fan on.""" + if speed is None: + speed = SPEED_MEDIUM + await self.async_set_speed(speed) + + async def async_turn_off(self, **kwargs): + """Turn the fan off.""" + await self.async_set_speed(SPEED_OFF) + + async def async_set_speed(self, speed: str) -> None: + """Set the speed of the fan.""" + self._smartbridge.set_fan(self.device_id, SPEED_TO_VALUE[speed]) + + @property + def is_on(self): + """Return true if device is on.""" + return VALUE_TO_SPEED[self._device["fan_speed"]] in [ + SPEED_LOW, + SPEED_MEDIUM, + SPEED_HIGH, + ] + + async def async_update(self): + """Update when forcing a refresh of the device.""" + self._device = self._smartbridge.get_device_by_id(self.device_id) + _LOGGER.debug("State of this lutron fan device is %s", self._device) diff --git a/homeassistant/components/lutron_caseta/light.py b/homeassistant/components/lutron_caseta/light.py index a764ad4b73a..af225d2939d 100644 --- a/homeassistant/components/lutron_caseta/light.py +++ b/homeassistant/components/lutron_caseta/light.py @@ -37,23 +37,23 @@ class LutronCasetaLight(LutronCasetaDevice, Light): @property def brightness(self): """Return the brightness of the light.""" - return to_hass_level(self._state["current_state"]) + return to_hass_level(self._device["current_state"]) async def async_turn_on(self, **kwargs): """Turn the light on.""" brightness = kwargs.get(ATTR_BRIGHTNESS, 255) - self._smartbridge.set_value(self._device_id, to_lutron_level(brightness)) + self._smartbridge.set_value(self.device_id, to_lutron_level(brightness)) async def async_turn_off(self, **kwargs): """Turn the light off.""" - self._smartbridge.set_value(self._device_id, 0) + self._smartbridge.set_value(self.device_id, 0) @property def is_on(self): """Return true if device is on.""" - return self._state["current_state"] > 0 + return self._device["current_state"] > 0 async def async_update(self): """Call when forcing a refresh of the device.""" - self._state = self._smartbridge.get_device_by_id(self._device_id) - _LOGGER.debug(self._state) + self._device = self._smartbridge.get_device_by_id(self.device_id) + _LOGGER.debug(self._device) diff --git a/homeassistant/components/lutron_caseta/manifest.json b/homeassistant/components/lutron_caseta/manifest.json index d1501a562db..e9df5ad1d46 100644 --- a/homeassistant/components/lutron_caseta/manifest.json +++ b/homeassistant/components/lutron_caseta/manifest.json @@ -3,7 +3,7 @@ "name": "Lutron caseta", "documentation": "https://www.home-assistant.io/integrations/lutron_caseta", "requirements": [ - "pylutron-caseta==0.5.0" + "pylutron-caseta==0.5.1" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/lutron_caseta/strings.json b/homeassistant/components/lutron_caseta/strings.json new file mode 100644 index 00000000000..cb7ab8c767e --- /dev/null +++ b/homeassistant/components/lutron_caseta/strings.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Lutron Caséta" + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/switch.py b/homeassistant/components/lutron_caseta/switch.py index fabd4e7fa76..f6eb846ecfb 100644 --- a/homeassistant/components/lutron_caseta/switch.py +++ b/homeassistant/components/lutron_caseta/switch.py @@ -27,18 +27,18 @@ class LutronCasetaLight(LutronCasetaDevice, SwitchDevice): async def async_turn_on(self, **kwargs): """Turn the switch on.""" - self._smartbridge.turn_on(self._device_id) + self._smartbridge.turn_on(self.device_id) async def async_turn_off(self, **kwargs): """Turn the switch off.""" - self._smartbridge.turn_off(self._device_id) + self._smartbridge.turn_off(self.device_id) @property def is_on(self): """Return true if device is on.""" - return self._state["current_state"] > 0 + return self._device["current_state"] > 0 async def async_update(self): """Update when forcing a refresh of the device.""" - self._state = self._smartbridge.get_device_by_id(self._device_id) - _LOGGER.debug(self._state) + self._device = self._smartbridge.get_device_by_id(self.device_id) + _LOGGER.debug(self._device) diff --git a/homeassistant/components/lyft/sensor.py b/homeassistant/components/lyft/sensor.py index 339b996c5d8..1b90d66398e 100644 --- a/homeassistant/components/lyft/sensor.py +++ b/homeassistant/components/lyft/sensor.py @@ -1,13 +1,16 @@ """Support for the Lyft API.""" -import logging from datetime import timedelta +import logging +from lyft_rides.auth import ClientCredentialGrant +from lyft_rides.client import LyftRidesClient +from lyft_rides.errors import APIError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -38,8 +41,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Lyft sensor.""" - from lyft_rides.auth import ClientCredentialGrant - from lyft_rides.errors import APIError auth_flow = ClientCredentialGrant( client_id=config.get(CONF_CLIENT_ID), @@ -208,7 +209,6 @@ class LyftEstimate: @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest product info and estimates from the Lyft API.""" - from lyft_rides.errors import APIError try: self.fetch_data() @@ -217,7 +217,6 @@ class LyftEstimate: def fetch_data(self): """Get the latest product info and estimates from the Lyft API.""" - from lyft_rides.client import LyftRidesClient client = LyftRidesClient(self._session) diff --git a/homeassistant/components/mailbox/__init__.py b/homeassistant/components/mailbox/__init__.py index 1252036e1b2..0381d932328 100644 --- a/homeassistant/components/mailbox/__init__.py +++ b/homeassistant/components/mailbox/__init__.py @@ -16,7 +16,6 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.setup import async_prepare_setup_platform - # mypy: allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/mailgun/.translations/da.json b/homeassistant/components/mailgun/.translations/da.json index 0e25974031d..f9152633706 100644 --- a/homeassistant/components/mailgun/.translations/da.json +++ b/homeassistant/components/mailgun/.translations/da.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "not_internet_accessible": "Dit Home Assistant system skal v\u00e6re tilg\u00e6ngeligt fra internettet for at modtage Mailgun meddelelser.", + "not_internet_accessible": "Din Home Assistant-instans skal v\u00e6re tilg\u00e6ngelig fra internettet for at modtage Mailgun-meddelelser", "one_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning." }, "create_entry": { - "default": "For at sende begivenheder til Home Assistant skal du konfigurere [Webhooks med Mailgun]({mailgun_url}).\n\n Udfyld f\u00f8lgende oplysninger: \n\n - URL: `{webhook_url}`\n - Metode: POST\n - Indholdstype: application/json\n\n Se [dokumentationen] ({docs_url}) om hvordan du konfigurerer automatiseringer til at h\u00e5ndtere indg\u00e5ende data." + "default": "For at sende h\u00e6ndelser til Home Assistant skal du konfigurere [Webhooks med Mailgun]({mailgun_url}).\n\n Udfyld f\u00f8lgende oplysninger: \n\n - Webadresse: `{webhook_url}`\n - Metode: POST\n - Indholdstype: application/json\n\nSe [dokumentationen] ({docs_url}) om hvordan du konfigurerer automatiseringer til at h\u00e5ndtere indg\u00e5ende data." }, "step": { "user": { diff --git a/homeassistant/components/mailgun/.translations/ko.json b/homeassistant/components/mailgun/.translations/ko.json index 4ca5b155e73..8f1f021caf6 100644 --- a/homeassistant/components/mailgun/.translations/ko.json +++ b/homeassistant/components/mailgun/.translations/ko.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "Mailgun \uc744 \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "Mailgun \uc744 \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "Mailgun Webhook \uc124\uc815" } }, diff --git a/homeassistant/components/mailgun/__init__.py b/homeassistant/components/mailgun/__init__.py index 4bcca0848f4..57c83d8c20c 100644 --- a/homeassistant/components/mailgun/__init__.py +++ b/homeassistant/components/mailgun/__init__.py @@ -6,13 +6,12 @@ import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_API_KEY, CONF_DOMAIN, CONF_WEBHOOK_ID from homeassistant.helpers import config_entry_flow +import homeassistant.helpers.config_validation as cv from .const import DOMAIN - _LOGGER = logging.getLogger(__name__) CONF_SANDBOX = "sandbox" diff --git a/homeassistant/components/mailgun/config_flow.py b/homeassistant/components/mailgun/config_flow.py index c575b4c0354..6fe87e7cbf4 100644 --- a/homeassistant/components/mailgun/config_flow.py +++ b/homeassistant/components/mailgun/config_flow.py @@ -1,7 +1,7 @@ """Config flow for Mailgun.""" from homeassistant.helpers import config_entry_flow -from .const import DOMAIN +from .const import DOMAIN config_entry_flow.register_webhook_flow( DOMAIN, diff --git a/homeassistant/components/mailgun/notify.py b/homeassistant/components/mailgun/notify.py index efa5a17430c..c2222cfd742 100644 --- a/homeassistant/components/mailgun/notify.py +++ b/homeassistant/components/mailgun/notify.py @@ -1,6 +1,12 @@ """Support for the Mailgun mail notifications.""" import logging +from pymailgunner import ( + Client, + MailgunCredentialsError, + MailgunDomainError, + MailgunError, +) import voluptuous as vol from homeassistant.components.notify import ( @@ -58,7 +64,6 @@ class MailgunNotificationService(BaseNotificationService): def initialize_client(self): """Initialize the connection to Mailgun.""" - from pymailgunner import Client self._client = Client(self._api_key, self._domain, self._sandbox) _LOGGER.debug("Mailgun domain: %s", self._client.domain) @@ -68,7 +73,6 @@ class MailgunNotificationService(BaseNotificationService): def connection_is_valid(self): """Check whether the provided credentials are valid.""" - from pymailgunner import MailgunCredentialsError, MailgunDomainError try: self.initialize_client() @@ -82,7 +86,6 @@ class MailgunNotificationService(BaseNotificationService): def send_message(self, message="", **kwargs): """Send a mail to the recipient.""" - from pymailgunner import MailgunError subject = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) data = kwargs.get(ATTR_DATA) diff --git a/homeassistant/components/manual/alarm_control_panel.py b/homeassistant/components/manual/alarm_control_panel.py index ac234dc0ac9..b41da2d51bd 100644 --- a/homeassistant/components/manual/alarm_control_panel.py +++ b/homeassistant/components/manual/alarm_control_panel.py @@ -7,6 +7,13 @@ import re import voluptuous as vol import homeassistant.components.alarm_control_panel as alarm +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_CUSTOM_BYPASS, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, + SUPPORT_ALARM_TRIGGER, +) from homeassistant.const import ( CONF_CODE, CONF_DELAY_TIME, @@ -25,8 +32,8 @@ from homeassistant.const import ( ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import track_point_in_time -import homeassistant.util.dt as dt_util from homeassistant.helpers.restore_state import RestoreEntity +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -234,6 +241,17 @@ class ManualAlarm(alarm.AlarmControlPanel, RestoreEntity): return self._state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return ( + SUPPORT_ALARM_ARM_HOME + | SUPPORT_ALARM_ARM_AWAY + | SUPPORT_ALARM_ARM_NIGHT + | SUPPORT_ALARM_TRIGGER + | SUPPORT_ALARM_ARM_CUSTOM_BYPASS + ) + @property def _active_state(self): """Get the current state.""" diff --git a/homeassistant/components/manual_mqtt/alarm_control_panel.py b/homeassistant/components/manual_mqtt/alarm_control_panel.py index c57fa275516..f11dac357e6 100644 --- a/homeassistant/components/manual_mqtt/alarm_control_panel.py +++ b/homeassistant/components/manual_mqtt/alarm_control_panel.py @@ -6,30 +6,33 @@ import re import voluptuous as vol +from homeassistant.components import mqtt import homeassistant.components.alarm_control_panel as alarm -import homeassistant.util.dt as dt_util +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, + SUPPORT_ALARM_TRIGGER, +) from homeassistant.const import ( + CONF_CODE, + CONF_DELAY_TIME, + CONF_DISARM_AFTER_TRIGGER, + CONF_NAME, + CONF_PENDING_TIME, + CONF_PLATFORM, + CONF_TRIGGER_TIME, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, - CONF_PLATFORM, - CONF_NAME, - CONF_CODE, - CONF_DELAY_TIME, - CONF_PENDING_TIME, - CONF_TRIGGER_TIME, - CONF_DISARM_AFTER_TRIGGER, ) -from homeassistant.components import mqtt - -from homeassistant.helpers.event import async_track_state_change from homeassistant.core import callback - import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import track_point_in_time +from homeassistant.helpers.event import async_track_state_change, track_point_in_time +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -278,6 +281,16 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel): return self._state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return ( + SUPPORT_ALARM_ARM_HOME + | SUPPORT_ALARM_ARM_AWAY + | SUPPORT_ALARM_ARM_NIGHT + | SUPPORT_ALARM_TRIGGER + ) + @property def _active_state(self): """Get the current state.""" diff --git a/homeassistant/components/matrix/__init__.py b/homeassistant/components/matrix/__init__.py index 4a9435808bb..f8a57572d04 100644 --- a/homeassistant/components/matrix/__init__.py +++ b/homeassistant/components/matrix/__init__.py @@ -1,22 +1,25 @@ -"""The matrix bot component.""" +"""The Matrix bot component.""" +from functools import partial import logging import os -from functools import partial +from matrix_client.client import MatrixClient, MatrixRequestError import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.components.notify import ATTR_TARGET, ATTR_MESSAGE +from homeassistant.components.notify import ATTR_MESSAGE, ATTR_TARGET from homeassistant.const import ( - CONF_USERNAME, - CONF_PASSWORD, - CONF_VERIFY_SSL, CONF_NAME, - EVENT_HOMEASSISTANT_STOP, + CONF_PASSWORD, + CONF_USERNAME, + CONF_VERIFY_SSL, EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.util.json import load_json, save_json from homeassistant.exceptions import HomeAssistantError +import homeassistant.helpers.config_validation as cv +from homeassistant.util.json import load_json, save_json + +from .const import DOMAIN, SERVICE_SEND_MESSAGE _LOGGER = logging.getLogger(__name__) @@ -30,10 +33,7 @@ CONF_EXPRESSION = "expression" EVENT_MATRIX_COMMAND = "matrix_command" -DOMAIN = "matrix" - COMMAND_SCHEMA = vol.All( - # Basic Schema vol.Schema( { vol.Exclusive(CONF_WORD, "trigger"): cv.string, @@ -42,7 +42,6 @@ COMMAND_SCHEMA = vol.All( vol.Optional(CONF_ROOMS, default=[]): vol.All(cv.ensure_list, [cv.string]), } ), - # Make sure it's either a word or an expression command cv.has_at_least_one_key(CONF_WORD, CONF_EXPRESSION), ) @@ -64,7 +63,6 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -SERVICE_SEND_MESSAGE = "send_message" SERVICE_SCHEMA_SEND_MESSAGE = vol.Schema( { @@ -76,8 +74,6 @@ SERVICE_SCHEMA_SEND_MESSAGE = vol.Schema( def setup(hass, config): """Set up the Matrix bot component.""" - from matrix_client.client import MatrixRequestError - config = config[DOMAIN] try: @@ -138,11 +134,11 @@ class MatrixBot: # so we only do it once per room. self._aliases_fetched_for = set() - # word commands are stored dict-of-dict: First dict indexes by room ID + # Word commands are stored dict-of-dict: First dict indexes by room ID # / alias, second dict indexes by the word self._word_commands = {} - # regular expression commands are stored as a list of commands per + # Regular expression commands are stored as a list of commands per # room, i.e., a dict-of-list self._expression_commands = {} @@ -184,7 +180,7 @@ class MatrixBot: self.hass.bus.listen_once(EVENT_HOMEASSISTANT_START, handle_startup) def _handle_room_message(self, room_id, room, event): - """Handle a message sent to a room.""" + """Handle a message sent to a Matrix room.""" if event["content"]["msgtype"] != "m.text": return @@ -194,7 +190,7 @@ class MatrixBot: _LOGGER.debug("Handling message: %s", event["content"]["body"]) if event["content"]["body"][0] == "!": - # Could trigger a single-word command. + # Could trigger a single-word command pieces = event["content"]["body"].split(" ") cmd = pieces[0][1:] @@ -248,9 +244,7 @@ class MatrixBot: return room def _join_rooms(self): - """Join the rooms that we listen for commands in.""" - from matrix_client.client import MatrixRequestError - + """Join the Matrix rooms that we listen for commands in.""" for room_id in self._listening_rooms: try: room = self._join_or_get_room(room_id) @@ -286,9 +280,7 @@ class MatrixBot: save_json(self._session_filepath, self._auth_tokens) def _login(self): - """Login to the matrix homeserver and return the client instance.""" - from matrix_client.client import MatrixRequestError - + """Login to the Matrix homeserver and return the client instance.""" # Attempt to generate a valid client using either of the two possible # login methods: client = None @@ -301,13 +293,12 @@ class MatrixBot: except MatrixRequestError as ex: _LOGGER.warning( - "Login by token failed, falling back to password. " - "login_by_token raised: (%d) %s", + "Login by token failed, falling back to password: %d, %s", ex.code, ex.content, ) - # If we still don't have a client try password. + # If we still don't have a client try password if not client: try: client = self._login_by_password() @@ -315,21 +306,17 @@ class MatrixBot: except MatrixRequestError as ex: _LOGGER.error( - "Login failed, both token and username/password invalid " - "login_by_password raised: (%d) %s", + "Login failed, both token and username/password invalid: %d, %s", ex.code, ex.content, ) - - # re-raise the error so _setup can catch it. + # Re-raise the error so _setup can catch it raise return client def _login_by_token(self): """Login using authentication token and return the client.""" - from matrix_client.client import MatrixClient - return MatrixClient( base_url=self._homeserver, token=self._auth_tokens[self._mx_id], @@ -339,8 +326,6 @@ class MatrixBot: def _login_by_password(self): """Login using password authentication and return the client.""" - from matrix_client.client import MatrixClient - _client = MatrixClient( base_url=self._homeserver, valid_cert_check=self._verify_tls ) @@ -352,8 +337,7 @@ class MatrixBot: return _client def _send_message(self, message, target_rooms): - """Send the message to the matrix server.""" - from matrix_client.client import MatrixRequestError + """Send the message to the Matrix server.""" for target_room in target_rooms: try: @@ -361,7 +345,7 @@ class MatrixBot: _LOGGER.debug(room.send_text(message)) except MatrixRequestError as ex: _LOGGER.error( - "Unable to deliver message to room '%s': (%d): %s", + "Unable to deliver message to room '%s': %d, %s", target_room, ex.code, ex.content, diff --git a/homeassistant/components/matrix/const.py b/homeassistant/components/matrix/const.py new file mode 100644 index 00000000000..6b082bde121 --- /dev/null +++ b/homeassistant/components/matrix/const.py @@ -0,0 +1,4 @@ +"""Constants for the Matrix integration.""" +DOMAIN = "matrix" + +SERVICE_SEND_MESSAGE = "send_message" diff --git a/homeassistant/components/matrix/manifest.json b/homeassistant/components/matrix/manifest.json index a467518c04e..e7d1fab6874 100644 --- a/homeassistant/components/matrix/manifest.json +++ b/homeassistant/components/matrix/manifest.json @@ -3,7 +3,7 @@ "name": "Matrix", "documentation": "https://www.home-assistant.io/integrations/matrix", "requirements": [ - "matrix-client==0.2.0" + "matrix-client==0.3.2" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/matrix/notify.py b/homeassistant/components/matrix/notify.py index 44a0587ba6d..9f1f0eb992a 100644 --- a/homeassistant/components/matrix/notify.py +++ b/homeassistant/components/matrix/notify.py @@ -3,40 +3,41 @@ import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.notify import ( + ATTR_MESSAGE, ATTR_TARGET, PLATFORM_SCHEMA, BaseNotificationService, - ATTR_MESSAGE, ) +import homeassistant.helpers.config_validation as cv + +from .const import DOMAIN, SERVICE_SEND_MESSAGE _LOGGER = logging.getLogger(__name__) CONF_DEFAULT_ROOM = "default_room" -DOMAIN = "matrix" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_DEFAULT_ROOM): cv.string}) def get_service(hass, config, discovery_info=None): """Get the Matrix notification service.""" - return MatrixNotificationService(config.get(CONF_DEFAULT_ROOM)) + return MatrixNotificationService(config[CONF_DEFAULT_ROOM]) class MatrixNotificationService(BaseNotificationService): - """Send Notifications to a Matrix Room.""" + """Send notifications to a Matrix room.""" def __init__(self, default_room): - """Set up the notification service.""" + """Set up the Matrix notification service.""" self._default_room = default_room def send_message(self, message="", **kwargs): - """Send the message to the matrix server.""" + """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} return self.hass.services.call( - DOMAIN, "send_message", service_data=service_data + DOMAIN, SERVICE_SEND_MESSAGE, service_data=service_data ) diff --git a/homeassistant/components/matrix/services.yaml b/homeassistant/components/matrix/services.yaml index e69de29bb2d..03c441a39ec 100644 --- a/homeassistant/components/matrix/services.yaml +++ b/homeassistant/components/matrix/services.yaml @@ -0,0 +1,9 @@ +send_message: + description: Send message to target room(s) + fields: + message: + description: The message to be sent. + example: 'This is a message I am sending to matrix' + target: + description: A list of room(s) to send the message to. + example: '#hasstest:matrix.org' \ No newline at end of file diff --git a/homeassistant/components/maxcube/__init__.py b/homeassistant/components/maxcube/__init__.py index 65a969bbcb8..1b65cb161e1 100644 --- a/homeassistant/components/maxcube/__init__.py +++ b/homeassistant/components/maxcube/__init__.py @@ -1,14 +1,16 @@ """Support for the MAX! Cube LAN Gateway.""" import logging -import time from socket import timeout from threading import Lock +import time +from maxcube.connection import MaxCubeConnection +from maxcube.cube import MaxCube import voluptuous as vol +from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SCAN_INTERVAL import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform -from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SCAN_INTERVAL _LOGGER = logging.getLogger(__name__) @@ -46,8 +48,6 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): """Establish connection to MAX! Cube.""" - from maxcube.connection import MaxCubeConnection - from maxcube.cube import MaxCube if DATA_KEY not in hass.data: hass.data[DATA_KEY] = {} diff --git a/homeassistant/components/maxcube/climate.py b/homeassistant/components/maxcube/climate.py index e09dfc2d99f..ff4b219ec21 100644 --- a/homeassistant/components/maxcube/climate.py +++ b/homeassistant/components/maxcube/climate.py @@ -4,16 +4,16 @@ import socket from maxcube.device import ( MAX_DEVICE_MODE_AUTOMATIC, + MAX_DEVICE_MODE_BOOST, MAX_DEVICE_MODE_MANUAL, MAX_DEVICE_MODE_VACATION, - MAX_DEVICE_MODE_BOOST, ) from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( HVAC_MODE_AUTO, - SUPPORT_TARGET_TEMPERATURE, SUPPORT_PRESET_MODE, + SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS diff --git a/homeassistant/components/mcp23017/binary_sensor.py b/homeassistant/components/mcp23017/binary_sensor.py index 088052c469e..e95b91389cd 100644 --- a/homeassistant/components/mcp23017/binary_sensor.py +++ b/homeassistant/components/mcp23017/binary_sensor.py @@ -1,13 +1,13 @@ """Support for binary sensor using I2C MCP23017 chip.""" import logging -import voluptuous as vol +import adafruit_mcp230xx # pylint: disable=import-error import board # pylint: disable=import-error import busio # pylint: disable=import-error -import adafruit_mcp230xx # pylint: disable=import-error import digitalio # pylint: disable=import-error +import voluptuous as vol -from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice from homeassistant.const import DEVICE_DEFAULT_NAME import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/mcp23017/switch.py b/homeassistant/components/mcp23017/switch.py index 399ed17c44b..8506106b705 100644 --- a/homeassistant/components/mcp23017/switch.py +++ b/homeassistant/components/mcp23017/switch.py @@ -1,16 +1,16 @@ """Support for switch sensor using I2C MCP23017 chip.""" import logging -import voluptuous as vol +import adafruit_mcp230xx # pylint: disable=import-error import board # pylint: disable=import-error import busio # pylint: disable=import-error -import adafruit_mcp230xx # pylint: disable=import-error import digitalio # pylint: disable=import-error +import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA from homeassistant.const import DEVICE_DEFAULT_NAME -from homeassistant.helpers.entity import ToggleEntity import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import ToggleEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/media_extractor/__init__.py b/homeassistant/components/media_extractor/__init__.py index 97cb7d9978a..7dc05368dcd 100644 --- a/homeassistant/components/media_extractor/__init__.py +++ b/homeassistant/components/media_extractor/__init__.py @@ -2,6 +2,8 @@ import logging import voluptuous as vol +from youtube_dl import YoutubeDL +from youtube_dl.utils import DownloadError, ExtractorError from homeassistant.components.media_player import MEDIA_PLAYER_PLAY_MEDIA_SCHEMA from homeassistant.components.media_player.const import ( @@ -44,7 +46,10 @@ def setup(hass, config): MediaExtractor(hass, config[DOMAIN], call.data).extract_and_send() hass.services.register( - DOMAIN, SERVICE_PLAY_MEDIA, play_media, schema=MEDIA_PLAYER_PLAY_MEDIA_SCHEMA + DOMAIN, + SERVICE_PLAY_MEDIA, + play_media, + schema=cv.make_entity_service_schema(MEDIA_PLAYER_PLAY_MEDIA_SCHEMA), ) return True @@ -98,9 +103,6 @@ class MediaExtractor: def get_stream_selector(self): """Return format selector for the media URL.""" - from youtube_dl import YoutubeDL - from youtube_dl.utils import DownloadError, ExtractorError - ydl = YoutubeDL({"quiet": True, "logger": _LOGGER}) try: diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index f413ffd16db..0c816686557 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -3,7 +3,7 @@ "name": "Media extractor", "documentation": "https://www.home-assistant.io/integrations/media_extractor", "requirements": [ - "youtube_dl==2019.11.05" + "youtube_dl==2020.01.01" ], "dependencies": [ "media_player" diff --git a/homeassistant/components/media_player/.translations/da.json b/homeassistant/components/media_player/.translations/da.json new file mode 100644 index 00000000000..a53bbed07d0 --- /dev/null +++ b/homeassistant/components/media_player/.translations/da.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "condition_type": { + "is_idle": "{entity_name} er inaktiv", + "is_off": "{entity_name} er slukket", + "is_on": "{entity_name} er t\u00e6ndt", + "is_paused": "{entity_name} er sat p\u00e5 pause", + "is_playing": "{entity_name} afspiller" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/media_player/.translations/ko.json b/homeassistant/components/media_player/.translations/ko.json index 7542154448f..49367eaf617 100644 --- a/homeassistant/components/media_player/.translations/ko.json +++ b/homeassistant/components/media_player/.translations/ko.json @@ -1,11 +1,11 @@ { "device_automation": { "condition_type": { - "is_idle": "{entity_name} \uc774(\uac00) \uc720\ud734\uc0c1\ud0dc\uc785\ub2c8\ub2e4", - "is_off": "{entity_name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4", - "is_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4", - "is_paused": "{entity_name} \uc774(\uac00) \uc77c\uc2dc\uc911\uc9c0\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "is_playing": "{entity_name} \uc774(\uac00) \uc7ac\uc0dd\uc911\uc785\ub2c8\ub2e4" + "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" } } } \ No newline at end of file diff --git a/homeassistant/components/media_player/.translations/no.json b/homeassistant/components/media_player/.translations/no.json index a05d907774f..388143b53ec 100644 --- a/homeassistant/components/media_player/.translations/no.json +++ b/homeassistant/components/media_player/.translations/no.json @@ -5,7 +5,7 @@ "is_off": "{entity_name} er sl\u00e5tt av", "is_on": "{entity_name} er sl\u00e5tt p\u00e5", "is_paused": "{entity_name} er satt p\u00e5 pause", - "is_playing": "{entity_name} spiller" + "is_playing": "{entity_name} spiller n\u00e5" } } } \ No newline at end of file diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index c783772365b..1375a0ed429 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -40,7 +40,6 @@ from homeassistant.const import ( from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa: F401 - ENTITY_SERVICE_SCHEMA, PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) @@ -98,7 +97,6 @@ from .const import ( SUPPORT_VOLUME_STEP, ) - # mypy: allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) @@ -123,42 +121,12 @@ DEVICE_CLASSES = [DEVICE_CLASS_TV, DEVICE_CLASS_SPEAKER] DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES)) -# Service call validation schemas -MEDIA_PLAYER_SET_VOLUME_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_MEDIA_VOLUME_LEVEL): cv.small_float} -) -MEDIA_PLAYER_MUTE_VOLUME_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_MEDIA_VOLUME_MUTED): cv.boolean} -) - -MEDIA_PLAYER_MEDIA_SEEK_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - { - vol.Required(ATTR_MEDIA_SEEK_POSITION): vol.All( - vol.Coerce(float), vol.Range(min=0) - ) - } -) - -MEDIA_PLAYER_SELECT_SOURCE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_INPUT_SOURCE): cv.string} -) - -MEDIA_PLAYER_SELECT_SOUND_MODE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_SOUND_MODE): cv.string} -) - -MEDIA_PLAYER_PLAY_MEDIA_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - { - vol.Required(ATTR_MEDIA_CONTENT_TYPE): cv.string, - vol.Required(ATTR_MEDIA_CONTENT_ID): cv.string, - vol.Optional(ATTR_MEDIA_ENQUEUE): cv.boolean, - } -) - -MEDIA_PLAYER_SET_SHUFFLE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_MEDIA_SHUFFLE): cv.boolean} -) +MEDIA_PLAYER_PLAY_MEDIA_SCHEMA = { + vol.Required(ATTR_MEDIA_CONTENT_TYPE): cv.string, + vol.Required(ATTR_MEDIA_CONTENT_ID): cv.string, + vol.Optional(ATTR_MEDIA_ENQUEUE): cv.boolean, +} ATTR_TO_PROPERTY = [ ATTR_MEDIA_VOLUME_LEVEL, @@ -223,65 +191,56 @@ async def async_setup(hass, config): await component.async_setup(config) component.async_register_entity_service( - SERVICE_TURN_ON, ENTITY_SERVICE_SCHEMA, "async_turn_on", [SUPPORT_TURN_ON] + SERVICE_TURN_ON, {}, "async_turn_on", [SUPPORT_TURN_ON] ) component.async_register_entity_service( - SERVICE_TURN_OFF, ENTITY_SERVICE_SCHEMA, "async_turn_off", [SUPPORT_TURN_OFF] + SERVICE_TURN_OFF, {}, "async_turn_off", [SUPPORT_TURN_OFF] ) component.async_register_entity_service( - SERVICE_TOGGLE, - ENTITY_SERVICE_SCHEMA, - "async_toggle", - [SUPPORT_TURN_OFF | SUPPORT_TURN_ON], + SERVICE_TOGGLE, {}, "async_toggle", [SUPPORT_TURN_OFF | SUPPORT_TURN_ON], ) component.async_register_entity_service( SERVICE_VOLUME_UP, - ENTITY_SERVICE_SCHEMA, + {}, "async_volume_up", [SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP], ) component.async_register_entity_service( SERVICE_VOLUME_DOWN, - ENTITY_SERVICE_SCHEMA, + {}, "async_volume_down", [SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP], ) component.async_register_entity_service( SERVICE_MEDIA_PLAY_PAUSE, - ENTITY_SERVICE_SCHEMA, + {}, "async_media_play_pause", [SUPPORT_PLAY | SUPPORT_PAUSE], ) component.async_register_entity_service( - SERVICE_MEDIA_PLAY, ENTITY_SERVICE_SCHEMA, "async_media_play", [SUPPORT_PLAY] + SERVICE_MEDIA_PLAY, {}, "async_media_play", [SUPPORT_PLAY] ) component.async_register_entity_service( - SERVICE_MEDIA_PAUSE, ENTITY_SERVICE_SCHEMA, "async_media_pause", [SUPPORT_PAUSE] + SERVICE_MEDIA_PAUSE, {}, "async_media_pause", [SUPPORT_PAUSE] ) component.async_register_entity_service( - SERVICE_MEDIA_STOP, ENTITY_SERVICE_SCHEMA, "async_media_stop", [SUPPORT_STOP] + SERVICE_MEDIA_STOP, {}, "async_media_stop", [SUPPORT_STOP] ) component.async_register_entity_service( - SERVICE_MEDIA_NEXT_TRACK, - ENTITY_SERVICE_SCHEMA, - "async_media_next_track", - [SUPPORT_NEXT_TRACK], + SERVICE_MEDIA_NEXT_TRACK, {}, "async_media_next_track", [SUPPORT_NEXT_TRACK], ) component.async_register_entity_service( SERVICE_MEDIA_PREVIOUS_TRACK, - ENTITY_SERVICE_SCHEMA, + {}, "async_media_previous_track", [SUPPORT_PREVIOUS_TRACK], ) component.async_register_entity_service( - SERVICE_CLEAR_PLAYLIST, - ENTITY_SERVICE_SCHEMA, - "async_clear_playlist", - [SUPPORT_CLEAR_PLAYLIST], + SERVICE_CLEAR_PLAYLIST, {}, "async_clear_playlist", [SUPPORT_CLEAR_PLAYLIST], ) component.async_register_entity_service( SERVICE_VOLUME_SET, - MEDIA_PLAYER_SET_VOLUME_SCHEMA, + {vol.Required(ATTR_MEDIA_VOLUME_LEVEL): cv.small_float}, lambda entity, call: entity.async_set_volume_level( volume=call.data[ATTR_MEDIA_VOLUME_LEVEL] ), @@ -289,7 +248,7 @@ async def async_setup(hass, config): ) component.async_register_entity_service( SERVICE_VOLUME_MUTE, - MEDIA_PLAYER_MUTE_VOLUME_SCHEMA, + {vol.Required(ATTR_MEDIA_VOLUME_MUTED): cv.boolean}, lambda entity, call: entity.async_mute_volume( mute=call.data[ATTR_MEDIA_VOLUME_MUTED] ), @@ -297,7 +256,11 @@ async def async_setup(hass, config): ) component.async_register_entity_service( SERVICE_MEDIA_SEEK, - MEDIA_PLAYER_MEDIA_SEEK_SCHEMA, + { + vol.Required(ATTR_MEDIA_SEEK_POSITION): vol.All( + vol.Coerce(float), vol.Range(min=0) + ) + }, lambda entity, call: entity.async_media_seek( position=call.data[ATTR_MEDIA_SEEK_POSITION] ), @@ -305,13 +268,13 @@ async def async_setup(hass, config): ) component.async_register_entity_service( SERVICE_SELECT_SOURCE, - MEDIA_PLAYER_SELECT_SOURCE_SCHEMA, + {vol.Required(ATTR_INPUT_SOURCE): cv.string}, "async_select_source", [SUPPORT_SELECT_SOURCE], ) component.async_register_entity_service( SERVICE_SELECT_SOUND_MODE, - MEDIA_PLAYER_SELECT_SOUND_MODE_SCHEMA, + {vol.Required(ATTR_SOUND_MODE): cv.string}, "async_select_sound_mode", [SUPPORT_SELECT_SOUND_MODE], ) @@ -327,7 +290,7 @@ async def async_setup(hass, config): ) component.async_register_entity_service( SERVICE_SHUFFLE_SET, - MEDIA_PLAYER_SET_SHUFFLE_SCHEMA, + {vol.Required(ATTR_MEDIA_SHUFFLE): cv.boolean}, "async_set_shuffle", [SUPPORT_SHUFFLE_SET], ) diff --git a/homeassistant/components/media_player/device_condition.py b/homeassistant/components/media_player/device_condition.py index a67a084a94f..a8091a6aed8 100644 --- a/homeassistant/components/media_player/device_condition.py +++ b/homeassistant/components/media_player/device_condition.py @@ -1,24 +1,26 @@ """Provides device automations for Media player.""" from typing import Dict, List + import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, CONF_CONDITION, - CONF_DOMAIN, - CONF_TYPE, CONF_DEVICE_ID, + CONF_DOMAIN, CONF_ENTITY_ID, + CONF_TYPE, + STATE_IDLE, STATE_OFF, STATE_ON, - STATE_IDLE, STATE_PAUSED, STATE_PLAYING, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import condition, config_validation as cv, entity_registry -from homeassistant.helpers.typing import ConfigType, TemplateVarsType from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA +from homeassistant.helpers.typing import ConfigType, TemplateVarsType + from . import DOMAIN CONDITION_TYPES = {"is_on", "is_off", "is_idle", "is_paused", "is_playing"} diff --git a/homeassistant/components/media_player/reproduce_state.py b/homeassistant/components/media_player/reproduce_state.py index dac08afe471..dc9078d3ffd 100644 --- a/homeassistant/components/media_player/reproduce_state.py +++ b/homeassistant/components/media_player/reproduce_state.py @@ -21,21 +21,20 @@ from homeassistant.core import Context, State from homeassistant.helpers.typing import HomeAssistantType from .const import ( + ATTR_INPUT_SOURCE, + ATTR_MEDIA_CONTENT_ID, + ATTR_MEDIA_CONTENT_TYPE, + ATTR_MEDIA_ENQUEUE, + ATTR_MEDIA_SEEK_POSITION, ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED, - ATTR_MEDIA_SEEK_POSITION, - ATTR_INPUT_SOURCE, ATTR_SOUND_MODE, - ATTR_MEDIA_CONTENT_TYPE, - ATTR_MEDIA_CONTENT_ID, - ATTR_MEDIA_ENQUEUE, - SERVICE_PLAY_MEDIA, - SERVICE_SELECT_SOURCE, - SERVICE_SELECT_SOUND_MODE, DOMAIN, + SERVICE_PLAY_MEDIA, + SERVICE_SELECT_SOUND_MODE, + SERVICE_SELECT_SOURCE, ) - # mypy: allow-untyped-defs diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index 5421085c308..19ef1cb14c0 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -107,20 +107,6 @@ media_seek: description: Position to seek to. The format is platform dependent. example: 100 -monoprice_snapshot: - description: Take a snapshot of the media player zone. - fields: - entity_id: - description: Name(s) of entities that will be snapshot. Platform dependent. - example: 'media_player.living_room' - -monoprice_restore: - description: Restore a snapshot of the media player zone. - fields: - entity_id: - description: Name(s) of entities that will be restored. Platform dependent. - example: 'media_player.living_room' - play_media: description: Send the media player the command for playing media. fields: @@ -139,7 +125,7 @@ select_source: fields: entity_id: description: Name(s) of entities to change source on. - example: 'media_player.media_player.txnr535_0009b0d81f82' + example: 'media_player.txnr535_0009b0d81f82' source: description: Name of the source to switch to. Platform dependent. example: 'video1' @@ -170,155 +156,3 @@ shuffle_set: shuffle: description: True/false for enabling/disabling shuffle. example: true - -channels_seek_forward: - description: Seek forward by a set number of seconds. - fields: - entity_id: - description: Name of entity for the instance of Channels to seek in. - example: 'media_player.family_room_channels' - -channels_seek_backward: - description: Seek backward by a set number of seconds. - fields: - entity_id: - description: Name of entity for the instance of Channels to seek in. - example: 'media_player.family_room_channels' - -channels_seek_by: - description: Seek by an inputted number of seconds. - fields: - entity_id: - description: Name of entity for the instance of Channels to seek in. - example: 'media_player.family_room_channels' - seconds: - description: Number of seconds to seek by. Negative numbers seek backwards. - example: 120 - -soundtouch_play_everywhere: - description: Play on all Bose Soundtouch devices. - fields: - master: - description: Name of the master entity that will coordinate the grouping. Platform dependent. It is a shortcut for creating a multi-room zone with all devices - example: 'media_player.soundtouch_home' - -soundtouch_create_zone: - description: Create a Sountouch multi-room zone. - fields: - master: - description: Name of the master entity that will coordinate the multi-room zone. Platform dependent. - example: 'media_player.soundtouch_home' - slaves: - description: Name of slaves entities to add to the new zone. - example: 'media_player.soundtouch_bedroom' - -soundtouch_add_zone_slave: - description: Add a slave to a Sountouch multi-room zone. - fields: - master: - description: Name of the master entity that is coordinating the multi-room zone. Platform dependent. - example: 'media_player.soundtouch_home' - slaves: - description: Name of slaves entities to add to the existing zone. - example: 'media_player.soundtouch_bedroom' - -soundtouch_remove_zone_slave: - description: Remove a slave from the Sounttouch multi-room zone. - fields: - master: - description: Name of the master entity that is coordinating the multi-room zone. Platform dependent. - example: 'media_player.soundtouch_home' - slaves: - description: Name of slaves entities to remove from the existing zone. - example: 'media_player.soundtouch_bedroom' - -squeezebox_call_method: - description: 'Call a Squeezebox JSON/RPC API method.' - fields: - entity_id: - description: Name(s) of the Squeexebox entities where to run the API method. - example: 'media_player.squeezebox_radio' - command: - description: Name of the Squeezebox command. - example: 'playlist' - parameters: - description: Optional array of parameters to be appended to the command. See 'Command Line Interface' official help page from Logitech for details. - example: '["loadtracks", "track.titlesearch=highway to hell"]' - -yamaha_enable_output: - description: Enable or disable an output port - fields: - entity_id: - description: Name(s) of entites to enable/disable port on. - example: 'media_player.yamaha' - port: - description: Name of port to enable/disable. - example: 'hdmi1' - enabled: - description: Boolean indicating if port should be enabled or not. - example: true - -bluesound_join: - description: Group player together. - fields: - master: - description: Entity ID of the player that should become the master of the group. - example: 'media_player.bluesound_livingroom' - entity_id: - description: Name(s) of entities that will coordinate the grouping. Platform dependent. - example: 'media_player.bluesound_livingroom' - -bluesound_unjoin: - description: Unjoin the player from a group. - fields: - entity_id: - description: Name(s) of entities that will be unjoined from their group. Platform dependent. - example: 'media_player.bluesound_livingroom' - -bluesound_set_sleep_timer: - description: "Set a Bluesound timer. It will increase timer in steps: 15, 30, 45, 60, 90, 0" - fields: - entity_id: - description: Name(s) of entities that will have a timer set. - example: 'media_player.bluesound_livingroom' - -bluesound_clear_sleep_timer: - description: Clear a Bluesound timer. - fields: - entity_id: - description: Name(s) of entities that will have the timer cleared. - example: 'media_player.bluesound_livingroom' - -songpal_set_sound_setting: - description: Change sound setting. - - fields: - entity_id: - description: Target device. - example: 'media_player.my_soundbar' - name: - description: Name of the setting. - example: 'nightMode' - value: - description: Value to set. - example: 'on' - -blackbird_set_all_zones: - description: Set all Blackbird zones to a single source. - fields: - entity_id: - description: Name of any blackbird zone. - example: 'media_player.zone_1' - source: - description: Name of source to switch to. - example: 'Source 1' - -epson_select_cmode: - description: Select Color mode of Epson projector - fields: - entity_id: - description: Name of projector - example: 'media_player.epson_projector' - cmode: - description: Name of Cmode - example: 'cinema' diff --git a/homeassistant/components/mediaroom/media_player.py b/homeassistant/components/mediaroom/media_player.py index 8e02ee56a75..539138783ee 100644 --- a/homeassistant/components/mediaroom/media_player.py +++ b/homeassistant/components/mediaroom/media_player.py @@ -1,9 +1,10 @@ """Support for the Mediaroom Set-up-box.""" import logging +from pymediaroom import PyMediaroomError, Remote, State, install_mediaroom_protocol import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, SUPPORT_NEXT_TRACK, @@ -99,7 +100,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([new_stb]) if not config[CONF_OPTIMISTIC]: - from pymediaroom import install_mediaroom_protocol already_installed = hass.data.get(DISCOVERY_MEDIAROOM, None) if not already_installed: @@ -123,7 +123,6 @@ class MediaroomDevice(MediaPlayerDevice): def set_state(self, mediaroom_state): """Map pymediaroom state to HA state.""" - from pymediaroom import State state_map = { State.OFF: STATE_OFF, @@ -139,7 +138,6 @@ class MediaroomDevice(MediaPlayerDevice): def __init__(self, host, device_id, optimistic=False, timeout=DEFAULT_TIMEOUT): """Initialize the device.""" - from pymediaroom import Remote self.host = host self.stb = Remote(host) @@ -184,7 +182,6 @@ class MediaroomDevice(MediaPlayerDevice): async def async_play_media(self, media_type, media_id, **kwargs): """Play media.""" - from pymediaroom import PyMediaroomError _LOGGER.debug( "STB(%s) Play media: %s (%s)", self.stb.stb_ip, media_id, media_type @@ -237,7 +234,6 @@ class MediaroomDevice(MediaPlayerDevice): async def async_turn_on(self): """Turn on the receiver.""" - from pymediaroom import PyMediaroomError try: self.set_state(await self.stb.turn_on()) @@ -250,7 +246,6 @@ class MediaroomDevice(MediaPlayerDevice): async def async_turn_off(self): """Turn off the receiver.""" - from pymediaroom import PyMediaroomError try: self.set_state(await self.stb.turn_off()) @@ -263,7 +258,6 @@ class MediaroomDevice(MediaPlayerDevice): async def async_media_play(self): """Send play command.""" - from pymediaroom import PyMediaroomError try: _LOGGER.debug("media_play()") @@ -277,7 +271,6 @@ class MediaroomDevice(MediaPlayerDevice): async def async_media_pause(self): """Send pause command.""" - from pymediaroom import PyMediaroomError try: await self.stb.send_cmd("PlayPause") @@ -290,7 +283,6 @@ class MediaroomDevice(MediaPlayerDevice): async def async_media_stop(self): """Send stop command.""" - from pymediaroom import PyMediaroomError try: await self.stb.send_cmd("Stop") @@ -303,7 +295,6 @@ class MediaroomDevice(MediaPlayerDevice): async def async_media_previous_track(self): """Send Program Down command.""" - from pymediaroom import PyMediaroomError try: await self.stb.send_cmd("ProgDown") @@ -316,7 +307,6 @@ class MediaroomDevice(MediaPlayerDevice): async def async_media_next_track(self): """Send Program Up command.""" - from pymediaroom import PyMediaroomError try: await self.stb.send_cmd("ProgUp") @@ -329,7 +319,6 @@ class MediaroomDevice(MediaPlayerDevice): async def async_volume_up(self): """Send volume up command.""" - from pymediaroom import PyMediaroomError try: await self.stb.send_cmd("VolUp") @@ -340,7 +329,6 @@ class MediaroomDevice(MediaPlayerDevice): async def async_volume_down(self): """Send volume up command.""" - from pymediaroom import PyMediaroomError try: await self.stb.send_cmd("VolDown") @@ -350,7 +338,6 @@ class MediaroomDevice(MediaPlayerDevice): async def async_mute_volume(self, mute): """Send mute command.""" - from pymediaroom import PyMediaroomError try: await self.stb.send_cmd("Mute") diff --git a/homeassistant/components/melissa/climate.py b/homeassistant/components/melissa/climate.py index 38f4977c96a..4b033811f43 100644 --- a/homeassistant/components/melissa/climate.py +++ b/homeassistant/components/melissa/climate.py @@ -3,6 +3,10 @@ import logging from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( + FAN_AUTO, + FAN_HIGH, + FAN_LOW, + FAN_MEDIUM, HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_DRY, @@ -12,7 +16,6 @@ from homeassistant.components.climate.const import ( SUPPORT_FAN_MODE, SUPPORT_TARGET_TEMPERATURE, ) -from homeassistant.components.fan import SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, TEMP_CELSIUS from . import DATA_MELISSA @@ -29,7 +32,7 @@ OP_MODES = [ HVAC_MODE_OFF, ] -FAN_MODES = [HVAC_MODE_AUTO, SPEED_HIGH, SPEED_MEDIUM, SPEED_LOW] +FAN_MODES = [FAN_AUTO, FAN_HIGH, FAN_MEDIUM, FAN_LOW] async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -200,11 +203,11 @@ class MelissaClimate(ClimateDevice): if fan == self._api.FAN_AUTO: return HVAC_MODE_AUTO if fan == self._api.FAN_LOW: - return SPEED_LOW + return FAN_LOW if fan == self._api.FAN_MEDIUM: - return SPEED_MEDIUM + return FAN_MEDIUM if fan == self._api.FAN_HIGH: - return SPEED_HIGH + return FAN_HIGH _LOGGER.warning("Fan mode %s could not be mapped to hass", fan) return None @@ -224,10 +227,10 @@ class MelissaClimate(ClimateDevice): """Translate hass fan modes to melissa modes.""" if fan == HVAC_MODE_AUTO: return self._api.FAN_AUTO - if fan == SPEED_LOW: + if fan == FAN_LOW: return self._api.FAN_LOW - if fan == SPEED_MEDIUM: + if fan == FAN_MEDIUM: return self._api.FAN_MEDIUM - if fan == SPEED_HIGH: + if fan == FAN_HIGH: return self._api.FAN_HIGH _LOGGER.warning("Melissa have no setting for %s fan mode", fan) diff --git a/homeassistant/components/meraki/device_tracker.py b/homeassistant/components/meraki/device_tracker.py index 3f50caa2c8b..1aa1485922e 100644 --- a/homeassistant/components/meraki/device_tracker.py +++ b/homeassistant/components/meraki/device_tracker.py @@ -5,15 +5,16 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.meraki/ """ -import logging import json +import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv + +from homeassistant.components.device_tracker import PLATFORM_SCHEMA, SOURCE_TYPE_ROUTER +from homeassistant.components.http import HomeAssistantView from homeassistant.const import HTTP_BAD_REQUEST, HTTP_UNPROCESSABLE_ENTITY from homeassistant.core import callback -from homeassistant.components.http import HomeAssistantView -from homeassistant.components.device_tracker import PLATFORM_SCHEMA, SOURCE_TYPE_ROUTER +import homeassistant.helpers.config_validation as cv CONF_VALIDATOR = "validator" CONF_SECRET = "secret" diff --git a/homeassistant/components/met/.translations/bg.json b/homeassistant/components/met/.translations/bg.json index aabb1aeda3f..aa85bed1d13 100644 --- a/homeassistant/components/met/.translations/bg.json +++ b/homeassistant/components/met/.translations/bg.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "\u0418\u043c\u0435\u0442\u043e \u0432\u0435\u0447\u0435 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430" + "name_exists": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0442\u043e \u0432\u0435\u0447\u0435 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430" }, "step": { "user": { diff --git a/homeassistant/components/met/.translations/da.json b/homeassistant/components/met/.translations/da.json index 9d44f1b2b6c..e36a6511aa3 100644 --- a/homeassistant/components/met/.translations/da.json +++ b/homeassistant/components/met/.translations/da.json @@ -12,7 +12,7 @@ "name": "Navn" }, "description": "Meteorologisk institutt", - "title": "Placering" + "title": "Lokalitet" } }, "title": "Met.no" diff --git a/homeassistant/components/met/__init__.py b/homeassistant/components/met/__init__.py index 305038c3e6a..7ef3c5f8796 100644 --- a/homeassistant/components/met/__init__.py +++ b/homeassistant/components/met/__init__.py @@ -1,5 +1,6 @@ """The met component.""" from homeassistant.core import Config, HomeAssistant + from .config_flow import MetFlowHandler # noqa: F401 from .const import DOMAIN # noqa: F401 diff --git a/homeassistant/components/met/config_flow.py b/homeassistant/components/met/config_flow.py index c7ff4973c7d..759f7f6fc89 100644 --- a/homeassistant/components/met/config_flow.py +++ b/homeassistant/components/met/config_flow.py @@ -6,7 +6,7 @@ from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, C from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from .const import DOMAIN, HOME_LOCATION_NAME, CONF_TRACK_HOME +from .const import CONF_TRACK_HOME, DOMAIN, HOME_LOCATION_NAME @callback diff --git a/homeassistant/components/met/weather.py b/homeassistant/components/met/weather.py index 2f9ddc5a67c..d99573a985e 100644 --- a/homeassistant/components/met/weather.py +++ b/homeassistant/components/met/weather.py @@ -5,16 +5,16 @@ from random import randrange import metno import voluptuous as vol -from homeassistant.core import callback from homeassistant.components.weather import PLATFORM_SCHEMA, WeatherEntity from homeassistant.const import ( CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, - TEMP_CELSIUS, EVENT_CORE_CONFIG_UPDATE, + TEMP_CELSIUS, ) +from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.event import async_call_later diff --git a/homeassistant/components/meteo_france/__init__.py b/homeassistant/components/meteo_france/__init__.py index cfcd78400bd..73b8dbb0e39 100644 --- a/homeassistant/components/meteo_france/__init__.py +++ b/homeassistant/components/meteo_france/__init__.py @@ -2,6 +2,8 @@ import datetime import logging +from meteofrance.client import meteofranceClient, meteofranceError +from vigilancemeteo import VigilanceMeteoError, VigilanceMeteoFranceProxy import voluptuous as vol from homeassistant.const import CONF_MONITORED_CONDITIONS @@ -9,7 +11,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform from homeassistant.util import Throttle -from .const import DOMAIN, CONF_CITY, SENSOR_TYPES, DATA_METEO_FRANCE +from .const import CONF_CITY, DATA_METEO_FRANCE, DOMAIN, SENSOR_TYPES _LOGGER = logging.getLogger(__name__) @@ -61,7 +63,6 @@ def setup(hass, config): # all weather_alert entities. if need_weather_alert_watcher: _LOGGER.debug("Weather Alert monitoring expected. Loading vigilancemeteo") - from vigilancemeteo import VigilanceMeteoFranceProxy, VigilanceMeteoError weather_alert_client = VigilanceMeteoFranceProxy() try: @@ -79,8 +80,6 @@ def setup(hass, config): city = location[CONF_CITY] - from meteofrance.client import meteofranceClient, meteofranceError - try: client = meteofranceClient(city) except meteofranceError as exp: @@ -127,7 +126,6 @@ class MeteoFranceUpdater: @Throttle(SCAN_INTERVAL) def update(self): """Get the latest data from Meteo-France.""" - from meteofrance.client import meteofranceError try: self._client.update() diff --git a/homeassistant/components/meteo_france/sensor.py b/homeassistant/components/meteo_france/sensor.py index 8c2bd32048f..f0c08ac1822 100644 --- a/homeassistant/components/meteo_france/sensor.py +++ b/homeassistant/components/meteo_france/sensor.py @@ -1,6 +1,8 @@ """Support for Meteo-France raining forecast sensor.""" import logging +from vigilancemeteo import DepartmentWeatherAlert + from homeassistant.const import ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS from homeassistant.helpers.entity import Entity @@ -8,11 +10,11 @@ from .const import ( ATTRIBUTION, CONF_CITY, DATA_METEO_FRANCE, - SENSOR_TYPES, + SENSOR_TYPE_CLASS, SENSOR_TYPE_ICON, SENSOR_TYPE_NAME, SENSOR_TYPE_UNIT, - SENSOR_TYPE_CLASS, + SENSOR_TYPES, ) _LOGGER = logging.getLogger(__name__) @@ -31,8 +33,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): client = hass.data[DATA_METEO_FRANCE][city] weather_alert_client = hass.data[DATA_METEO_FRANCE]["weather_alert_client"] - from vigilancemeteo import DepartmentWeatherAlert - alert_watcher = None if "weather_alert" in monitored_conditions: datas = hass.data[DATA_METEO_FRANCE][city].get_data() diff --git a/homeassistant/components/meteo_france/weather.py b/homeassistant/components/meteo_france/weather.py index 00da55809ff..c96080808e9 100644 --- a/homeassistant/components/meteo_france/weather.py +++ b/homeassistant/components/meteo_france/weather.py @@ -9,8 +9,8 @@ from homeassistant.components.weather import ( ATTR_FORECAST_TIME, WeatherEntity, ) -import homeassistant.util.dt as dt_util from homeassistant.const import TEMP_CELSIUS +import homeassistant.util.dt as dt_util from .const import ATTRIBUTION, CONDITION_CLASSES, CONF_CITY, DATA_METEO_FRANCE diff --git a/homeassistant/components/meteoalarm/binary_sensor.py b/homeassistant/components/meteoalarm/binary_sensor.py index 55041f59cf2..7d3bea4c995 100644 --- a/homeassistant/components/meteoalarm/binary_sensor.py +++ b/homeassistant/components/meteoalarm/binary_sensor.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +from meteoalertapi import Meteoalert import voluptuous as vol from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice @@ -33,7 +34,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the MeteoAlarm binary sensor platform.""" - from meteoalertapi import Meteoalert country = config[CONF_COUNTRY] province = config[CONF_PROVINCE] diff --git a/homeassistant/components/metoffice/manifest.json b/homeassistant/components/metoffice/manifest.json index f5624e33edb..572b3a7bcb8 100644 --- a/homeassistant/components/metoffice/manifest.json +++ b/homeassistant/components/metoffice/manifest.json @@ -2,9 +2,7 @@ "domain": "metoffice", "name": "Metoffice", "documentation": "https://www.home-assistant.io/integrations/metoffice", - "requirements": [ - "datapoint==0.4.3" - ], + "requirements": ["datapoint==0.9.5"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/mfi/sensor.py b/homeassistant/components/mfi/sensor.py index 5d9b3be738a..671a52bbf01 100644 --- a/homeassistant/components/mfi/sensor.py +++ b/homeassistant/components/mfi/sensor.py @@ -1,23 +1,24 @@ """Support for Ubiquiti mFi sensors.""" import logging +from mficlient.client import FailedToLogin, MFiClient import requests import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_PASSWORD, - CONF_USERNAME, - TEMP_CELSIUS, - STATE_ON, - STATE_OFF, CONF_HOST, - CONF_SSL, - CONF_VERIFY_SSL, + CONF_PASSWORD, CONF_PORT, + CONF_SSL, + CONF_USERNAME, + CONF_VERIFY_SSL, + STATE_OFF, + STATE_ON, + TEMP_CELSIUS, ) -from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -57,8 +58,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): default_port = 6443 if use_tls else 6080 port = int(config.get(CONF_PORT, default_port)) - from mficlient.client import FailedToLogin, MFiClient - try: client = MFiClient( host, username, password, port=port, use_tls=use_tls, verify=verify_tls diff --git a/homeassistant/components/mfi/switch.py b/homeassistant/components/mfi/switch.py index 1da09e7f78c..00cb23a102e 100644 --- a/homeassistant/components/mfi/switch.py +++ b/homeassistant/components/mfi/switch.py @@ -1,16 +1,17 @@ """Support for Ubiquiti mFi switches.""" import logging +from mficlient.client import FailedToLogin, MFiClient import requests import voluptuous as vol -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import ( CONF_HOST, - CONF_PORT, CONF_PASSWORD, - CONF_USERNAME, + CONF_PORT, CONF_SSL, + CONF_USERNAME, CONF_VERIFY_SSL, ) import homeassistant.helpers.config_validation as cv @@ -44,8 +45,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): default_port = 6443 if use_tls else 6080 port = int(config.get(CONF_PORT, default_port)) - from mficlient.client import FailedToLogin, MFiClient - try: client = MFiClient( host, username, password, port=port, use_tls=use_tls, verify=verify_tls diff --git a/homeassistant/components/mhz19/sensor.py b/homeassistant/components/mhz19/sensor.py index 460decd41b6..aedd5ea9b09 100644 --- a/homeassistant/components/mhz19/sensor.py +++ b/homeassistant/components/mhz19/sensor.py @@ -1,20 +1,21 @@ """Support for CO2 sensor connected to a serial port.""" -import logging from datetime import timedelta +import logging +from pmsensor import co2sensor import voluptuous as vol +from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( ATTR_TEMPERATURE, - CONF_NAME, CONF_MONITORED_CONDITIONS, + CONF_NAME, TEMP_FAHRENHEIT, ) -from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.util.temperature import celsius_to_fahrenheit +from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle +from homeassistant.util.temperature import celsius_to_fahrenheit _LOGGER = logging.getLogger(__name__) @@ -41,7 +42,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the available CO2 sensors.""" - from pmsensor import co2sensor try: co2sensor.read_mh_z19(config.get(CONF_SERIAL_DEVICE)) @@ -116,9 +116,9 @@ class MHZ19Sensor(Entity): class MHZClient: """Get the latest data from the MH-Z sensor.""" - def __init__(self, co2sensor, serial): + def __init__(self, co2sens, serial): """Initialize the sensor.""" - self.co2sensor = co2sensor + self.co2sensor = co2sens self._serial = serial self.data = dict() diff --git a/homeassistant/components/microsoft/tts.py b/homeassistant/components/microsoft/tts.py index 447d2a4d46a..074605e07fe 100644 --- a/homeassistant/components/microsoft/tts.py +++ b/homeassistant/components/microsoft/tts.py @@ -2,6 +2,7 @@ from http.client import HTTPException import logging +from pycsspeechtts import pycsspeechtts import voluptuous as vol from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider @@ -142,7 +143,6 @@ class MicrosoftProvider(Provider): """Load TTS from Microsoft.""" if language is None: language = self._lang - from pycsspeechtts import pycsspeechtts try: trans = pycsspeechtts.TTSTranslator(self._apikey, self._region) diff --git a/homeassistant/components/microsoft_face/__init__.py b/homeassistant/components/microsoft_face/__init__.py index 5d0c50e536a..244c8a0e8ee 100644 --- a/homeassistant/components/microsoft_face/__init__.py +++ b/homeassistant/components/microsoft_face/__init__.py @@ -8,7 +8,7 @@ from aiohttp.hdrs import CONTENT_TYPE import async_timeout import voluptuous as vol -from homeassistant.const import CONF_API_KEY, CONF_TIMEOUT, ATTR_NAME +from homeassistant.const import ATTR_NAME, CONF_API_KEY, CONF_TIMEOUT from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/miflora/sensor.py b/homeassistant/components/miflora/sensor.py index a08c4ce5eac..815a6e97bb8 100644 --- a/homeassistant/components/miflora/sensor.py +++ b/homeassistant/components/miflora/sensor.py @@ -106,6 +106,7 @@ class MiFloraSensor(Entity): self._icon = icon self._name = name self._state = None + self._available = False self.data = [] self._force_update = force_update # Median is used to filter out outliers. median of 3 will filter @@ -132,6 +133,11 @@ class MiFloraSensor(Entity): """Return the state of the sensor.""" return self._state + @property + def available(self): + """Return True if entity is available.""" + return self._available + @property def unit_of_measurement(self): """Return the units of measurement.""" @@ -156,15 +162,14 @@ class MiFloraSensor(Entity): try: _LOGGER.debug("Polling data for %s", self.name) data = self.poller.parameter_value(self.parameter) - except OSError as ioerr: - _LOGGER.info("Polling error %s", ioerr) - return - except BluetoothBackendException as bterror: - _LOGGER.info("Polling error %s", bterror) + except (OSError, BluetoothBackendException) as err: + _LOGGER.info("Polling error %s: %s", type(err).__name__, err) + self._available = False return if data is not None: _LOGGER.debug("%s = %s", self.name, data) + self._available = True self.data.append(data) else: _LOGGER.info("Did not receive any data from Mi Flora sensor %s", self.name) diff --git a/homeassistant/components/mikrotik/__init__.py b/homeassistant/components/mikrotik/__init__.py index aacd3c65b3e..9b533288d86 100644 --- a/homeassistant/components/mikrotik/__init__.py +++ b/homeassistant/components/mikrotik/__init__.py @@ -2,34 +2,35 @@ import logging import ssl -import voluptuous as vol import librouteros from librouteros.login import login_plain, login_token +import voluptuous as vol +from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER from homeassistant.const import ( CONF_HOST, + CONF_METHOD, CONF_PASSWORD, - CONF_USERNAME, CONF_PORT, CONF_SSL, - CONF_METHOD, + CONF_USERNAME, ) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.discovery import load_platform -from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER + from .const import ( - NAME, + CONF_ARP_PING, + CONF_ENCODING, + CONF_LOGIN_METHOD, + CONF_TRACK_DEVICES, + DEFAULT_ENCODING, DOMAIN, HOSTS, + IDENTITY, + MIKROTIK_SERVICES, MTK_LOGIN_PLAIN, MTK_LOGIN_TOKEN, - DEFAULT_ENCODING, - IDENTITY, - CONF_TRACK_DEVICES, - CONF_ENCODING, - CONF_ARP_PING, - CONF_LOGIN_METHOD, - MIKROTIK_SERVICES, + NAME, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/mikrotik/device_tracker.py b/homeassistant/components/mikrotik/device_tracker.py index 6c3fb559cba..92fcfac4ae4 100644 --- a/homeassistant/components/mikrotik/device_tracker.py +++ b/homeassistant/components/mikrotik/device_tracker.py @@ -5,18 +5,19 @@ from homeassistant.components.device_tracker import ( DOMAIN as DEVICE_TRACKER, DeviceScanner, ) -from homeassistant.util import slugify from homeassistant.const import CONF_METHOD +from homeassistant.util import slugify + from .const import ( - HOSTS, - MIKROTIK, - CONF_ARP_PING, - MIKROTIK_SERVICES, - CAPSMAN, - WIRELESS, - DHCP, ARP, ATTR_DEVICE_TRACKER, + CAPSMAN, + CONF_ARP_PING, + DHCP, + HOSTS, + MIKROTIK, + MIKROTIK_SERVICES, + WIRELESS, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/mill/climate.py b/homeassistant/components/mill/climate.py index 1166bd451c0..875d217247c 100644 --- a/homeassistant/components/mill/climate.py +++ b/homeassistant/components/mill/climate.py @@ -6,12 +6,11 @@ import voluptuous as vol from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice from homeassistant.components.climate.const import ( - DOMAIN, + FAN_ON, HVAC_MODE_HEAT, HVAC_MODE_OFF, SUPPORT_FAN_MODE, SUPPORT_TARGET_TEMPERATURE, - FAN_ON, ) from homeassistant.const import ( ATTR_TEMPERATURE, @@ -22,15 +21,18 @@ from homeassistant.const import ( from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession -_LOGGER = logging.getLogger(__name__) +from .const import ( + ATTR_AWAY_TEMP, + ATTR_COMFORT_TEMP, + ATTR_ROOM_NAME, + ATTR_SLEEP_TEMP, + DOMAIN, + MAX_TEMP, + MIN_TEMP, + SERVICE_SET_ROOM_TEMP, +) -ATTR_AWAY_TEMP = "away_temp" -ATTR_COMFORT_TEMP = "comfort_temp" -ATTR_ROOM_NAME = "room_name" -ATTR_SLEEP_TEMP = "sleep_temp" -MAX_TEMP = 35 -MIN_TEMP = 5 -SERVICE_SET_ROOM_TEMP = "mill_set_room_temperature" +_LOGGER = logging.getLogger(__name__) SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE diff --git a/homeassistant/components/mill/const.py b/homeassistant/components/mill/const.py new file mode 100644 index 00000000000..65c67b72b6e --- /dev/null +++ b/homeassistant/components/mill/const.py @@ -0,0 +1,10 @@ +"""Constants for the Mill heater component.""" + +ATTR_AWAY_TEMP = "away_temp" +ATTR_COMFORT_TEMP = "comfort_temp" +ATTR_ROOM_NAME = "room_name" +ATTR_SLEEP_TEMP = "sleep_temp" +MAX_TEMP = 35 +MIN_TEMP = 5 +DOMAIN = "mill" +SERVICE_SET_ROOM_TEMP = "set_room_temperature" diff --git a/homeassistant/components/mill/services.yaml b/homeassistant/components/mill/services.yaml index e69de29bb2d..e9e59b50170 100644 --- a/homeassistant/components/mill/services.yaml +++ b/homeassistant/components/mill/services.yaml @@ -0,0 +1,15 @@ +set_room_temperature: + description: Set Mill room temperatures. + fields: + room_name: + description: Name of room to change. + example: 'kitchen' + away_temp: + description: Away temp. + example: 12 + comfort_temp: + description: Comfort temp. + example: 22 + sleep_temp: + description: Sleep temp. + example: 17 \ No newline at end of file diff --git a/homeassistant/components/min_max/sensor.py b/homeassistant/components/min_max/sensor.py index 519bba464de..80beaf1f798 100644 --- a/homeassistant/components/min_max/sensor.py +++ b/homeassistant/components/min_max/sensor.py @@ -3,16 +3,16 @@ import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, - STATE_UNKNOWN, - STATE_UNAVAILABLE, - CONF_TYPE, ATTR_UNIT_OF_MEASUREMENT, + CONF_NAME, + CONF_TYPE, + STATE_UNAVAILABLE, + STATE_UNKNOWN, ) 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 @@ -153,7 +153,7 @@ class MinMaxSensor(Entity): self.last = float(new_state.state) except ValueError: _LOGGER.warning( - "Unable to store state. " "Only numerical states are supported" + "Unable to store state. Only numerical states are supported" ) hass.async_add_job(self.async_update_ha_state, True) diff --git a/homeassistant/components/minio/__init__.py b/homeassistant/components/minio/__init__.py index d411d913082..4f5159ed9d5 100644 --- a/homeassistant/components/minio/__init__.py +++ b/homeassistant/components/minio/__init__.py @@ -1,8 +1,8 @@ """Minio component.""" import logging import os -import threading from queue import Queue +import threading from typing import List import voluptuous as vol @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP import homeassistant.helpers.config_validation as cv -from .minio_helper import create_minio_client, MinioEventThread +from .minio_helper import MinioEventThread, create_minio_client _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/minio/minio_helper.py b/homeassistant/components/minio/minio_helper.py index bd7b15d27d4..2aaba9d4085 100644 --- a/homeassistant/components/minio/minio_helper.py +++ b/homeassistant/components/minio/minio_helper.py @@ -1,11 +1,11 @@ """Minio helper methods.""" -import time from collections.abc import Iterable import json import logging +from queue import Queue import re import threading -from queue import Queue +import time from typing import Iterator, List from urllib.parse import unquote diff --git a/homeassistant/components/mjpeg/camera.py b/homeassistant/components/mjpeg/camera.py index b7cfa20a163..ab0409694d1 100644 --- a/homeassistant/components/mjpeg/camera.py +++ b/homeassistant/components/mjpeg/camera.py @@ -1,7 +1,7 @@ """Support for IP Cameras.""" import asyncio -import logging from contextlib import closing +import logging import aiohttp import async_timeout @@ -9,21 +9,21 @@ import requests from requests.auth import HTTPBasicAuth, HTTPDigestAuth import voluptuous as vol +from homeassistant.components.camera import PLATFORM_SCHEMA, Camera from homeassistant.const import ( - CONF_NAME, - CONF_USERNAME, - CONF_PASSWORD, CONF_AUTHENTICATION, + CONF_NAME, + CONF_PASSWORD, + CONF_USERNAME, + CONF_VERIFY_SSL, HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION, - CONF_VERIFY_SSL, -) -from homeassistant.components.camera import PLATFORM_SCHEMA, Camera -from homeassistant.helpers.aiohttp_client import ( - async_get_clientsession, - async_aiohttp_proxy_web, ) from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.aiohttp_client import ( + async_aiohttp_proxy_web, + async_get_clientsession, +) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/mobile_app/.translations/da.json b/homeassistant/components/mobile_app/.translations/da.json index 551e9957254..54dc85e7255 100644 --- a/homeassistant/components/mobile_app/.translations/da.json +++ b/homeassistant/components/mobile_app/.translations/da.json @@ -1,14 +1,14 @@ { "config": { "abort": { - "install_app": "\u00c5bn Mobile App for at konfigurere integrationen med Home Assistant. Se [dokumentationen]({apps_url}) for at f\u00e5 en liste over kompatible apps." + "install_app": "\u00c5bn mobilappen for at konfigurere integrationen med Home Assistant. Se [dokumentationen]({apps_url}) for at f\u00e5 vist en liste over kompatible apps." }, "step": { "confirm": { - "description": "Vil du konfigurere Mobile App komponenten?", - "title": "Mobile App" + "description": "Vil du konfigurere mobilapp-komponenten?", + "title": "Mobilapp" } }, - "title": "Mobile App" + "title": "Mobilapp" } } \ No newline at end of file diff --git a/homeassistant/components/mobile_app/__init__.py b/homeassistant/components/mobile_app/__init__.py index ca2a58d1f96..56594f3e2c3 100644 --- a/homeassistant/components/mobile_app/__init__.py +++ b/homeassistant/components/mobile_app/__init__.py @@ -76,14 +76,9 @@ async def async_setup_entry(hass, entry): device_registry = await dr.async_get_registry(hass) - identifiers = { - (ATTR_DEVICE_ID, registration[ATTR_DEVICE_ID]), - (CONF_WEBHOOK_ID, registration[CONF_WEBHOOK_ID]), - } - device = device_registry.async_get_or_create( config_entry_id=entry.entry_id, - identifiers=identifiers, + identifiers={(DOMAIN, registration[ATTR_DEVICE_ID])}, manufacturer=registration[ATTR_MANUFACTURER], model=registration[ATTR_MODEL], name=registration[ATTR_DEVICE_NAME], diff --git a/homeassistant/components/mobile_app/const.py b/homeassistant/components/mobile_app/const.py index 0b6a93a39ea..720cf7106e7 100644 --- a/homeassistant/components/mobile_app/const.py +++ b/homeassistant/components/mobile_app/const.py @@ -1,19 +1,4 @@ """Constants for mobile_app.""" -import voluptuous as vol - -from homeassistant.components.binary_sensor import ( - DEVICE_CLASSES as BINARY_SENSOR_CLASSES, -) -from homeassistant.components.device_tracker import ( - ATTR_BATTERY, - ATTR_GPS, - ATTR_GPS_ACCURACY, - ATTR_LOCATION_NAME, -) -from homeassistant.components.sensor import DEVICE_CLASSES as SENSOR_CLASSES -from homeassistant.const import ATTR_DOMAIN, ATTR_SERVICE, ATTR_SERVICE_DATA -from homeassistant.helpers import config_validation as cv - DOMAIN = "mobile_app" STORAGE_KEY = DOMAIN @@ -71,100 +56,6 @@ ERR_ENCRYPTION_REQUIRED = "encryption_required" ERR_SENSOR_NOT_REGISTERED = "not_registered" ERR_SENSOR_DUPLICATE_UNIQUE_ID = "duplicate_unique_id" -WEBHOOK_TYPE_CALL_SERVICE = "call_service" -WEBHOOK_TYPE_FIRE_EVENT = "fire_event" -WEBHOOK_TYPE_GET_CONFIG = "get_config" -WEBHOOK_TYPE_GET_ZONES = "get_zones" -WEBHOOK_TYPE_REGISTER_SENSOR = "register_sensor" -WEBHOOK_TYPE_RENDER_TEMPLATE = "render_template" -WEBHOOK_TYPE_UPDATE_LOCATION = "update_location" -WEBHOOK_TYPE_UPDATE_REGISTRATION = "update_registration" -WEBHOOK_TYPE_UPDATE_SENSOR_STATES = "update_sensor_states" - -WEBHOOK_TYPES = [ - WEBHOOK_TYPE_CALL_SERVICE, - WEBHOOK_TYPE_FIRE_EVENT, - WEBHOOK_TYPE_GET_CONFIG, - WEBHOOK_TYPE_GET_ZONES, - WEBHOOK_TYPE_REGISTER_SENSOR, - WEBHOOK_TYPE_RENDER_TEMPLATE, - WEBHOOK_TYPE_UPDATE_LOCATION, - WEBHOOK_TYPE_UPDATE_REGISTRATION, - WEBHOOK_TYPE_UPDATE_SENSOR_STATES, -] - - -REGISTRATION_SCHEMA = vol.Schema( - { - vol.Optional(ATTR_APP_DATA, default={}): dict, - vol.Required(ATTR_APP_ID): cv.string, - vol.Required(ATTR_APP_NAME): cv.string, - vol.Required(ATTR_APP_VERSION): cv.string, - vol.Required(ATTR_DEVICE_NAME): cv.string, - vol.Required(ATTR_MANUFACTURER): cv.string, - vol.Required(ATTR_MODEL): cv.string, - vol.Required(ATTR_OS_NAME): cv.string, - vol.Optional(ATTR_OS_VERSION): cv.string, - vol.Required(ATTR_SUPPORTS_ENCRYPTION, default=False): cv.boolean, - } -) - -UPDATE_REGISTRATION_SCHEMA = vol.Schema( - { - vol.Optional(ATTR_APP_DATA, default={}): dict, - vol.Required(ATTR_APP_VERSION): cv.string, - vol.Required(ATTR_DEVICE_NAME): cv.string, - vol.Required(ATTR_MANUFACTURER): cv.string, - vol.Required(ATTR_MODEL): cv.string, - vol.Optional(ATTR_OS_VERSION): cv.string, - } -) - -WEBHOOK_PAYLOAD_SCHEMA = vol.Schema( - { - vol.Required(ATTR_WEBHOOK_TYPE): cv.string, # vol.In(WEBHOOK_TYPES) - vol.Required(ATTR_WEBHOOK_DATA, default={}): vol.Any(dict, list), - vol.Optional(ATTR_WEBHOOK_ENCRYPTED, default=False): cv.boolean, - vol.Optional(ATTR_WEBHOOK_ENCRYPTED_DATA): cv.string, - } -) - -CALL_SERVICE_SCHEMA = vol.Schema( - { - vol.Required(ATTR_DOMAIN): cv.string, - vol.Required(ATTR_SERVICE): cv.string, - vol.Optional(ATTR_SERVICE_DATA, default={}): dict, - } -) - -FIRE_EVENT_SCHEMA = vol.Schema( - { - vol.Required(ATTR_EVENT_TYPE): cv.string, - vol.Optional(ATTR_EVENT_DATA, default={}): dict, - } -) - -RENDER_TEMPLATE_SCHEMA = vol.Schema( - { - str: { - vol.Required(ATTR_TEMPLATE): cv.template, - vol.Optional(ATTR_TEMPLATE_VARIABLES, default={}): dict, - } - } -) - -UPDATE_LOCATION_SCHEMA = vol.Schema( - { - vol.Optional(ATTR_LOCATION_NAME): cv.string, - vol.Required(ATTR_GPS): cv.gps, - vol.Required(ATTR_GPS_ACCURACY): cv.positive_int, - vol.Optional(ATTR_BATTERY): cv.positive_int, - vol.Optional(ATTR_SPEED): cv.positive_int, - vol.Optional(ATTR_ALTITUDE): cv.positive_int, - vol.Optional(ATTR_COURSE): cv.positive_int, - vol.Optional(ATTR_VERTICAL_ACCURACY): cv.positive_int, - } -) ATTR_SENSOR_ATTRIBUTES = "attributes" ATTR_SENSOR_DEVICE_CLASS = "device_class" @@ -177,49 +68,5 @@ ATTR_SENSOR_TYPE_SENSOR = "sensor" ATTR_SENSOR_UNIQUE_ID = "unique_id" ATTR_SENSOR_UOM = "unit_of_measurement" -SENSOR_TYPES = [ATTR_SENSOR_TYPE_BINARY_SENSOR, ATTR_SENSOR_TYPE_SENSOR] - -COMBINED_CLASSES = sorted(set(BINARY_SENSOR_CLASSES + SENSOR_CLASSES)) - SIGNAL_SENSOR_UPDATE = DOMAIN + "_sensor_update" SIGNAL_LOCATION_UPDATE = DOMAIN + "_location_update_{}" - -REGISTER_SENSOR_SCHEMA = vol.Schema( - { - vol.Optional(ATTR_SENSOR_ATTRIBUTES, default={}): dict, - vol.Optional(ATTR_SENSOR_DEVICE_CLASS): vol.All( - vol.Lower, vol.In(COMBINED_CLASSES) - ), - vol.Required(ATTR_SENSOR_NAME): cv.string, - vol.Required(ATTR_SENSOR_TYPE): vol.In(SENSOR_TYPES), - vol.Required(ATTR_SENSOR_UNIQUE_ID): cv.string, - vol.Optional(ATTR_SENSOR_UOM): cv.string, - vol.Required(ATTR_SENSOR_STATE): vol.Any(bool, str, int, float), - vol.Optional(ATTR_SENSOR_ICON, default="mdi:cellphone"): cv.icon, - } -) - -UPDATE_SENSOR_STATE_SCHEMA = vol.All( - cv.ensure_list, - [ - vol.Schema( - { - vol.Optional(ATTR_SENSOR_ATTRIBUTES, default={}): dict, - vol.Optional(ATTR_SENSOR_ICON, default="mdi:cellphone"): cv.icon, - vol.Required(ATTR_SENSOR_STATE): vol.Any(bool, str, int, float), - vol.Required(ATTR_SENSOR_TYPE): vol.In(SENSOR_TYPES), - vol.Required(ATTR_SENSOR_UNIQUE_ID): cv.string, - } - ) - ], -) - -WEBHOOK_SCHEMAS = { - WEBHOOK_TYPE_CALL_SERVICE: CALL_SERVICE_SCHEMA, - WEBHOOK_TYPE_FIRE_EVENT: FIRE_EVENT_SCHEMA, - WEBHOOK_TYPE_REGISTER_SENSOR: REGISTER_SENSOR_SCHEMA, - WEBHOOK_TYPE_RENDER_TEMPLATE: RENDER_TEMPLATE_SCHEMA, - WEBHOOK_TYPE_UPDATE_LOCATION: UPDATE_LOCATION_SCHEMA, - WEBHOOK_TYPE_UPDATE_REGISTRATION: UPDATE_REGISTRATION_SCHEMA, - WEBHOOK_TYPE_UPDATE_SENSOR_STATES: UPDATE_SENSOR_STATE_SCHEMA, -} diff --git a/homeassistant/components/mobile_app/device_tracker.py b/homeassistant/components/mobile_app/device_tracker.py index f58f80aa5fc..480bfee512f 100644 --- a/homeassistant/components/mobile_app/device_tracker.py +++ b/homeassistant/components/mobile_app/device_tracker.py @@ -1,6 +1,12 @@ """Device tracker platform that adds support for OwnTracks over MQTT.""" import logging +from homeassistant.components.device_tracker import ( + ATTR_BATTERY, + ATTR_GPS, + ATTR_GPS_ACCURACY, + ATTR_LOCATION_NAME, +) 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 @@ -9,13 +15,9 @@ from homeassistant.helpers.restore_state import RestoreEntity from .const import ( ATTR_ALTITUDE, - ATTR_BATTERY, ATTR_COURSE, ATTR_DEVICE_ID, ATTR_DEVICE_NAME, - ATTR_GPS, - ATTR_GPS_ACCURACY, - ATTR_LOCATION_NAME, ATTR_SPEED, ATTR_VERTICAL_ACCURACY, SIGNAL_LOCATION_UPDATE, diff --git a/homeassistant/components/mobile_app/helpers.py b/homeassistant/components/mobile_app/helpers.py index cad25f371dd..400ff31be89 100644 --- a/homeassistant/components/mobile_app/helpers.py +++ b/homeassistant/components/mobile_app/helpers.py @@ -111,7 +111,7 @@ def error_response( def supports_encryption() -> bool: """Test if we support encryption.""" try: - import nacl # noqa: F401 pylint: disable=unused-import + import nacl # noqa: F401 pylint: disable=unused-import, import-outside-toplevel return True except OSError: diff --git a/homeassistant/components/mobile_app/http_api.py b/homeassistant/components/mobile_app/http_api.py index ee69f15fb11..717413f889a 100644 --- a/homeassistant/components/mobile_app/http_api.py +++ b/homeassistant/components/mobile_app/http_api.py @@ -1,29 +1,34 @@ """Provides an HTTP API for mobile_app.""" +import secrets from typing import Dict import uuid from aiohttp.web import Request, Response from nacl.secret import SecretBox +import voluptuous as vol -from homeassistant.auth.util import generate_secret -from homeassistant.components.cloud import ( - CloudNotAvailable, - async_create_cloudhook, - async_remote_ui_url, -) 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.helpers import config_validation as cv from .const import ( + ATTR_APP_DATA, + ATTR_APP_ID, + ATTR_APP_NAME, + ATTR_APP_VERSION, ATTR_DEVICE_ID, + ATTR_DEVICE_NAME, + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_OS_NAME, + ATTR_OS_VERSION, ATTR_SUPPORTS_ENCRYPTION, CONF_CLOUDHOOK_URL, CONF_REMOTE_UI_URL, CONF_SECRET, CONF_USER_ID, DOMAIN, - REGISTRATION_SCHEMA, ) from .helpers import supports_encryption @@ -34,22 +39,37 @@ class RegistrationsView(HomeAssistantView): url = "/api/mobile_app/registrations" name = "api:mobile_app:register" - @RequestDataValidator(REGISTRATION_SCHEMA) + @RequestDataValidator( + { + vol.Optional(ATTR_APP_DATA, default={}): dict, + vol.Required(ATTR_APP_ID): cv.string, + vol.Required(ATTR_APP_NAME): cv.string, + vol.Required(ATTR_APP_VERSION): cv.string, + vol.Required(ATTR_DEVICE_NAME): cv.string, + vol.Required(ATTR_MANUFACTURER): cv.string, + vol.Required(ATTR_MODEL): cv.string, + vol.Required(ATTR_OS_NAME): cv.string, + vol.Optional(ATTR_OS_VERSION): cv.string, + vol.Required(ATTR_SUPPORTS_ENCRYPTION, default=False): cv.boolean, + } + ) async def post(self, request: Request, data: Dict) -> Response: """Handle the POST request for registration.""" hass = request.app["hass"] - webhook_id = generate_secret() + webhook_id = secrets.token_hex() if hass.components.cloud.async_active_subscription(): - data[CONF_CLOUDHOOK_URL] = await async_create_cloudhook(hass, webhook_id) + data[ + CONF_CLOUDHOOK_URL + ] = await hass.components.cloud.async_create_cloudhook(webhook_id) data[ATTR_DEVICE_ID] = str(uuid.uuid4()).replace("-", "") data[CONF_WEBHOOK_ID] = webhook_id if data[ATTR_SUPPORTS_ENCRYPTION] and supports_encryption(): - data[CONF_SECRET] = generate_secret(SecretBox.KEY_SIZE) + data[CONF_SECRET] = secrets.token_hex(SecretBox.KEY_SIZE) data[CONF_USER_ID] = request["hass_user"].id @@ -60,8 +80,8 @@ class RegistrationsView(HomeAssistantView): remote_ui_url = None try: - remote_ui_url = async_remote_ui_url(hass) - except 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 ab140b4148e..230a60fdf25 100644 --- a/homeassistant/components/mobile_app/manifest.json +++ b/homeassistant/components/mobile_app/manifest.json @@ -3,15 +3,8 @@ "name": "Home Assistant Mobile App Support", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/mobile_app", - "requirements": [ - "PyNaCl==1.3.0" - ], - "dependencies": [ - "cloud", - "http", - "webhook" - ], - "codeowners": [ - "@robbiet480" - ] + "requirements": ["PyNaCl==1.3.0"], + "dependencies": ["http", "webhook"], + "after_dependencies": ["cloud"], + "codeowners": ["@robbiet480"] } diff --git a/homeassistant/components/mobile_app/notify.py b/homeassistant/components/mobile_app/notify.py index 8ac34c9af1d..b51bf235cf0 100644 --- a/homeassistant/components/mobile_app/notify.py +++ b/homeassistant/components/mobile_app/notify.py @@ -139,9 +139,9 @@ class MobileAppNotificationService(BaseNotificationService): return fallback_error = result.get("errorMessage", "Unknown error") - fallback_message = ( - "Internal server error, " "please try again later: " "{}" - ).format(fallback_error) + fallback_message = "Internal server error, please try again later: {}".format( + fallback_error + ) message = result.get("message", fallback_message) if response.status == 429: _LOGGER.warning(message) diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index 66188500fd6..3a477d89925 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -1,11 +1,21 @@ """Webhook handlers for mobile_app.""" +from functools import wraps import logging from aiohttp.web import HTTPBadRequest, Request, Response import voluptuous as vol -from homeassistant.components.cloud import CloudNotAvailable, async_remote_ui_url +from homeassistant.components.binary_sensor import ( + DEVICE_CLASSES as BINARY_SENSOR_CLASSES, +) +from homeassistant.components.device_tracker import ( + ATTR_BATTERY, + ATTR_GPS, + ATTR_GPS_ACCURACY, + ATTR_LOCATION_NAME, +) from homeassistant.components.frontend import MANIFEST_JSON +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_DOMAIN, @@ -17,12 +27,17 @@ from homeassistant.const import ( ) from homeassistant.core import EventOrigin from homeassistant.exceptions import HomeAssistantError, ServiceNotFound, TemplateError -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.template import attach from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.util.decorator import Registry from .const import ( + ATTR_ALTITUDE, + ATTR_APP_DATA, + ATTR_APP_VERSION, + ATTR_COURSE, ATTR_DEVICE_ID, ATTR_DEVICE_NAME, ATTR_EVENT_DATA, @@ -30,11 +45,21 @@ from .const import ( ATTR_MANUFACTURER, ATTR_MODEL, ATTR_OS_VERSION, + ATTR_SENSOR_ATTRIBUTES, + ATTR_SENSOR_DEVICE_CLASS, + ATTR_SENSOR_ICON, + ATTR_SENSOR_NAME, + ATTR_SENSOR_STATE, ATTR_SENSOR_TYPE, + ATTR_SENSOR_TYPE_BINARY_SENSOR, + ATTR_SENSOR_TYPE_SENSOR, ATTR_SENSOR_UNIQUE_ID, + ATTR_SENSOR_UOM, + ATTR_SPEED, ATTR_SUPPORTS_ENCRYPTION, ATTR_TEMPLATE, ATTR_TEMPLATE_VARIABLES, + ATTR_VERTICAL_ACCURACY, ATTR_WEBHOOK_DATA, ATTR_WEBHOOK_ENCRYPTED, ATTR_WEBHOOK_ENCRYPTED_DATA, @@ -51,18 +76,6 @@ from .const import ( ERR_SENSOR_NOT_REGISTERED, SIGNAL_LOCATION_UPDATE, SIGNAL_SENSOR_UPDATE, - WEBHOOK_PAYLOAD_SCHEMA, - WEBHOOK_SCHEMAS, - WEBHOOK_TYPE_CALL_SERVICE, - WEBHOOK_TYPE_FIRE_EVENT, - WEBHOOK_TYPE_GET_CONFIG, - WEBHOOK_TYPE_GET_ZONES, - WEBHOOK_TYPE_REGISTER_SENSOR, - WEBHOOK_TYPE_RENDER_TEMPLATE, - WEBHOOK_TYPE_UPDATE_LOCATION, - WEBHOOK_TYPE_UPDATE_REGISTRATION, - WEBHOOK_TYPE_UPDATE_SENSOR_STATES, - WEBHOOK_TYPES, ) from .helpers import ( _decrypt_payload, @@ -77,6 +90,46 @@ from .helpers import ( _LOGGER = logging.getLogger(__name__) +WEBHOOK_COMMANDS = Registry() + +COMBINED_CLASSES = set(BINARY_SENSOR_CLASSES + SENSOR_CLASSES) +SENSOR_TYPES = [ATTR_SENSOR_TYPE_BINARY_SENSOR, ATTR_SENSOR_TYPE_SENSOR] + +WEBHOOK_PAYLOAD_SCHEMA = vol.Schema( + { + vol.Required(ATTR_WEBHOOK_TYPE): cv.string, + vol.Required(ATTR_WEBHOOK_DATA, default={}): vol.Any(dict, list), + vol.Optional(ATTR_WEBHOOK_ENCRYPTED, default=False): cv.boolean, + vol.Optional(ATTR_WEBHOOK_ENCRYPTED_DATA): cv.string, + } +) + + +def validate_schema(schema): + """Decorate a webhook function with a schema.""" + if isinstance(schema, dict): + schema = vol.Schema(schema) + + def wrapper(func): + """Wrap function so we validate schema.""" + + @wraps(func) + async def validate_and_run(hass, config_entry, data): + """Validate input and call handler.""" + try: + data = schema(data) + except vol.Invalid as ex: + err = vol.humanize.humanize_error(data, ex) + _LOGGER.error("Received invalid webhook payload: %s", err) + return empty_okay_response() + + return await func(hass, config_entry, data) + + return validate_and_run + + return wrapper + + async def handle_webhook( hass: HomeAssistantType, webhook_id: str, request: Request ) -> Response: @@ -84,12 +137,8 @@ async def handle_webhook( if webhook_id in hass.data[DOMAIN][DATA_DELETED_IDS]: return Response(status=410) - headers = {} - config_entry = hass.data[DOMAIN][DATA_CONFIG_ENTRIES][webhook_id] - registration = config_entry.data - try: req_data = await request.json() except ValueError: @@ -98,11 +147,11 @@ async def handle_webhook( if ( ATTR_WEBHOOK_ENCRYPTED not in req_data - and registration[ATTR_SUPPORTS_ENCRYPTION] + and config_entry.data[ATTR_SUPPORTS_ENCRYPTION] ): _LOGGER.warning( "Refusing to accept unencrypted webhook from %s", - registration[ATTR_DEVICE_NAME], + config_entry.data[ATTR_DEVICE_NAME], ) return error_response(ERR_ENCRYPTION_REQUIRED, "Encryption required") @@ -119,202 +168,286 @@ async def handle_webhook( if req_data[ATTR_WEBHOOK_ENCRYPTED]: enc_data = req_data[ATTR_WEBHOOK_ENCRYPTED_DATA] - webhook_payload = _decrypt_payload(registration[CONF_SECRET], enc_data) + webhook_payload = _decrypt_payload(config_entry.data[CONF_SECRET], enc_data) - if webhook_type not in WEBHOOK_TYPES: + if webhook_type not in WEBHOOK_COMMANDS: _LOGGER.error("Received invalid webhook type: %s", webhook_type) return empty_okay_response() - data = webhook_payload + _LOGGER.debug( + "Received webhook payload for type %s: %s", webhook_type, webhook_payload + ) - _LOGGER.debug("Received webhook payload for type %s: %s", webhook_type, data) + return await WEBHOOK_COMMANDS[webhook_type](hass, config_entry, webhook_payload) - if webhook_type in WEBHOOK_SCHEMAS: + +@WEBHOOK_COMMANDS.register("call_service") +@validate_schema( + { + vol.Required(ATTR_DOMAIN): cv.string, + vol.Required(ATTR_SERVICE): cv.string, + vol.Optional(ATTR_SERVICE_DATA, default={}): dict, + } +) +async def webhook_call_service(hass, config_entry, data): + """Handle a call service webhook.""" + try: + await hass.services.async_call( + data[ATTR_DOMAIN], + data[ATTR_SERVICE], + data[ATTR_SERVICE_DATA], + blocking=True, + context=registration_context(config_entry.data), + ) + except (vol.Invalid, ServiceNotFound, Exception) as ex: + _LOGGER.error( + "Error when calling service during mobile_app " + "webhook (device name: %s): %s", + config_entry.data[ATTR_DEVICE_NAME], + ex, + ) + raise HTTPBadRequest() + + return empty_okay_response() + + +@WEBHOOK_COMMANDS.register("fire_event") +@validate_schema( + { + vol.Required(ATTR_EVENT_TYPE): cv.string, + vol.Optional(ATTR_EVENT_DATA, default={}): dict, + } +) +async def webhook_fire_event(hass, config_entry, data): + """Handle a fire event webhook.""" + event_type = data[ATTR_EVENT_TYPE] + hass.bus.async_fire( + event_type, + data[ATTR_EVENT_DATA], + EventOrigin.remote, + context=registration_context(config_entry.data), + ) + return empty_okay_response() + + +@WEBHOOK_COMMANDS.register("render_template") +@validate_schema( + { + str: { + vol.Required(ATTR_TEMPLATE): cv.template, + vol.Optional(ATTR_TEMPLATE_VARIABLES, default={}): dict, + } + } +) +async def webhook_render_template(hass, config_entry, data): + """Handle a render template webhook.""" + resp = {} + for key, item in data.items(): try: - data = WEBHOOK_SCHEMAS[webhook_type](webhook_payload) - except vol.Invalid as ex: - err = vol.humanize.humanize_error(webhook_payload, ex) - _LOGGER.error("Received invalid webhook payload: %s", err) - return empty_okay_response(headers=headers) + tpl = item[ATTR_TEMPLATE] + attach(hass, tpl) + resp[key] = tpl.async_render(item.get(ATTR_TEMPLATE_VARIABLES)) + except TemplateError as ex: + resp[key] = {"error": str(ex)} - context = registration_context(registration) + return webhook_response(resp, registration=config_entry.data) - if webhook_type == WEBHOOK_TYPE_CALL_SERVICE: - try: - await hass.services.async_call( - data[ATTR_DOMAIN], - data[ATTR_SERVICE], - data[ATTR_SERVICE_DATA], - blocking=True, - context=context, + +@WEBHOOK_COMMANDS.register("update_location") +@validate_schema( + { + vol.Optional(ATTR_LOCATION_NAME): cv.string, + vol.Required(ATTR_GPS): cv.gps, + vol.Required(ATTR_GPS_ACCURACY): cv.positive_int, + vol.Optional(ATTR_BATTERY): cv.positive_int, + vol.Optional(ATTR_SPEED): cv.positive_int, + vol.Optional(ATTR_ALTITUDE): vol.Coerce(float), + vol.Optional(ATTR_COURSE): cv.positive_int, + vol.Optional(ATTR_VERTICAL_ACCURACY): cv.positive_int, + } +) +async def webhook_update_location(hass, config_entry, data): + """Handle an update location webhook.""" + hass.helpers.dispatcher.async_dispatcher_send( + SIGNAL_LOCATION_UPDATE.format(config_entry.entry_id), data + ) + return empty_okay_response() + + +@WEBHOOK_COMMANDS.register("update_registration") +@validate_schema( + { + vol.Optional(ATTR_APP_DATA, default={}): dict, + vol.Required(ATTR_APP_VERSION): cv.string, + vol.Required(ATTR_DEVICE_NAME): cv.string, + vol.Required(ATTR_MANUFACTURER): cv.string, + vol.Required(ATTR_MODEL): cv.string, + vol.Optional(ATTR_OS_VERSION): cv.string, + } +) +async def webhook_update_registration(hass, config_entry, data): + """Handle an update registration webhook.""" + new_registration = {**config_entry.data, **data} + + device_registry = await dr.async_get_registry(hass) + + device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={(DOMAIN, config_entry.data[ATTR_DEVICE_ID])}, + manufacturer=new_registration[ATTR_MANUFACTURER], + model=new_registration[ATTR_MODEL], + name=new_registration[ATTR_DEVICE_NAME], + sw_version=new_registration[ATTR_OS_VERSION], + ) + + hass.config_entries.async_update_entry(config_entry, data=new_registration) + + return webhook_response( + safe_registration(new_registration), registration=new_registration, + ) + + +@WEBHOOK_COMMANDS.register("register_sensor") +@validate_schema( + { + vol.Optional(ATTR_SENSOR_ATTRIBUTES, default={}): dict, + vol.Optional(ATTR_SENSOR_DEVICE_CLASS): vol.All( + vol.Lower, vol.In(COMBINED_CLASSES) + ), + vol.Required(ATTR_SENSOR_NAME): cv.string, + vol.Required(ATTR_SENSOR_TYPE): vol.In(SENSOR_TYPES), + vol.Required(ATTR_SENSOR_UNIQUE_ID): cv.string, + vol.Optional(ATTR_SENSOR_UOM): cv.string, + vol.Required(ATTR_SENSOR_STATE): vol.Any(bool, str, int, float), + vol.Optional(ATTR_SENSOR_ICON, default="mdi:cellphone"): cv.icon, + } +) +async def webhook_register_sensor(hass, config_entry, data): + """Handle a register sensor webhook.""" + entity_type = data[ATTR_SENSOR_TYPE] + + unique_id = data[ATTR_SENSOR_UNIQUE_ID] + + unique_store_key = f"{config_entry.data[CONF_WEBHOOK_ID]}_{unique_id}" + + if unique_store_key in hass.data[DOMAIN][entity_type]: + _LOGGER.error("Refusing to re-register existing sensor %s!", unique_id) + return error_response( + ERR_SENSOR_DUPLICATE_UNIQUE_ID, + f"{entity_type} {unique_id} already exists!", + status=409, + ) + + data[CONF_WEBHOOK_ID] = config_entry.data[CONF_WEBHOOK_ID] + + hass.data[DOMAIN][entity_type][unique_store_key] = data + + try: + await hass.data[DOMAIN][DATA_STORE].async_save(savable_state(hass)) + except HomeAssistantError as ex: + _LOGGER.error("Error registering sensor: %s", ex) + return empty_okay_response() + + register_signal = "{}_{}_register".format(DOMAIN, data[ATTR_SENSOR_TYPE]) + async_dispatcher_send(hass, register_signal, data) + + return webhook_response( + {"success": True}, registration=config_entry.data, status=HTTP_CREATED, + ) + + +@WEBHOOK_COMMANDS.register("update_sensor_states") +@validate_schema( + vol.All( + cv.ensure_list, + [ + vol.Schema( + { + vol.Optional(ATTR_SENSOR_ATTRIBUTES, default={}): dict, + vol.Optional(ATTR_SENSOR_ICON, default="mdi:cellphone"): cv.icon, + vol.Required(ATTR_SENSOR_STATE): vol.Any(bool, str, int, float), + vol.Required(ATTR_SENSOR_TYPE): vol.In(SENSOR_TYPES), + vol.Required(ATTR_SENSOR_UNIQUE_ID): cv.string, + } ) - # noqa: E722 pylint: disable=broad-except - except (vol.Invalid, ServiceNotFound, Exception) as ex: + ], + ) +) +async def webhook_update_sensor_states(hass, config_entry, data): + """Handle an update sensor states webhook.""" + resp = {} + for sensor in data: + entity_type = sensor[ATTR_SENSOR_TYPE] + + unique_id = sensor[ATTR_SENSOR_UNIQUE_ID] + + unique_store_key = f"{config_entry.data[CONF_WEBHOOK_ID]}_{unique_id}" + + if unique_store_key not in hass.data[DOMAIN][entity_type]: _LOGGER.error( - "Error when calling service during mobile_app " - "webhook (device name: %s): %s", - registration[ATTR_DEVICE_NAME], - ex, + "Refusing to update non-registered sensor: %s", unique_store_key ) - raise HTTPBadRequest() + err_msg = f"{entity_type} {unique_id} is not registered" + resp[unique_id] = { + "success": False, + "error": {"code": ERR_SENSOR_NOT_REGISTERED, "message": err_msg}, + } + continue - return empty_okay_response(headers=headers) + entry = hass.data[DOMAIN][entity_type][unique_store_key] - if webhook_type == WEBHOOK_TYPE_FIRE_EVENT: - event_type = data[ATTR_EVENT_TYPE] - hass.bus.async_fire( - event_type, data[ATTR_EVENT_DATA], EventOrigin.remote, context=context - ) - return empty_okay_response(headers=headers) + new_state = {**entry, **sensor} - if webhook_type == WEBHOOK_TYPE_RENDER_TEMPLATE: - resp = {} - for key, item in data.items(): - try: - tpl = item[ATTR_TEMPLATE] - attach(hass, tpl) - resp[key] = tpl.async_render(item.get(ATTR_TEMPLATE_VARIABLES)) - # noqa: E722 pylint: disable=broad-except - except TemplateError as ex: - resp[key] = {"error": str(ex)} + hass.data[DOMAIN][entity_type][unique_store_key] = new_state - return webhook_response(resp, registration=registration, headers=headers) - - if webhook_type == WEBHOOK_TYPE_UPDATE_LOCATION: - hass.helpers.dispatcher.async_dispatcher_send( - SIGNAL_LOCATION_UPDATE.format(config_entry.entry_id), data - ) - return empty_okay_response(headers=headers) - - if webhook_type == WEBHOOK_TYPE_UPDATE_REGISTRATION: - new_registration = {**registration, **data} - - device_registry = await dr.async_get_registry(hass) - - device_registry.async_get_or_create( - config_entry_id=config_entry.entry_id, - identifiers={ - (ATTR_DEVICE_ID, registration[ATTR_DEVICE_ID]), - (CONF_WEBHOOK_ID, registration[CONF_WEBHOOK_ID]), - }, - manufacturer=new_registration[ATTR_MANUFACTURER], - model=new_registration[ATTR_MODEL], - name=new_registration[ATTR_DEVICE_NAME], - sw_version=new_registration[ATTR_OS_VERSION], - ) - - hass.config_entries.async_update_entry(config_entry, data=new_registration) - - return webhook_response( - safe_registration(new_registration), - registration=registration, - headers=headers, - ) - - if webhook_type == WEBHOOK_TYPE_REGISTER_SENSOR: - entity_type = data[ATTR_SENSOR_TYPE] - - unique_id = data[ATTR_SENSOR_UNIQUE_ID] - - unique_store_key = f"{webhook_id}_{unique_id}" - - if unique_store_key in hass.data[DOMAIN][entity_type]: - _LOGGER.error("Refusing to re-register existing sensor %s!", unique_id) - return error_response( - ERR_SENSOR_DUPLICATE_UNIQUE_ID, - f"{entity_type} {unique_id} already exists!", - status=409, - ) - - data[CONF_WEBHOOK_ID] = webhook_id - - hass.data[DOMAIN][entity_type][unique_store_key] = data + safe = savable_state(hass) try: - await hass.data[DOMAIN][DATA_STORE].async_save(savable_state(hass)) + await hass.data[DOMAIN][DATA_STORE].async_save(safe) except HomeAssistantError as ex: - _LOGGER.error("Error registering sensor: %s", ex) + _LOGGER.error("Error updating mobile_app registration: %s", ex) return empty_okay_response() - register_signal = "{}_{}_register".format(DOMAIN, data[ATTR_SENSOR_TYPE]) - async_dispatcher_send(hass, register_signal, data) + async_dispatcher_send(hass, SIGNAL_SENSOR_UPDATE, new_state) - return webhook_response( - {"success": True}, - registration=registration, - status=HTTP_CREATED, - headers=headers, - ) + resp[unique_id] = {"success": True} - if webhook_type == WEBHOOK_TYPE_UPDATE_SENSOR_STATES: - resp = {} - for sensor in data: - entity_type = sensor[ATTR_SENSOR_TYPE] + return webhook_response(resp, registration=config_entry.data) - unique_id = sensor[ATTR_SENSOR_UNIQUE_ID] - unique_store_key = f"{webhook_id}_{unique_id}" +@WEBHOOK_COMMANDS.register("get_zones") +async def webhook_get_zones(hass, config_entry, data): + """Handle a get zones webhook.""" + zones = [ + hass.states.get(entity_id) + for entity_id in sorted(hass.states.async_entity_ids(ZONE_DOMAIN)) + ] + return webhook_response(zones, registration=config_entry.data) - if unique_store_key not in hass.data[DOMAIN][entity_type]: - _LOGGER.error( - "Refusing to update non-registered sensor: %s", unique_store_key - ) - err_msg = f"{entity_type} {unique_id} is not registered" - resp[unique_id] = { - "success": False, - "error": {"code": ERR_SENSOR_NOT_REGISTERED, "message": err_msg}, - } - continue - entry = hass.data[DOMAIN][entity_type][unique_store_key] +@WEBHOOK_COMMANDS.register("get_config") +async def webhook_get_config(hass, config_entry, data): + """Handle a get config webhook.""" + hass_config = hass.config.as_dict() - new_state = {**entry, **sensor} + resp = { + "latitude": hass_config["latitude"], + "longitude": hass_config["longitude"], + "elevation": hass_config["elevation"], + "unit_system": hass_config["unit_system"], + "location_name": hass_config["location_name"], + "time_zone": hass_config["time_zone"], + "components": hass_config["components"], + "version": hass_config["version"], + "theme_color": MANIFEST_JSON["theme_color"], + } - hass.data[DOMAIN][entity_type][unique_store_key] = new_state + if CONF_CLOUDHOOK_URL in config_entry.data: + resp[CONF_CLOUDHOOK_URL] = config_entry.data[CONF_CLOUDHOOK_URL] - safe = savable_state(hass) + try: + resp[CONF_REMOTE_UI_URL] = hass.components.cloud.async_remote_ui_url() + except hass.components.cloud.CloudNotAvailable: + pass - try: - await hass.data[DOMAIN][DATA_STORE].async_save(safe) - except HomeAssistantError as ex: - _LOGGER.error("Error updating mobile_app registration: %s", ex) - return empty_okay_response() - - async_dispatcher_send(hass, SIGNAL_SENSOR_UPDATE, new_state) - - resp[unique_id] = {"success": True} - - return webhook_response(resp, registration=registration, headers=headers) - - if webhook_type == WEBHOOK_TYPE_GET_ZONES: - zones = ( - hass.states.get(entity_id) - for entity_id in sorted(hass.states.async_entity_ids(ZONE_DOMAIN)) - ) - return webhook_response(list(zones), registration=registration, headers=headers) - - if webhook_type == WEBHOOK_TYPE_GET_CONFIG: - - hass_config = hass.config.as_dict() - - resp = { - "latitude": hass_config["latitude"], - "longitude": hass_config["longitude"], - "elevation": hass_config["elevation"], - "unit_system": hass_config["unit_system"], - "location_name": hass_config["location_name"], - "time_zone": hass_config["time_zone"], - "components": hass_config["components"], - "version": hass_config["version"], - "theme_color": MANIFEST_JSON["theme_color"], - } - - if CONF_CLOUDHOOK_URL in registration: - resp[CONF_CLOUDHOOK_URL] = registration[CONF_CLOUDHOOK_URL] - - try: - resp[CONF_REMOTE_UI_URL] = async_remote_ui_url(hass) - except CloudNotAvailable: - pass - - return webhook_response(resp, registration=registration, headers=headers) + return webhook_response(resp, registration=config_entry.data) diff --git a/homeassistant/components/mobile_app/websocket_api.py b/homeassistant/components/mobile_app/websocket_api.py index 813d0a9cf89..a18e5247bfa 100644 --- a/homeassistant/components/mobile_app/websocket_api.py +++ b/homeassistant/components/mobile_app/websocket_api.py @@ -1,7 +1,6 @@ """Websocket API for mobile_app.""" import voluptuous as vol -from homeassistant.components.cloud import async_delete_cloudhook from homeassistant.components.websocket_api import ( ActiveConnection, async_register_command, @@ -116,7 +115,7 @@ async def websocket_delete_registration( except HomeAssistantError: return error_message(msg["id"], "internal_error", "Error deleting registration") - if CONF_CLOUDHOOK_URL in registration and "cloud" in hass.config.components: - await async_delete_cloudhook(hass, webhook_id) + if CONF_CLOUDHOOK_URL in registration: + await hass.components.cloud.async_delete_cloudhook(webhook_id) connection.send_message(result_message(msg["id"], "ok")) diff --git a/homeassistant/components/mochad/__init__.py b/homeassistant/components/mochad/__init__.py index 77426e8ae2c..683681b50a0 100644 --- a/homeassistant/components/mochad/__init__.py +++ b/homeassistant/components/mochad/__init__.py @@ -2,11 +2,16 @@ import logging import threading +from pymochad import controller, exceptions import voluptuous as vol +from homeassistant.const import ( + CONF_HOST, + CONF_PORT, + EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP, +) import homeassistant.helpers.config_validation as cv -from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP -from homeassistant.const import CONF_HOST, CONF_PORT _LOGGER = logging.getLogger(__name__) @@ -37,8 +42,6 @@ def setup(hass, config): host = conf.get(CONF_HOST) port = conf.get(CONF_PORT) - from pymochad import exceptions - global CONTROLLER try: CONTROLLER = MochadCtrl(host, port) @@ -68,8 +71,6 @@ class MochadCtrl: self._host = host self._port = port - from pymochad import controller - self.ctrl = controller.PyMochad(server=self._host, port=self._port) @property diff --git a/homeassistant/components/mochad/light.py b/homeassistant/components/mochad/light.py index 899908c34bd..871caadd95c 100644 --- a/homeassistant/components/mochad/light.py +++ b/homeassistant/components/mochad/light.py @@ -1,30 +1,32 @@ """Support for X10 dimmer over Mochad.""" import logging +from pymochad import device import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, + PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, Light, - PLATFORM_SCHEMA, ) -from homeassistant.components import mochad -from homeassistant.const import CONF_NAME, CONF_PLATFORM, CONF_DEVICES, CONF_ADDRESS +from homeassistant.const import CONF_ADDRESS, CONF_DEVICES, CONF_NAME, CONF_PLATFORM from homeassistant.helpers import config_validation as cv +from . import CONF_COMM_TYPE, CONTROLLER, DOMAIN, REQ_LOCK + _LOGGER = logging.getLogger(__name__) CONF_BRIGHTNESS_LEVELS = "brightness_levels" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { - vol.Required(CONF_PLATFORM): mochad.DOMAIN, + vol.Required(CONF_PLATFORM): DOMAIN, CONF_DEVICES: [ { vol.Optional(CONF_NAME): cv.string, vol.Required(CONF_ADDRESS): cv.x10_address, - vol.Optional(mochad.CONF_COMM_TYPE): cv.string, + vol.Optional(CONF_COMM_TYPE): cv.string, vol.Optional(CONF_BRIGHTNESS_LEVELS, default=32): vol.All( vol.Coerce(int), vol.In([32, 64, 256]) ), @@ -37,7 +39,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up X10 dimmers over a mochad controller.""" devs = config.get(CONF_DEVICES) - add_entities([MochadLight(hass, mochad.CONTROLLER.ctrl, dev) for dev in devs]) + add_entities([MochadLight(hass, CONTROLLER.ctrl, dev) for dev in devs]) return True @@ -46,12 +48,11 @@ class MochadLight(Light): def __init__(self, hass, ctrl, dev): """Initialize a Mochad Light Device.""" - from pymochad import device self._controller = ctrl self._address = dev[CONF_ADDRESS] self._name = dev.get(CONF_NAME, f"x10_light_dev_{self._address}") - self._comm_type = dev.get(mochad.CONF_COMM_TYPE, "pl") + self._comm_type = dev.get(CONF_COMM_TYPE, "pl") self.light = device.Device(ctrl, self._address, comm_type=self._comm_type) self._brightness = 0 self._state = self._get_device_status() @@ -64,7 +65,7 @@ class MochadLight(Light): def _get_device_status(self): """Get the status of the light from mochad.""" - with mochad.REQ_LOCK: + with REQ_LOCK: status = self.light.get_status().rstrip() return status == "on" @@ -106,7 +107,7 @@ class MochadLight(Light): def turn_on(self, **kwargs): """Send the command to turn the light on.""" brightness = kwargs.get(ATTR_BRIGHTNESS, 255) - with mochad.REQ_LOCK: + with REQ_LOCK: if self._brightness_levels > 32: out_brightness = self._calculate_brightness_value(brightness) self.light.send_cmd(f"xdim {out_brightness}") @@ -124,7 +125,7 @@ class MochadLight(Light): def turn_off(self, **kwargs): """Send the command to turn the light on.""" - with mochad.REQ_LOCK: + with REQ_LOCK: self.light.send_cmd("off") self._controller.read_data() # There is no persistence for X10 modules so we need to prepare diff --git a/homeassistant/components/mochad/switch.py b/homeassistant/components/mochad/switch.py index 0713d50eb4b..14fb601f919 100644 --- a/homeassistant/components/mochad/switch.py +++ b/homeassistant/components/mochad/switch.py @@ -1,24 +1,27 @@ """Support for X10 switch over Mochad.""" import logging +from pymochad import device +from pymochad.exceptions import MochadException import voluptuous as vol -from homeassistant.components import mochad from homeassistant.components.switch import SwitchDevice -from homeassistant.const import CONF_NAME, CONF_DEVICES, CONF_PLATFORM, CONF_ADDRESS +from homeassistant.const import CONF_ADDRESS, CONF_DEVICES, CONF_NAME, CONF_PLATFORM from homeassistant.helpers import config_validation as cv +from . import CONF_COMM_TYPE, CONTROLLER, DOMAIN, REQ_LOCK + _LOGGER = logging.getLogger(__name__) PLATFORM_SCHEMA = vol.Schema( { - vol.Required(CONF_PLATFORM): mochad.DOMAIN, + vol.Required(CONF_PLATFORM): DOMAIN, CONF_DEVICES: [ { vol.Optional(CONF_NAME): cv.string, vol.Required(CONF_ADDRESS): cv.x10_address, - vol.Optional(mochad.CONF_COMM_TYPE): cv.string, + vol.Optional(CONF_COMM_TYPE): cv.string, } ], } @@ -28,7 +31,7 @@ PLATFORM_SCHEMA = vol.Schema( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up X10 switches over a mochad controller.""" devs = config.get(CONF_DEVICES) - add_entities([MochadSwitch(hass, mochad.CONTROLLER.ctrl, dev) for dev in devs]) + add_entities([MochadSwitch(hass, CONTROLLER.ctrl, dev) for dev in devs]) return True @@ -37,12 +40,11 @@ class MochadSwitch(SwitchDevice): def __init__(self, hass, ctrl, dev): """Initialize a Mochad Switch Device.""" - from pymochad import device self._controller = ctrl self._address = dev[CONF_ADDRESS] self._name = dev.get(CONF_NAME, "x10_switch_dev_%s" % self._address) - self._comm_type = dev.get(mochad.CONF_COMM_TYPE, "pl") + self._comm_type = dev.get(CONF_COMM_TYPE, "pl") self.switch = device.Device(ctrl, self._address, comm_type=self._comm_type) # Init with false to avoid locking HA for long on CM19A (goes from rf # to pl via TM751, but not other way around) @@ -58,10 +60,9 @@ class MochadSwitch(SwitchDevice): def turn_on(self, **kwargs): """Turn the switch on.""" - from pymochad.exceptions import MochadException _LOGGER.debug("Reconnect %s:%s", self._controller.server, self._controller.port) - with mochad.REQ_LOCK: + with REQ_LOCK: try: # Recycle socket on new command to recover mochad connection self._controller.reconnect() @@ -75,10 +76,9 @@ class MochadSwitch(SwitchDevice): def turn_off(self, **kwargs): """Turn the switch off.""" - from pymochad.exceptions import MochadException _LOGGER.debug("Reconnect %s:%s", self._controller.server, self._controller.port) - with mochad.REQ_LOCK: + with REQ_LOCK: try: # Recycle socket on new command to recover mochad connection self._controller.reconnect() @@ -92,7 +92,7 @@ class MochadSwitch(SwitchDevice): def _get_device_status(self): """Get the status of the switch from mochad.""" - with mochad.REQ_LOCK: + with REQ_LOCK: status = self.switch.get_status().rstrip() return status == "on" diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index a6e901af749..823703ac4c9 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -2,6 +2,8 @@ import logging import threading +from pymodbus.client.sync import ModbusSerialClient, ModbusTcpClient, ModbusUdpClient +from pymodbus.transaction import ModbusRtuFramer import voluptuous as vol from homeassistant.const import ( @@ -91,9 +93,7 @@ def setup_client(client_config): client_type = client_config[CONF_TYPE] if client_type == "serial": - from pymodbus.client.sync import ModbusSerialClient as ModbusClient - - return ModbusClient( + return ModbusSerialClient( method=client_config[CONF_METHOD], port=client_config[CONF_PORT], baudrate=client_config[CONF_BAUDRATE], @@ -103,27 +103,20 @@ def setup_client(client_config): timeout=client_config[CONF_TIMEOUT], ) if client_type == "rtuovertcp": - from pymodbus.client.sync import ModbusTcpClient as ModbusClient - from pymodbus.transaction import ModbusRtuFramer - - return ModbusClient( + return ModbusTcpClient( host=client_config[CONF_HOST], port=client_config[CONF_PORT], framer=ModbusRtuFramer, timeout=client_config[CONF_TIMEOUT], ) if client_type == "tcp": - from pymodbus.client.sync import ModbusTcpClient as ModbusClient - - return ModbusClient( + return ModbusTcpClient( host=client_config[CONF_HOST], port=client_config[CONF_PORT], timeout=client_config[CONF_TIMEOUT], ) if client_type == "udp": - from pymodbus.client.sync import ModbusUdpClient as ModbusClient - - return ModbusClient( + return ModbusUdpClient( host=client_config[CONF_HOST], port=client_config[CONF_PORT], timeout=client_config[CONF_TIMEOUT], diff --git a/homeassistant/components/modbus/binary_sensor.py b/homeassistant/components/modbus/binary_sensor.py index cbefd1271d0..9a431d24b0c 100644 --- a/homeassistant/components/modbus/binary_sensor.py +++ b/homeassistant/components/modbus/binary_sensor.py @@ -1,11 +1,15 @@ """Support for Modbus Coil sensors.""" import logging +from typing import Optional import voluptuous as vol -from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, CONF_SLAVE +from homeassistant.components.binary_sensor import ( + DEVICE_CLASSES_SCHEMA, + PLATFORM_SCHEMA, + BinarySensorDevice, +) +from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME, CONF_SLAVE from homeassistant.helpers import config_validation as cv from . import CONF_HUB, DEFAULT_HUB, DOMAIN as MODBUS_DOMAIN @@ -21,6 +25,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_COIL): cv.positive_int, vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, vol.Optional(CONF_SLAVE): cv.positive_int, } @@ -36,7 +41,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): hub = hass.data[MODBUS_DOMAIN][coil.get(CONF_HUB)] sensors.append( ModbusCoilSensor( - hub, coil.get(CONF_NAME), coil.get(CONF_SLAVE), coil.get(CONF_COIL) + hub, + coil.get(CONF_NAME), + coil.get(CONF_SLAVE), + coil.get(CONF_COIL), + coil.get(CONF_DEVICE_CLASS), ) ) @@ -46,12 +55,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class ModbusCoilSensor(BinarySensorDevice): """Modbus coil sensor.""" - def __init__(self, hub, name, slave, coil): + def __init__(self, hub, name, slave, coil, device_class): """Initialize the Modbus coil sensor.""" self._hub = hub self._name = name self._slave = int(slave) if slave else None self._coil = int(coil) + self._device_class = device_class self._value = None @property @@ -64,6 +74,11 @@ class ModbusCoilSensor(BinarySensorDevice): """Return the state of the sensor.""" return self._value + @property + def device_class(self) -> Optional[str]: + """Return the device class of the sensor.""" + return self._device_class + def update(self): """Update the state of the sensor.""" result = self._hub.read_coils(self._slave, self._coil, 1) diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index 64b45b03c95..99ea686543d 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -6,10 +6,16 @@ import voluptuous as vol from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice from homeassistant.components.climate.const import ( + HVAC_MODE_AUTO, SUPPORT_TARGET_TEMPERATURE, - HVAC_MODE_HEAT, ) -from homeassistant.const import ATTR_TEMPERATURE, CONF_NAME, CONF_SLAVE +from homeassistant.const import ( + ATTR_TEMPERATURE, + CONF_NAME, + CONF_SLAVE, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) import homeassistant.helpers.config_validation as cv from . import CONF_HUB, DEFAULT_HUB, DOMAIN as MODBUS_DOMAIN @@ -21,12 +27,17 @@ CONF_CURRENT_TEMP = "current_temp_register" CONF_DATA_TYPE = "data_type" CONF_COUNT = "data_count" CONF_PRECISION = "precision" - +CONF_SCALE = "scale" +CONF_OFFSET = "offset" +CONF_UNIT = "temperature_unit" DATA_TYPE_INT = "int" DATA_TYPE_UINT = "uint" DATA_TYPE_FLOAT = "float" +CONF_MAX_TEMP = "max_temp" +CONF_MIN_TEMP = "min_temp" +CONF_STEP = "temp_step" SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE -HVAC_MODES = [HVAC_MODE_HEAT] +HVAC_MODES = [HVAC_MODE_AUTO] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -40,6 +51,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ), vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, vol.Optional(CONF_PRECISION, default=1): cv.positive_int, + vol.Optional(CONF_SCALE, default=1): vol.Coerce(float), + vol.Optional(CONF_OFFSET, default=0): vol.Coerce(float), + vol.Optional(CONF_MAX_TEMP, default=5): cv.positive_int, + vol.Optional(CONF_MIN_TEMP, default=35): cv.positive_int, + vol.Optional(CONF_STEP, default=0.5): vol.Coerce(float), + vol.Optional(CONF_UNIT, default="C"): cv.string, } ) @@ -53,6 +70,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): data_type = config.get(CONF_DATA_TYPE) count = config.get(CONF_COUNT) precision = config.get(CONF_PRECISION) + scale = config.get(CONF_SCALE) + offset = config.get(CONF_OFFSET) + unit = config.get(CONF_UNIT) + max_temp = config.get(CONF_MAX_TEMP) + min_temp = config.get(CONF_MIN_TEMP) + temp_step = config.get(CONF_STEP) hub_name = config.get(CONF_HUB) hub = hass.data[MODBUS_DOMAIN][hub_name] @@ -67,6 +90,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): data_type, count, precision, + scale, + offset, + unit, + max_temp, + min_temp, + temp_step, ) ], True, @@ -86,6 +115,12 @@ class ModbusThermostat(ClimateDevice): data_type, count, precision, + scale, + offset, + unit, + max_temp, + min_temp, + temp_step, ): """Initialize the unit.""" self._hub = hub @@ -98,6 +133,12 @@ class ModbusThermostat(ClimateDevice): self._data_type = data_type self._count = int(count) self._precision = precision + self._scale = scale + self._offset = offset + self._unit = unit + self._max_temp = max_temp + self._min_temp = min_temp + self._temp_step = temp_step self._structure = ">f" data_types = { @@ -123,7 +164,7 @@ class ModbusThermostat(ClimateDevice): @property def hvac_mode(self): """Return the current HVAC mode.""" - return HVAC_MODE_HEAT + return HVAC_MODE_AUTO @property def hvac_modes(self): @@ -145,9 +186,31 @@ class ModbusThermostat(ClimateDevice): """Return the target temperature.""" return self._target_temperature + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return TEMP_FAHRENHEIT if self._unit == "F" else TEMP_CELSIUS + + @property + def min_temp(self): + """Return the minimum temperature.""" + return self._min_temp + + @property + def max_temp(self): + """Return the maximum temperature.""" + return self._max_temp + + @property + def target_temperature_step(self): + """Return the supported step of target temperature.""" + return self._temp_step + def set_temperature(self, **kwargs): """Set new target temperature.""" - target_temperature = kwargs.get(ATTR_TEMPERATURE) + 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) @@ -170,7 +233,10 @@ class ModbusThermostat(ClimateDevice): [x.to_bytes(2, byteorder="big") for x in result.registers] ) val = struct.unpack(self._structure, byte_string)[0] - register_value = format(val, f".{self._precision}f") + register_value = format( + (self._scale * val) + self._offset, f".{self._precision}f" + ) + register_value = float(register_value) return register_value def write_register(self, register, value): diff --git a/homeassistant/components/modbus/manifest.json b/homeassistant/components/modbus/manifest.json index 8d271d5a95f..356a9f6a9c0 100644 --- a/homeassistant/components/modbus/manifest.json +++ b/homeassistant/components/modbus/manifest.json @@ -6,5 +6,7 @@ "pymodbus==1.5.2" ], "dependencies": [], - "codeowners": [] + "codeowners": [ + "@adamchengtkc" + ] } diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index 1a5c71812d6..484382983ac 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -1,12 +1,13 @@ """Support for Modbus Register sensors.""" import logging import struct +from typing import Any, Optional, Union -from typing import Any, Union import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA from homeassistant.const import ( + CONF_DEVICE_CLASS, CONF_NAME, CONF_OFFSET, CONF_SLAVE, @@ -67,6 +68,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_DATA_TYPE, default=DATA_TYPE_INT): vol.In( [DATA_TYPE_INT, DATA_TYPE_UINT, DATA_TYPE_FLOAT, DATA_TYPE_CUSTOM] ), + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, vol.Optional(CONF_OFFSET, default=0): number, vol.Optional(CONF_PRECISION, default=0): cv.positive_int, @@ -100,7 +102,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) except KeyError: _LOGGER.error( - "Unable to detect data type for %s sensor, " "try a custom type", + "Unable to detect data type for %s sensor, try a custom type", register.get(CONF_NAME), ) continue @@ -117,7 +119,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if register.get(CONF_COUNT) * 2 != size: _LOGGER.error( - "Structure size (%d bytes) mismatch registers count " "(%d words)", + "Structure size (%d bytes) mismatch registers count (%d words)", size, register.get(CONF_COUNT), ) @@ -139,6 +141,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): register.get(CONF_OFFSET), structure, register.get(CONF_PRECISION), + register.get(CONF_DEVICE_CLASS), ) ) @@ -164,6 +167,7 @@ class ModbusRegisterSensor(RestoreEntity): offset, structure, precision, + device_class, ): """Initialize the modbus register sensor.""" self._hub = hub @@ -178,6 +182,7 @@ class ModbusRegisterSensor(RestoreEntity): self._offset = offset self._precision = precision self._structure = structure + self._device_class = device_class self._value = None async def async_added_to_hass(self): @@ -202,6 +207,11 @@ class ModbusRegisterSensor(RestoreEntity): """Return the unit of measurement.""" return self._unit_of_measurement + @property + def device_class(self) -> Optional[str]: + """Return the device class of the sensor.""" + return self._device_class + def update(self): """Update the state of the sensor.""" if self._register_type == REGISTER_TYPE_INPUT: diff --git a/homeassistant/components/modbus/services.yaml b/homeassistant/components/modbus/services.yaml index 8713257b47c..2158528814f 100644 --- a/homeassistant/components/modbus/services.yaml +++ b/homeassistant/components/modbus/services.yaml @@ -1,7 +1,7 @@ write_coil: description: Write to a modbus coil. fields: - address: {description: Address of the register to read., example: 0} + address: {description: Address of the register to write to., example: 0} state: {description: State to write., example: false} unit: {description: Address of the modbus unit., example: 21} write_register: diff --git a/homeassistant/components/modbus/switch.py b/homeassistant/components/modbus/switch.py index 43ef649f788..0ed33dedb57 100644 --- a/homeassistant/components/modbus/switch.py +++ b/homeassistant/components/modbus/switch.py @@ -3,7 +3,7 @@ import logging import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.switch import PLATFORM_SCHEMA from homeassistant.const import ( CONF_COMMAND_OFF, CONF_COMMAND_ON, @@ -236,7 +236,7 @@ class ModbusRegisterSwitch(ModbusCoilSwitch): self._is_on = False else: _LOGGER.error( - "Unexpected response from hub %s, slave %s " "register %s, got 0x%2x", + "Unexpected response from hub %s, slave %s register %s, got 0x%2x", self._hub.name, self._slave, self._verify_register, diff --git a/homeassistant/components/modem_callerid/sensor.py b/homeassistant/components/modem_callerid/sensor.py index 7acb345e27e..7ffda3e6124 100644 --- a/homeassistant/components/modem_callerid/sensor.py +++ b/homeassistant/components/modem_callerid/sensor.py @@ -1,15 +1,18 @@ """A sensor for incoming calls using a USB modem that supports caller ID.""" import logging + +from basicmodem.basicmodem import BasicModem as bm import voluptuous as vol -from homeassistant.const import ( - STATE_IDLE, - EVENT_HOMEASSISTANT_STOP, - CONF_NAME, - CONF_DEVICE, -) + from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.helpers.entity import Entity +from homeassistant.const import ( + CONF_DEVICE, + CONF_NAME, + EVENT_HOMEASSISTANT_STOP, + STATE_IDLE, +) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Modem CallerID" @@ -29,7 +32,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up modem caller ID sensor platform.""" - from basicmodem.basicmodem import BasicModem as bm name = config.get(CONF_NAME) port = config.get(CONF_DEVICE) diff --git a/homeassistant/components/mold_indicator/sensor.py b/homeassistant/components/mold_indicator/sensor.py index d0e5c7d51dd..0d6c6f55284 100644 --- a/homeassistant/components/mold_indicator/sensor.py +++ b/homeassistant/components/mold_indicator/sensor.py @@ -6,20 +6,19 @@ import voluptuous as vol from homeassistant import util from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.core import callback from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, + CONF_NAME, EVENT_HOMEASSISTANT_START, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT, - CONF_NAME, ) +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 -import homeassistant.helpers.config_validation as cv - _LOGGER = logging.getLogger(__name__) ATTR_CRITICAL_TEMP = "estimated_critical_temp" @@ -111,7 +110,7 @@ class MoldIndicator(Entity): def mold_indicator_sensors_state_listener(entity, old_state, new_state): """Handle for state changes for dependent sensors.""" _LOGGER.debug( - "Sensor state change for %s that had old state %s " "and new state %s", + "Sensor state change for %s that had old state %s and new state %s", entity, old_state, new_state, @@ -189,7 +188,7 @@ class MoldIndicator(Entity): # Return an error if the sensor change its state to Unknown. if state.state == STATE_UNKNOWN: _LOGGER.error( - "Unable to parse temperature sensor %s with state:" " %s", + "Unable to parse temperature sensor %s with state: %s", state.entity_id, state.state, ) @@ -200,7 +199,7 @@ class MoldIndicator(Entity): if temp is None: _LOGGER.error( - "Unable to parse temperature sensor %s with state:" " %s", + "Unable to parse temperature sensor %s with state: %s", state.entity_id, state.state, ) @@ -212,7 +211,7 @@ class MoldIndicator(Entity): if unit == TEMP_CELSIUS: return temp _LOGGER.error( - "Temp sensor %s has unsupported unit: %s (allowed: %s, " "%s)", + "Temp sensor %s has unsupported unit: %s (allowed: %s, %s)", state.entity_id, unit, TEMP_CELSIUS, @@ -307,7 +306,7 @@ class MoldIndicator(Entity): if None in (self._dewpoint, self._calib_factor) or self._calib_factor == 0: _LOGGER.debug( - "Invalid inputs - dewpoint: %s," " calibration-factor: %s", + "Invalid inputs - dewpoint: %s, calibration-factor: %s", self._dewpoint, self._calib_factor, ) diff --git a/homeassistant/components/monoprice/const.py b/homeassistant/components/monoprice/const.py new file mode 100644 index 00000000000..e8d813d2529 --- /dev/null +++ b/homeassistant/components/monoprice/const.py @@ -0,0 +1,5 @@ +"""Constants for the Monoprice 6-Zone Amplifier Media Player component.""" + +DOMAIN = "monoprice" +SERVICE_SNAPSHOT = "snapshot" +SERVICE_RESTORE = "restore" diff --git a/homeassistant/components/monoprice/media_player.py b/homeassistant/components/monoprice/media_player.py index 5f2af33e23a..20b2ecebcf4 100644 --- a/homeassistant/components/monoprice/media_player.py +++ b/homeassistant/components/monoprice/media_player.py @@ -1,11 +1,12 @@ """Support for interfacing with Monoprice 6 zone home audio controller.""" import logging +from pymonoprice import get_monoprice +from serial import SerialException import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( - DOMAIN, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, @@ -22,6 +23,8 @@ from homeassistant.const import ( ) import homeassistant.helpers.config_validation as cv +from .const import DOMAIN, SERVICE_RESTORE, SERVICE_SNAPSHOT + _LOGGER = logging.getLogger(__name__) SUPPORT_MONOPRICE = ( @@ -42,9 +45,6 @@ CONF_SOURCES = "sources" DATA_MONOPRICE = "monoprice" -SERVICE_SNAPSHOT = "snapshot" -SERVICE_RESTORE = "restore" - # Valid zone ids: 11-16 or 21-26 or 31-36 ZONE_IDS = vol.All( vol.Coerce(int), @@ -71,9 +71,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Monoprice 6-zone amplifier platform.""" port = config.get(CONF_PORT) - from serial import SerialException - from pymonoprice import get_monoprice - try: monoprice = get_monoprice(port) except SerialException: diff --git a/homeassistant/components/monoprice/services.yaml b/homeassistant/components/monoprice/services.yaml index e69de29bb2d..420270e10ac 100644 --- a/homeassistant/components/monoprice/services.yaml +++ b/homeassistant/components/monoprice/services.yaml @@ -0,0 +1,13 @@ +snapshot: + description: Take a snapshot of the media player zone. + fields: + entity_id: + description: Name(s) of entities that will be snapshot. Platform dependent. + example: 'media_player.living_room' + +restore: + description: Restore a snapshot of the media player zone. + fields: + entity_id: + description: Name(s) of entities that will be restored. Platform dependent. + example: 'media_player.living_room' diff --git a/homeassistant/components/moon/sensor.py b/homeassistant/components/moon/sensor.py index 39247b096cc..3a7dd9e2084 100644 --- a/homeassistant/components/moon/sensor.py +++ b/homeassistant/components/moon/sensor.py @@ -1,19 +1,38 @@ """Support for tracking the moon phases.""" import logging +from astral import Astral import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_NAME -import homeassistant.util.dt as dt_util -from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Moon" -ICON = "mdi:brightness-3" +STATE_FIRST_QUARTER = "first_quarter" +STATE_FULL_MOON = "full_moon" +STATE_LAST_QUARTER = "last_quarter" +STATE_NEW_MOON = "new_moon" +STATE_WANING_CRESCENT = "waning_crescent" +STATE_WANING_GIBBOUS = "waning_gibbous" +STATE_WAXING_GIBBOUS = "waxing_gibbous" +STATE_WAXING_CRESCENT = "waxing_crescent" + +MOON_ICONS = { + STATE_FIRST_QUARTER: "mdi:moon-first-quarter", + STATE_FULL_MOON: "mdi:moon-full", + STATE_LAST_QUARTER: "mdi:moon-last-quarter", + STATE_NEW_MOON: "mdi:moon-new", + STATE_WANING_CRESCENT: "mdi:moon-waning-crescent", + STATE_WANING_GIBBOUS: "mdi:moon-waning-gibbous", + STATE_WAXING_CRESCENT: "mdi:moon-waxing-crescent", + STATE_WAXING_GIBBOUS: "mdi:moon-waxing-gibbous", +} PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( {vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string} @@ -31,7 +50,7 @@ class MoonSensor(Entity): """Representation of a Moon sensor.""" def __init__(self, name): - """Initialize the sensor.""" + """Initialize the moon sensor.""" self._name = name self._state = None @@ -44,29 +63,27 @@ class MoonSensor(Entity): def state(self): """Return the state of the device.""" if self._state == 0: - return "new_moon" + return STATE_NEW_MOON if self._state < 7: - return "waxing_crescent" + return STATE_WAXING_CRESCENT if self._state == 7: - return "first_quarter" + return STATE_FIRST_QUARTER if self._state < 14: - return "waxing_gibbous" + return STATE_WAXING_GIBBOUS if self._state == 14: - return "full_moon" + return STATE_FULL_MOON if self._state < 21: - return "waning_gibbous" + return STATE_WANING_GIBBOUS if self._state == 21: - return "last_quarter" - return "waning_crescent" + return STATE_LAST_QUARTER + return STATE_WANING_CRESCENT @property def icon(self): """Icon to use in the frontend, if any.""" - return ICON + return MOON_ICONS.get(self.state) async def async_update(self): """Get the time and updates the states.""" - from astral import Astral - today = dt_util.as_local(dt_util.utcnow()).date() self._state = Astral().moon_phase(today) diff --git a/homeassistant/components/mpchc/media_player.py b/homeassistant/components/mpchc/media_player.py index 580156a5653..a3f2c500030 100644 --- a/homeassistant/components/mpchc/media_player.py +++ b/homeassistant/components/mpchc/media_player.py @@ -5,7 +5,7 @@ import re import requests import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py index 2628815727c..6460becbb3e 100644 --- a/homeassistant/components/mpd/media_player.py +++ b/homeassistant/components/mpd/media_player.py @@ -37,6 +37,7 @@ from homeassistant.const import ( ) import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -98,6 +99,8 @@ class MpdDevice(MediaPlayerDevice): self._is_connected = False self._muted = False self._muted_volume = 0 + self._media_position_updated_at = None + self._media_position = None # set up MPD client self._client = mpd.MPDClient() @@ -130,6 +133,11 @@ class MpdDevice(MediaPlayerDevice): self._status = self._client.status() self._currentsong = self._client.currentsong() + position = self._status["time"] + if self._media_position != position: + self._media_position_updated_at = dt_util.utcnow() + self._media_position = position + self._update_playlists() @property @@ -188,6 +196,20 @@ class MpdDevice(MediaPlayerDevice): # Time does not exist for streams return self._currentsong.get("time") + @property + def media_position(self): + """Position of current playing media in seconds. + + This is returned as part of the mpd status rather than in the details + of the current song. + """ + return self._media_position + + @property + def media_position_updated_at(self): + """Last valid time of media position.""" + return self._media_position_updated_at + @property def media_title(self): """Return the title of current playing media.""" diff --git a/homeassistant/components/mqtt/.translations/da.json b/homeassistant/components/mqtt/.translations/da.json index ebe5696f514..93ea57d49ea 100644 --- a/homeassistant/components/mqtt/.translations/da.json +++ b/homeassistant/components/mqtt/.translations/da.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning af MQTT" + "single_instance_allowed": "Kun en enkelt konfiguration af MQTT er tilladt." }, "error": { "cannot_connect": "Kunne ikke oprette forbindelse til broker" @@ -10,20 +10,20 @@ "broker": { "data": { "broker": "Broker", - "discovery": "Aktiv\u00e9r opdagelse", + "discovery": "Aktiv\u00e9r automatisk fund", "password": "Adgangskode", "port": "Port", "username": "Brugernavn" }, - "description": "Indtast venligst forbindelsesindstillinger for din MQTT broker.", + "description": "Indtast venligst forbindelsesindstillinger for din MQTT-broker.", "title": "MQTT" }, "hassio_confirm": { "data": { "discovery": "Aktiv\u00e9r opdagelse" }, - "description": "Vil du konfigurere Home Assistant til at oprette forbindelse til MQTT brokeren, der leveres af hass.io add-on {addon}?", - "title": "MQTT Broker via Hass.io add-on" + "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" } }, "title": "MQTT" diff --git a/homeassistant/components/mqtt/.translations/it.json b/homeassistant/components/mqtt/.translations/it.json index ed33b182a96..cf2b3ddf7d5 100644 --- a/homeassistant/components/mqtt/.translations/it.json +++ b/homeassistant/components/mqtt/.translations/it.json @@ -22,8 +22,8 @@ "data": { "discovery": "Attiva l'individuazione" }, - "description": "Vuoi configurare Home Assistant per connettersi al broker MQTT fornito dall'add-on di Hass.io {addon}?", - "title": "Broker MQTT tramite l'add-on di Hass.io" + "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" } }, "title": "MQTT" diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 70523a840a3..a9d5ac93ebc 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -1,6 +1,5 @@ """Support for MQTT message handling.""" import asyncio -import sys from functools import partial, wraps import inspect from itertools import groupby @@ -10,14 +9,13 @@ from operator import attrgetter import os import socket import ssl +import sys import time from typing import Any, Callable, List, Optional, Union import attr import requests.certs import voluptuous as vol -import paho.mqtt.client as mqtt -from paho.mqtt.matcher import MQTTMatcher from homeassistant import config_entries from homeassistant.components import websocket_api @@ -34,9 +32,9 @@ from homeassistant.const import ( ) from homeassistant.core import Event, ServiceCall, callback from homeassistant.exceptions import ( + ConfigEntryNotReady, HomeAssistantError, Unauthorized, - ConfigEntryNotReady, ) from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -49,16 +47,16 @@ from homeassistant.util.logging import catch_log_exception # Loading the config flow file will register the flow from . import config_flow, discovery, server # noqa: F401 pylint: disable=unused-import from .const import ( + ATTR_DISCOVERY_HASH, CONF_BROKER, CONF_DISCOVERY, - DEFAULT_DISCOVERY, CONF_STATE_TOPIC, - ATTR_DISCOVERY_HASH, - PROTOCOL_311, + DEFAULT_DISCOVERY, DEFAULT_QOS, + PROTOCOL_311, ) from .discovery import MQTT_DISCOVERY_UPDATED, clear_discovery_hash -from .models import PublishPayloadType, Message, MessageCallbackType +from .models import Message, MessageCallbackType, PublishPayloadType from .subscription import async_subscribe_topics, async_unsubscribe_topics _LOGGER = logging.getLogger(__name__) @@ -138,10 +136,10 @@ def valid_topic(value: Any) -> str: raise vol.Invalid("MQTT topic name/filter must not be empty.") if len(raw_value) > 65535: raise vol.Invalid( - "MQTT topic name/filter must not be longer than " "65535 encoded bytes." + "MQTT topic name/filter must not be longer than 65535 encoded bytes." ) if "\0" in value: - raise vol.Invalid("MQTT topic name/filter must not contain null " "character.") + raise vol.Invalid("MQTT topic name/filter must not contain null character.") return value @@ -153,7 +151,7 @@ def valid_subscribe_topic(value: Any) -> str: i < len(value) - 1 and value[i + 1] != "/" ): raise vol.Invalid( - "Single-level wildcard must occupy an entire " "level of the filter" + "Single-level wildcard must occupy an entire level of the filter" ) index = value.find("#") @@ -166,7 +164,7 @@ def valid_subscribe_topic(value: Any) -> str: ) if len(value) > 1 and value[index - 1] != "/": raise vol.Invalid( - "Multi-level wildcard must be after a topic " "level separator." + "Multi-level wildcard must be after a topic level separator." ) return value @@ -335,7 +333,6 @@ MQTT_PUBLISH_SCHEMA = vol.Schema( ) -# pylint: disable=invalid-name SubscribePayloadType = Union[str, bytes] # Only bytes if encoding is None @@ -726,6 +723,11 @@ class MQTT: tls_version: Optional[int], ) -> None: """Initialize Home Assistant MQTT client.""" + # We don't import them on the top because some integrations + # should be able to optionally rely on MQTT. + # pylint: disable=import-outside-toplevel + import paho.mqtt.client as mqtt + self.hass = hass self.broker = broker self.port = port @@ -787,6 +789,9 @@ class MQTT: This method is a coroutine. """ + # pylint: disable=import-outside-toplevel + import paho.mqtt.client as mqtt + result: int = None try: result = await self.hass.async_add_job( @@ -878,6 +883,9 @@ class MQTT: Resubscribe to all topics we were subscribed to and publish birth message. """ + # pylint: disable=import-outside-toplevel + import paho.mqtt.client as mqtt + if result_code != mqtt.CONNACK_ACCEPTED: _LOGGER.error( "Unable to connect to the MQTT broker: %s", @@ -969,6 +977,9 @@ class MQTT: def _raise_on_error(result_code: int) -> None: """Raise error if error result.""" + # pylint: disable=import-outside-toplevel + import paho.mqtt.client as mqtt + if result_code != 0: raise HomeAssistantError( "Error talking to MQTT: {}".format(mqtt.error_string(result_code)) @@ -977,6 +988,9 @@ def _raise_on_error(result_code: int) -> None: def _match_topic(subscription: str, topic: str) -> bool: """Test if topic matches subscription.""" + # pylint: disable=import-outside-toplevel + from paho.mqtt.matcher import MQTTMatcher + matcher = MQTTMatcher() matcher[subscription] = True try: diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index 5e995494a64..6f9b1720102 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -135,6 +135,8 @@ ABBREVIATIONS = { "stat_off": "state_off", "stat_on": "state_on", "stat_open": "state_open", + "stat_locked": "state_locked", + "stat_unlocked": "state_unlocked", "stat_t": "state_topic", "stat_tpl": "state_template", "stat_val_tpl": "state_value_template", diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index 7171a55b270..43d0bb570a8 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -6,6 +6,11 @@ import voluptuous as vol from homeassistant.components import mqtt import homeassistant.components.alarm_control_panel as alarm +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, +) from homeassistant.const import ( CONF_CODE, CONF_DEVICE, @@ -223,6 +228,11 @@ class MqttAlarm( """Return the state of the device.""" return self._state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT + @property def code_format(self): """Return one or more digits/characters.""" diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index f3ae36c5746..831c47c3621 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -7,7 +7,7 @@ import voluptuous as vol from homeassistant.components import camera, mqtt from homeassistant.components.camera import PLATFORM_SCHEMA, Camera -from homeassistant.const import CONF_NAME, CONF_DEVICE +from homeassistant.const import CONF_DEVICE, CONF_NAME from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 4b163c523fa..f9d7d8752f2 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -14,22 +14,25 @@ from homeassistant.components.climate.const import ( ATTR_TARGET_TEMP_LOW, DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, + FAN_AUTO, + FAN_HIGH, + FAN_LOW, + FAN_MEDIUM, HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_DRY, HVAC_MODE_FAN_ONLY, HVAC_MODE_HEAT, HVAC_MODE_OFF, + PRESET_AWAY, + PRESET_NONE, SUPPORT_AUX_HEAT, SUPPORT_FAN_MODE, SUPPORT_PRESET_MODE, SUPPORT_SWING_MODE, SUPPORT_TARGET_TEMPERATURE, - PRESET_AWAY, SUPPORT_TARGET_TEMPERATURE_RANGE, - PRESET_NONE, ) -from homeassistant.components.fan import SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM from homeassistant.const import ( ATTR_TEMPERATURE, CONF_DEVICE, @@ -165,8 +168,7 @@ PLATFORM_SCHEMA = ( vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_FAN_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional( - CONF_FAN_MODE_LIST, - default=[HVAC_MODE_AUTO, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH], + 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, @@ -339,7 +341,7 @@ class MqttClimate( self._target_temp_high = config[CONF_TEMP_INITIAL] if self._topic[CONF_FAN_MODE_STATE_TOPIC] is None: - self._current_fan_mode = SPEED_LOW + self._current_fan_mode = FAN_LOW if self._topic[CONF_SWING_MODE_STATE_TOPIC] is None: self._current_swing_mode = HVAC_MODE_OFF if self._topic[CONF_MODE_STATE_TOPIC] is None: @@ -756,12 +758,14 @@ class MqttClimate( if self._away: optimistic_update = optimistic_update or self._set_away_mode(False) elif preset_mode == PRESET_AWAY: + if self._hold: + self._set_hold_mode(None) optimistic_update = optimistic_update or self._set_away_mode(True) - - if self._hold: - optimistic_update = optimistic_update or self._set_hold_mode(None) - elif preset_mode not in (None, PRESET_AWAY): - optimistic_update = optimistic_update or self._set_hold_mode(preset_mode) + else: + hold_mode = preset_mode + if preset_mode == PRESET_NONE: + hold_mode = None + optimistic_update = optimistic_update or self._set_hold_mode(hold_mode) if optimistic_update: self.async_write_ha_state() diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index a8a378e723c..d3c6ee819b5 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -3,7 +3,6 @@ from collections import OrderedDict import queue import voluptuous as vol -import paho.mqtt.client as mqtt from homeassistant import config_entries from homeassistant.const import ( @@ -126,6 +125,8 @@ class FlowHandler(config_entries.ConfigFlow): def try_connection(broker, port, username, password, protocol="3.1"): """Test if we can connect to an MQTT broker.""" + import paho.mqtt.client as mqtt + if protocol == "3.1": proto = mqtt.MQTTv31 else: diff --git a/homeassistant/components/mqtt/device_tracker.py b/homeassistant/components/mqtt/device_tracker.py index d25d7ce21d3..bcc969f0354 100644 --- a/homeassistant/components/mqtt/device_tracker.py +++ b/homeassistant/components/mqtt/device_tracker.py @@ -5,9 +5,9 @@ import voluptuous as vol from homeassistant.components import mqtt from homeassistant.components.device_tracker import PLATFORM_SCHEMA, SOURCE_TYPES +from homeassistant.const import CONF_DEVICES, STATE_HOME, STATE_NOT_HOME from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_DEVICES, STATE_NOT_HOME, STATE_HOME from . import CONF_QOS diff --git a/homeassistant/components/mqtt/light/__init__.py b/homeassistant/components/mqtt/light/__init__.py index 95a850fb9e8..a72008c059f 100644 --- a/homeassistant/components/mqtt/light/__init__.py +++ b/homeassistant/components/mqtt/light/__init__.py @@ -15,7 +15,8 @@ from homeassistant.components.mqtt.discovery import ( clear_discovery_hash, ) from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.typing import HomeAssistantType, ConfigType +from homeassistant.helpers.typing import ConfigType, HomeAssistantType + from .schema import CONF_SCHEMA, MQTT_LIGHT_SCHEMA_SCHEMA from .schema_basic import PLATFORM_SCHEMA_BASIC, async_setup_entity_basic from .schema_json import PLATFORM_SCHEMA_JSON, async_setup_entity_json diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index 829809dd9c3..ff57db7c8c1 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -8,7 +8,6 @@ import logging import voluptuous as vol -from homeassistant.core import callback from homeassistant.components import mqtt from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -16,29 +15,12 @@ from homeassistant.components.light import ( ATTR_EFFECT, ATTR_HS_COLOR, ATTR_WHITE_VALUE, - Light, SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, - SUPPORT_COLOR, SUPPORT_WHITE_VALUE, -) -from homeassistant.const import ( - CONF_BRIGHTNESS, - CONF_COLOR_TEMP, - CONF_DEVICE, - CONF_EFFECT, - CONF_HS, - CONF_NAME, - CONF_OPTIMISTIC, - CONF_PAYLOAD_OFF, - CONF_PAYLOAD_ON, - STATE_ON, - CONF_RGB, - CONF_STATE, - CONF_VALUE_TEMPLATE, - CONF_WHITE_VALUE, - CONF_XY, + Light, ) from homeassistant.components.mqtt import ( CONF_COMMAND_TOPIC, @@ -52,8 +34,26 @@ from homeassistant.components.mqtt import ( MqttEntityDeviceInfo, subscription, ) -from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.const import ( + CONF_BRIGHTNESS, + CONF_COLOR_TEMP, + CONF_DEVICE, + CONF_EFFECT, + CONF_HS, + CONF_NAME, + CONF_OPTIMISTIC, + CONF_PAYLOAD_OFF, + CONF_PAYLOAD_ON, + CONF_RGB, + CONF_STATE, + CONF_VALUE_TEMPLATE, + CONF_WHITE_VALUE, + CONF_XY, + STATE_ON, +) +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.util.color as color_util from .schema import MQTT_LIGHT_SCHEMA_SCHEMA diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index c80ab2f95a7..dd69a8e87d6 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -5,9 +5,9 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.mqtt_template/ """ import logging + import voluptuous as vol -from homeassistant.core import callback from homeassistant.components import mqtt from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -17,21 +17,14 @@ from homeassistant.components.light import ( ATTR_HS_COLOR, ATTR_TRANSITION, ATTR_WHITE_VALUE, - Light, SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, - SUPPORT_COLOR, SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE, -) -from homeassistant.const import ( - CONF_DEVICE, - CONF_NAME, - CONF_OPTIMISTIC, - STATE_ON, - STATE_OFF, + Light, ) from homeassistant.components.mqtt import ( CONF_COMMAND_TOPIC, @@ -45,9 +38,17 @@ from homeassistant.components.mqtt import ( MqttEntityDeviceInfo, subscription, ) +from homeassistant.const import ( + CONF_DEVICE, + CONF_NAME, + CONF_OPTIMISTIC, + STATE_OFF, + STATE_ON, +) +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -import homeassistant.util.color as color_util from homeassistant.helpers.restore_state import RestoreEntity +import homeassistant.util.color as color_util from .schema import MQTT_LIGHT_SCHEMA_SCHEMA diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index ccf8f2569fa..6910e955288 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -36,10 +36,16 @@ _LOGGER = logging.getLogger(__name__) CONF_PAYLOAD_LOCK = "payload_lock" CONF_PAYLOAD_UNLOCK = "payload_unlock" +CONF_STATE_LOCKED = "state_locked" +CONF_STATE_UNLOCKED = "state_unlocked" + DEFAULT_NAME = "MQTT Lock" DEFAULT_OPTIMISTIC = False DEFAULT_PAYLOAD_LOCK = "LOCK" DEFAULT_PAYLOAD_UNLOCK = "UNLOCK" +DEFAULT_STATE_LOCKED = "LOCKED" +DEFAULT_STATE_UNLOCKED = "UNLOCKED" + PLATFORM_SCHEMA = ( mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( { @@ -50,6 +56,10 @@ PLATFORM_SCHEMA = ( 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, } ) @@ -152,9 +162,9 @@ class MqttLock( payload = msg.payload if value_template is not None: payload = value_template.async_render_with_possible_json_value(payload) - if payload == self._config[CONF_PAYLOAD_LOCK]: + if payload == self._config[CONF_STATE_LOCKED]: self._state = True - elif payload == self._config[CONF_PAYLOAD_UNLOCK]: + elif payload == self._config[CONF_STATE_UNLOCKED]: self._state = False self.async_write_ha_state() diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py index 5f014aadd08..cfdecd3383d 100644 --- a/homeassistant/components/mqtt/models.py +++ b/homeassistant/components/mqtt/models.py @@ -1,9 +1,8 @@ """Modesl used by multiple MQTT modules.""" -from typing import Union, Callable +from typing import Callable, Union import attr -# pylint: disable=invalid-name PublishPayloadType = Union[str, bytes, int, float, None] diff --git a/homeassistant/components/mqtt/server.py b/homeassistant/components/mqtt/server.py index f5d369a75c7..3ed2fb71b14 100644 --- a/homeassistant/components/mqtt/server.py +++ b/homeassistant/components/mqtt/server.py @@ -4,8 +4,6 @@ import logging import tempfile import voluptuous as vol -from hbmqtt.broker import Broker, BrokerException -from passlib.apps import custom_app_context from homeassistant.const import EVENT_HOMEASSISTANT_STOP import homeassistant.helpers.config_validation as cv @@ -37,6 +35,9 @@ def async_start(hass, password, server_config): This method is a coroutine. """ + # pylint: disable=import-outside-toplevel + from hbmqtt.broker import Broker, BrokerException + passwd = tempfile.NamedTemporaryFile() gen_server_config, client_config = generate_config(hass, passwd, password) @@ -65,6 +66,9 @@ def async_start(hass, password, server_config): def generate_config(hass, passwd, password): """Generate a configuration based on current Home Assistant instance.""" + # pylint: disable=import-outside-toplevel + from passlib.apps import custom_app_context + config = { "listeners": { "default": { diff --git a/homeassistant/components/mqtt/vacuum/__init__.py b/homeassistant/components/mqtt/vacuum/__init__.py index 12fd4c51693..84f564e5c7e 100644 --- a/homeassistant/components/mqtt/vacuum/__init__.py +++ b/homeassistant/components/mqtt/vacuum/__init__.py @@ -8,14 +8,15 @@ import logging import voluptuous as vol -from homeassistant.components.vacuum import DOMAIN from homeassistant.components.mqtt import ATTR_DISCOVERY_HASH from homeassistant.components.mqtt.discovery import ( MQTT_DISCOVERY_NEW, clear_discovery_hash, ) +from homeassistant.components.vacuum import DOMAIN from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .schema import CONF_SCHEMA, LEGACY, STATE, MQTT_VACUUM_SCHEMA + +from .schema import CONF_SCHEMA, LEGACY, MQTT_VACUUM_SCHEMA, STATE from .schema_legacy import PLATFORM_SCHEMA_LEGACY, async_setup_entity_legacy from .schema_state import PLATFORM_SCHEMA_STATE, async_setup_entity_state diff --git a/homeassistant/components/mqtt/vacuum/schema_legacy.py b/homeassistant/components/mqtt/vacuum/schema_legacy.py index d770cfbb7f8..6c08b18bc9c 100644 --- a/homeassistant/components/mqtt/vacuum/schema_legacy.py +++ b/homeassistant/components/mqtt/vacuum/schema_legacy.py @@ -1,10 +1,18 @@ """Support for Legacy MQTT vacuum.""" -import logging import json +import logging import voluptuous as vol from homeassistant.components import mqtt +from homeassistant.components.mqtt import ( + CONF_UNIQUE_ID, + MqttAttributes, + MqttAvailability, + MqttDiscoveryUpdate, + MqttEntityDeviceInfo, + subscription, +) from homeassistant.components.vacuum import ( SUPPORT_BATTERY, SUPPORT_CLEAN_SPOT, @@ -24,15 +32,6 @@ from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.icon import icon_for_battery_level -from homeassistant.components.mqtt import ( - CONF_UNIQUE_ID, - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, - subscription, -) - from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/mqtt/vacuum/schema_state.py b/homeassistant/components/mqtt/vacuum/schema_state.py index 40b3eeb752c..9dd5053d019 100644 --- a/homeassistant/components/mqtt/vacuum/schema_state.py +++ b/homeassistant/components/mqtt/vacuum/schema_state.py @@ -1,27 +1,39 @@ """Support for a State MQTT vacuum.""" -import logging import json +import logging import voluptuous as vol from homeassistant.components import mqtt +from homeassistant.components.mqtt import ( + CONF_COMMAND_TOPIC, + CONF_QOS, + CONF_RETAIN, + CONF_STATE_TOPIC, + CONF_UNIQUE_ID, + MqttAttributes, + MqttAvailability, + MqttDiscoveryUpdate, + MqttEntityDeviceInfo, + subscription, +) from homeassistant.components.vacuum import ( + STATE_CLEANING, + STATE_DOCKED, + STATE_ERROR, + STATE_IDLE, + STATE_PAUSED, + STATE_RETURNING, SUPPORT_BATTERY, SUPPORT_CLEAN_SPOT, SUPPORT_FAN_SPEED, - SUPPORT_START, SUPPORT_LOCATE, SUPPORT_PAUSE, SUPPORT_RETURN_HOME, SUPPORT_SEND_COMMAND, + SUPPORT_START, SUPPORT_STATUS, SUPPORT_STOP, - STATE_CLEANING, - STATE_DOCKED, - STATE_PAUSED, - STATE_IDLE, - STATE_RETURNING, - STATE_ERROR, StateVacuumDevice, ) from homeassistant.const import ( @@ -33,19 +45,6 @@ from homeassistant.const import ( from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.components.mqtt import ( - CONF_UNIQUE_ID, - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, - subscription, - CONF_COMMAND_TOPIC, - CONF_RETAIN, - CONF_STATE_TOPIC, - CONF_QOS, -) - from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/mqtt_eventstream/__init__.py b/homeassistant/components/mqtt_eventstream/__init__.py index ce11c7cb933..48448355df8 100644 --- a/homeassistant/components/mqtt_eventstream/__init__.py +++ b/homeassistant/components/mqtt_eventstream/__init__.py @@ -4,7 +4,6 @@ import json import voluptuous as vol -from homeassistant.core import callback from homeassistant.components.mqtt import valid_publish_topic, valid_subscribe_topic from homeassistant.const import ( ATTR_SERVICE_DATA, @@ -13,7 +12,7 @@ from homeassistant.const import ( EVENT_TIME_CHANGED, MATCH_ALL, ) -from homeassistant.core import EventOrigin, State +from homeassistant.core import EventOrigin, State, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.json import JSONEncoder diff --git a/homeassistant/components/mqtt_json/device_tracker.py b/homeassistant/components/mqtt_json/device_tracker.py index a7dda18e7e6..8f64636b817 100644 --- a/homeassistant/components/mqtt_json/device_tracker.py +++ b/homeassistant/components/mqtt_json/device_tracker.py @@ -5,17 +5,17 @@ import logging import voluptuous as vol from homeassistant.components import mqtt -from homeassistant.core import callback -from homeassistant.components.mqtt import CONF_QOS from homeassistant.components.device_tracker import PLATFORM_SCHEMA -import homeassistant.helpers.config_validation as cv +from homeassistant.components.mqtt import CONF_QOS from homeassistant.const import ( - CONF_DEVICES, + ATTR_BATTERY_LEVEL, ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, - ATTR_BATTERY_LEVEL, + CONF_DEVICES, ) +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/mqtt_room/sensor.py b/homeassistant/components/mqtt_room/sensor.py index 5323383e892..d8dfa65f799 100644 --- a/homeassistant/components/mqtt_room/sensor.py +++ b/homeassistant/components/mqtt_room/sensor.py @@ -1,16 +1,16 @@ """Support for MQTT room presence detection.""" -import logging -import json from datetime import timedelta +import json +import logging import voluptuous as vol from homeassistant.components import mqtt -import homeassistant.helpers.config_validation as cv from homeassistant.components.mqtt import CONF_STATE_TOPIC from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, CONF_TIMEOUT, STATE_NOT_HOME, ATTR_ID +from homeassistant.const import ATTR_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 diff --git a/homeassistant/components/mqtt_statestream/__init__.py b/homeassistant/components/mqtt_statestream/__init__.py index f99b341df5b..e35f2653283 100644 --- a/homeassistant/components/mqtt_statestream/__init__.py +++ b/homeassistant/components/mqtt_statestream/__init__.py @@ -3,6 +3,7 @@ import json import voluptuous as vol +from homeassistant.components.mqtt import valid_publish_topic from homeassistant.const import ( CONF_DOMAINS, CONF_ENTITIES, @@ -11,11 +12,10 @@ from homeassistant.const import ( MATCH_ALL, ) from homeassistant.core import callback -from homeassistant.components.mqtt import valid_publish_topic +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import generate_filter from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.json import JSONEncoder -import homeassistant.helpers.config_validation as cv CONF_BASE_TOPIC = "base_topic" CONF_PUBLISH_ATTRIBUTES = "publish_attributes" diff --git a/homeassistant/components/mycroft/notify.py b/homeassistant/components/mycroft/notify.py index 93b724f97cd..335eff87546 100644 --- a/homeassistant/components/mycroft/notify.py +++ b/homeassistant/components/mycroft/notify.py @@ -1,8 +1,9 @@ """Mycroft AI notification platform.""" import logging -from homeassistant.components.notify import BaseNotificationService +from mycroftapi import MycroftAPI +from homeassistant.components.notify import BaseNotificationService _LOGGER = logging.getLogger(__name__) @@ -21,7 +22,6 @@ class MycroftNotificationService(BaseNotificationService): def send_message(self, message="", **kwargs): """Send a message mycroft to speak on instance.""" - from mycroftapi import MycroftAPI text = message mycroft = MycroftAPI(self.mycroft_ip) diff --git a/homeassistant/components/myq/cover.py b/homeassistant/components/myq/cover.py index b6da7174f05..8a83f398e64 100644 --- a/homeassistant/components/myq/cover.py +++ b/homeassistant/components/myq/cover.py @@ -6,10 +6,10 @@ from pymyq.errors import MyQError import voluptuous as vol from homeassistant.components.cover import ( - CoverDevice, PLATFORM_SCHEMA, SUPPORT_CLOSE, SUPPORT_OPEN, + CoverDevice, ) from homeassistant.const import ( CONF_PASSWORD, diff --git a/homeassistant/components/mysensors/__init__.py b/homeassistant/components/mysensors/__init__.py index cbedd947843..a528be15e14 100644 --- a/homeassistant/components/mysensors/__init__.py +++ b/homeassistant/components/mysensors/__init__.py @@ -25,7 +25,7 @@ from .const import ( MYSENSORS_GATEWAYS, ) from .device import get_mysensors_devices -from .gateway import get_mysensors_gateway, setup_gateways, finish_setup +from .gateway import finish_setup, get_mysensors_gateway, setup_gateways _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/mysensors/climate.py b/homeassistant/components/mysensors/climate.py index dc053e60de1..4939c0c83e5 100644 --- a/homeassistant/components/mysensors/climate.py +++ b/homeassistant/components/mysensors/climate.py @@ -8,10 +8,10 @@ from homeassistant.components.climate.const import ( HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_HEAT, + HVAC_MODE_OFF, SUPPORT_FAN_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, - HVAC_MODE_OFF, ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT diff --git a/homeassistant/components/mysensors/const.py b/homeassistant/components/mysensors/const.py index 45f603a2cb4..ccb646eb47e 100644 --- a/homeassistant/components/mysensors/const.py +++ b/homeassistant/components/mysensors/const.py @@ -25,6 +25,8 @@ NODE_CALLBACK = "mysensors_node_callback_{}_{}" TYPE = "type" UPDATE_DELAY = 0.1 +SERVICE_SEND_IR_CODE = "send_ir_code" + BINARY_SENSOR_TYPES = { "S_DOOR": {"V_TRIPPED"}, "S_MOTION": {"V_TRIPPED"}, diff --git a/homeassistant/components/mysensors/device.py b/homeassistant/components/mysensors/device.py index 6d766530b04..e5853fce5ca 100644 --- a/homeassistant/components/mysensors/device.py +++ b/homeassistant/components/mysensors/device.py @@ -1,6 +1,6 @@ """Handle MySensors devices.""" -import logging from functools import partial +import logging from homeassistant.const import ATTR_BATTERY_LEVEL, STATE_OFF, STATE_ON from homeassistant.core import callback diff --git a/homeassistant/components/mysensors/gateway.py b/homeassistant/components/mysensors/gateway.py index 366692205a7..903ec069b51 100644 --- a/homeassistant/components/mysensors/gateway.py +++ b/homeassistant/components/mysensors/gateway.py @@ -6,6 +6,7 @@ import socket import sys import async_timeout +from mysensors import mysensors import voluptuous as vol from homeassistant.const import CONF_OPTIMISTIC, EVENT_HOMEASSISTANT_STOP @@ -84,7 +85,6 @@ async def setup_gateways(hass, config): async def _get_gateway(hass, config, gateway_conf, persistence_file): """Return gateway after setup of the gateway.""" - from mysensors import mysensors conf = config[DOMAIN] persistence = conf[CONF_PERSISTENCE] diff --git a/homeassistant/components/mysensors/handler.py b/homeassistant/components/mysensors/handler.py index 0923b6bc8de..31e836f4767 100644 --- a/homeassistant/components/mysensors/handler.py +++ b/homeassistant/components/mysensors/handler.py @@ -5,7 +5,7 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.util import decorator -from .const import MYSENSORS_GATEWAY_READY, CHILD_CALLBACK, NODE_CALLBACK +from .const import CHILD_CALLBACK, MYSENSORS_GATEWAY_READY, NODE_CALLBACK from .device import get_mysensors_devices from .helpers import discover_mysensors_platform, validate_set_msg diff --git a/homeassistant/components/mysensors/light.py b/homeassistant/components/mysensors/light.py index 8f0d0906311..19eb8e9e92c 100644 --- a/homeassistant/components/mysensors/light.py +++ b/homeassistant/components/mysensors/light.py @@ -11,8 +11,8 @@ from homeassistant.components.light import ( Light, ) from homeassistant.const import STATE_OFF, STATE_ON -from homeassistant.util.color import rgb_hex_to_rgb_list 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 diff --git a/homeassistant/components/mysensors/sensor.py b/homeassistant/components/mysensors/sensor.py index a7d1cad98fa..ddad451d20f 100644 --- a/homeassistant/components/mysensors/sensor.py +++ b/homeassistant/components/mysensors/sensor.py @@ -2,10 +2,10 @@ from homeassistant.components import mysensors from homeassistant.components.sensor import DOMAIN from homeassistant.const import ( + ENERGY_KILO_WATT_HOUR, + POWER_WATT, TEMP_CELSIUS, TEMP_FAHRENHEIT, - POWER_WATT, - ENERGY_KILO_WATT_HOUR, ) SENSORS = { diff --git a/homeassistant/components/mysensors/services.yaml b/homeassistant/components/mysensors/services.yaml index e69de29bb2d..74a5ff0f183 100644 --- a/homeassistant/components/mysensors/services.yaml +++ b/homeassistant/components/mysensors/services.yaml @@ -0,0 +1,9 @@ +send_ir_code: + description: Set an IR code as a state attribute for a MySensors IR device switch and turn the switch on. + fields: + entity_id: + description: Name(s) of entities that should have the IR code set and be turned on. Platform dependent. + example: 'switch.living_room_1_1' + V_IR_SEND: + description: IR code to send. + example: '0xC284' \ No newline at end of file diff --git a/homeassistant/components/mysensors/switch.py b/homeassistant/components/mysensors/switch.py index c624aaafa34..ec28649d70f 100644 --- a/homeassistant/components/mysensors/switch.py +++ b/homeassistant/components/mysensors/switch.py @@ -1,13 +1,14 @@ """Support for MySensors switches.""" import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components import mysensors from homeassistant.components.switch import DOMAIN, SwitchDevice 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 ATTR_IR_CODE = "V_IR_SEND" -SERVICE_SEND_IR_CODE = "mysensors_send_ir_code" SEND_IR_CODE_SERVICE_SCHEMA = vol.Schema( {vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Required(ATTR_IR_CODE): cv.string} @@ -64,7 +65,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= await device.async_turn_on(**kwargs) hass.services.async_register( - DOMAIN, + MYSENSORS_DOMAIN, SERVICE_SEND_IR_CODE, async_send_ir_code_service, schema=SEND_IR_CODE_SERVICE_SCHEMA, diff --git a/homeassistant/components/mystrom/light.py b/homeassistant/components/mystrom/light.py index d878ee60302..56fe369144b 100644 --- a/homeassistant/components/mystrom/light.py +++ b/homeassistant/components/mystrom/light.py @@ -1,21 +1,23 @@ """Support for myStrom Wifi bulbs.""" import logging +from pymystrom.bulb import MyStromBulb +from pymystrom.exceptions import MyStromConnectionError import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.light import ( - Light, - PLATFORM_SCHEMA, ATTR_BRIGHTNESS, - SUPPORT_BRIGHTNESS, - SUPPORT_EFFECT, ATTR_EFFECT, - SUPPORT_FLASH, - SUPPORT_COLOR, ATTR_HS_COLOR, + PLATFORM_SCHEMA, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, + SUPPORT_EFFECT, + SUPPORT_FLASH, + Light, ) from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -39,8 +41,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the myStrom Light platform.""" - from pymystrom.bulb import MyStromBulb - from pymystrom.exceptions import MyStromConnectionError host = config.get(CONF_HOST) mac = config.get(CONF_MAC) @@ -107,7 +107,6 @@ class MyStromLight(Light): def turn_on(self, **kwargs): """Turn on the light.""" - from pymystrom.exceptions import MyStromConnectionError brightness = kwargs.get(ATTR_BRIGHTNESS, 255) effect = kwargs.get(ATTR_EFFECT) @@ -136,7 +135,6 @@ class MyStromLight(Light): def turn_off(self, **kwargs): """Turn off the bulb.""" - from pymystrom.exceptions import MyStromConnectionError try: self._bulb.set_off() @@ -145,7 +143,6 @@ class MyStromLight(Light): def update(self): """Fetch new state data for this light.""" - from pymystrom.exceptions import MyStromConnectionError try: self._state = self._bulb.get_status() diff --git a/homeassistant/components/mystrom/switch.py b/homeassistant/components/mystrom/switch.py index 0eca5598cc9..3a045e0391d 100644 --- a/homeassistant/components/mystrom/switch.py +++ b/homeassistant/components/mystrom/switch.py @@ -3,8 +3,8 @@ import logging import voluptuous as vol -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, CONF_HOST +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice +from homeassistant.const import CONF_HOST, CONF_NAME import homeassistant.helpers.config_validation as cv DEFAULT_NAME = "myStrom Switch" diff --git a/homeassistant/components/n26/__init__.py b/homeassistant/components/n26/__init__.py index e89d78a76f9..f8379cb310f 100644 --- a/homeassistant/components/n26/__init__.py +++ b/homeassistant/components/n26/__init__.py @@ -2,9 +2,9 @@ from datetime import datetime, timedelta, timezone import logging -import voluptuous as vol - from n26 import api as n26_api, config as n26_config +from requests import HTTPError +import voluptuous as vol from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME import homeassistant.helpers.config_validation as cv @@ -51,8 +51,6 @@ def setup(hass, config): api = n26_api.Api(n26_config.Config(user, password)) - from requests import HTTPError - try: api.get_token() except HTTPError as err: diff --git a/homeassistant/components/nad/media_player.py b/homeassistant/components/nad/media_player.py index 61003d980e1..0c29aac427f 100644 --- a/homeassistant/components/nad/media_player.py +++ b/homeassistant/components/nad/media_player.py @@ -1,10 +1,10 @@ """Support for interfacing with NAD receivers through RS-232.""" import logging +from nad_receiver import NADReceiver, NADReceiverTCP, NADReceiverTelnet import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, @@ -13,7 +13,8 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, ) -from homeassistant.const import CONF_NAME, STATE_OFF, STATE_ON, CONF_HOST +from homeassistant.const import CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -64,8 +65,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the NAD platform.""" if config.get(CONF_TYPE) == "RS232": - from nad_receiver import NADReceiver - add_entities( [ NAD( @@ -79,8 +78,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): True, ) elif config.get(CONF_TYPE) == "Telnet": - from nad_receiver import NADReceiverTelnet - add_entities( [ NAD( @@ -94,8 +91,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): True, ) else: - from nad_receiver import NADReceiverTCP - add_entities( [ NADtcp( diff --git a/homeassistant/components/nanoleaf/light.py b/homeassistant/components/nanoleaf/light.py index e3f3cfbeab1..4b08d0b9751 100644 --- a/homeassistant/components/nanoleaf/light.py +++ b/homeassistant/components/nanoleaf/light.py @@ -1,6 +1,7 @@ """Support for Nanoleaf Lights.""" import logging +from pynanoleaf import Nanoleaf, Unavailable import voluptuous as vol from homeassistant.components.light import ( @@ -54,7 +55,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Nanoleaf light.""" - from pynanoleaf import Nanoleaf, Unavailable if DATA_NANOLEAF not in hass.data: hass.data[DATA_NANOLEAF] = dict() @@ -222,7 +222,6 @@ class NanoleafLight(Light): def update(self): """Fetch new state data for this light.""" - from pynanoleaf import Unavailable try: self._available = self._light.available diff --git a/homeassistant/components/neato/.translations/da.json b/homeassistant/components/neato/.translations/da.json index ca180efa005..736234e92da 100644 --- a/homeassistant/components/neato/.translations/da.json +++ b/homeassistant/components/neato/.translations/da.json @@ -5,7 +5,7 @@ "invalid_credentials": "Ugyldige legitimationsoplysninger" }, "create_entry": { - "default": "Se [Neato-dokumentation] ({docs_url})." + "default": "Se [Neato-dokumentation]({docs_url})." }, "error": { "invalid_credentials": "Ugyldige legitimationsoplysninger", @@ -15,10 +15,11 @@ "user": { "data": { "password": "Adgangskode", - "username": "Brugernavn" + "username": "Brugernavn", + "vendor": "Udbyder" }, - "description": "Se [Neato-dokumentation] ({docs_url}).", - "title": "Neato kontooplysninger" + "description": "Se [Neato-dokumentation]({docs_url}).", + "title": "Neato-kontooplysninger" } }, "title": "Neato" diff --git a/homeassistant/components/neato/.translations/ko.json b/homeassistant/components/neato/.translations/ko.json index aeb591f7b20..391d0aee191 100644 --- a/homeassistant/components/neato/.translations/ko.json +++ b/homeassistant/components/neato/.translations/ko.json @@ -5,7 +5,7 @@ "invalid_credentials": "\uc0ac\uc6a9\uc790 \uc774\ub984 \ud639\uc740 \ube44\ubc00\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "create_entry": { - "default": "[Neato \uc124\uba85\uc11c] ({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." + "default": "[Neato \uc124\uba85\uc11c]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." }, "error": { "invalid_credentials": "\uc0ac\uc6a9\uc790 \uc774\ub984 \ud639\uc740 \ube44\ubc00\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", @@ -18,7 +18,7 @@ "username": "\uc0ac\uc6a9\uc790 \uc774\ub984", "vendor": "\uacf5\uae09 \uc5c5\uccb4" }, - "description": "[Neato \uc124\uba85\uc11c] ({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694.", + "description": "[Neato \uc124\uba85\uc11c]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694.", "title": "Neato \uacc4\uc815 \uc815\ubcf4" } }, diff --git a/homeassistant/components/neato/.translations/ru.json b/homeassistant/components/neato/.translations/ru.json index 999e45880cf..57989a346fa 100644 --- a/homeassistant/components/neato/.translations/ru.json +++ b/homeassistant/components/neato/.translations/ru.json @@ -2,13 +2,13 @@ "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.", - "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435." + "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435." }, "create_entry": { "default": "\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." }, "error": { - "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", + "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", "unexpected_error": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { diff --git a/homeassistant/components/neato/__init__.py b/homeassistant/components/neato/__init__.py index ddf9789f678..ad4eb02eccc 100644 --- a/homeassistant/components/neato/__init__.py +++ b/homeassistant/components/neato/__init__.py @@ -1,11 +1,11 @@ """Support for Neato botvac connected vacuum cleaners.""" import asyncio -import logging from datetime import timedelta +import logging -import voluptuous as vol from pybotvac import Account, Neato, Vorwerk from pybotvac.exceptions import NeatoException, NeatoLoginException, NeatoRobotException +import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_PASSWORD, CONF_USERNAME @@ -21,7 +21,6 @@ from .const import ( NEATO_MAP_DATA, NEATO_PERSISTENT_MAPS, NEATO_ROBOTS, - SCAN_INTERVAL_MINUTES, VALID_VENDORS, ) @@ -161,7 +160,7 @@ class NeatoHub: self.logged_in = True _LOGGER.debug("Successfully connected to Neato API") - @Throttle(timedelta(minutes=SCAN_INTERVAL_MINUTES)) + @Throttle(timedelta(minutes=1)) def update_robots(self): """Update the robot states.""" _LOGGER.debug("Running HUB.update_robots %s", self._hass.data.get(NEATO_ROBOTS)) diff --git a/homeassistant/components/neato/const.py b/homeassistant/components/neato/const.py index 6dbaeb10d36..cfe8a2dad9d 100644 --- a/homeassistant/components/neato/const.py +++ b/homeassistant/components/neato/const.py @@ -11,6 +11,8 @@ NEATO_ROBOTS = "neato_robots" SCAN_INTERVAL_MINUTES = 5 +SERVICE_NEATO_CUSTOM_CLEANING = "custom_cleaning" + VALID_VENDORS = ["neato", "vorwerk"] MODE = {1: "Eco", 2: "Turbo"} diff --git a/homeassistant/components/neato/sensor.py b/homeassistant/components/neato/sensor.py index 36175151e0e..fd5d8036f5f 100644 --- a/homeassistant/components/neato/sensor.py +++ b/homeassistant/components/neato/sensor.py @@ -41,22 +41,14 @@ class NeatoSensor(Entity): def __init__(self, neato, robot): """Initialize Neato sensor.""" self.robot = robot - self.neato = neato - self._available = self.neato.logged_in if self.neato is not None else False + self._available = neato.logged_in if neato is not None else False self._robot_name = f"{self.robot.name} {BATTERY}" self._robot_serial = self.robot.serial self._state = None def update(self): """Update Neato Sensor.""" - if self.neato is None: - _LOGGER.error("Error while updating sensor") - self._state = None - self._available = False - return - try: - self.neato.update_robots() self._state = self.robot.state except NeatoRobotException as ex: if self._available: diff --git a/homeassistant/components/neato/services.yaml b/homeassistant/components/neato/services.yaml index e69de29bb2d..b40edabd779 100644 --- a/homeassistant/components/neato/services.yaml +++ b/homeassistant/components/neato/services.yaml @@ -0,0 +1,18 @@ +custom_cleaning: + description: Zone Cleaning service call specific to Neato Botvacs. + fields: + entity_id: + description: Name of the vacuum entity. [Required] + example: 'vacuum.neato' + mode: + description: "Set the cleaning mode: 1 for eco and 2 for turbo. Defaults to turbo if not set." + example: 2 + navigation: + description: "Set the navigation mode: 1 for normal, 2 for extra care, 3 for deep. Defaults to normal if not set." + example: 1 + category: + description: "Whether to use a persistent map or not for cleaning (i.e. No go lines): 2 for no map, 4 for map. Default to using map if not set (and fallback to no map if no map is found)." + example: 2 + zone: + description: Only supported on the Botvac D7. Name of the zone to clean. Defaults to no zone i.e. complete house cleanup. + example: "Kitchen" \ No newline at end of file diff --git a/homeassistant/components/neato/switch.py b/homeassistant/components/neato/switch.py index 8536af63945..6aa0e11a43e 100644 --- a/homeassistant/components/neato/switch.py +++ b/homeassistant/components/neato/switch.py @@ -45,8 +45,7 @@ class NeatoConnectedSwitch(ToggleEntity): """Initialize the Neato Connected switches.""" self.type = switch_type self.robot = robot - self.neato = neato - self._available = self.neato.logged_in if self.neato is not None else False + self._available = neato.logged_in if neato is not None else False self._robot_name = f"{self.robot.name} {SWITCH_TYPES[self.type][0]}" self._state = None self._schedule_state = None @@ -55,15 +54,8 @@ class NeatoConnectedSwitch(ToggleEntity): def update(self): """Update the states of Neato switches.""" - if self.neato is None: - _LOGGER.error("Error while updating switches") - self._state = None - self._available = False - return - _LOGGER.debug("Running switch update") try: - self.neato.update_robots() self._state = self.robot.state except NeatoRobotException as ex: if self._available: # Print only once when available diff --git a/homeassistant/components/neato/vacuum.py b/homeassistant/components/neato/vacuum.py index 40ed79042c7..d8a3e4ded45 100644 --- a/homeassistant/components/neato/vacuum.py +++ b/homeassistant/components/neato/vacuum.py @@ -7,7 +7,6 @@ import voluptuous as vol from homeassistant.components.vacuum import ( ATTR_STATUS, - DOMAIN, STATE_CLEANING, STATE_DOCKED, STATE_ERROR, @@ -40,6 +39,7 @@ from .const import ( NEATO_PERSISTENT_MAPS, NEATO_ROBOTS, SCAN_INTERVAL_MINUTES, + SERVICE_NEATO_CUSTOM_CLEANING, ) _LOGGER = logging.getLogger(__name__) @@ -73,8 +73,6 @@ ATTR_NAVIGATION = "navigation" ATTR_CATEGORY = "category" ATTR_ZONE = "zone" -SERVICE_NEATO_CUSTOM_CLEANING = "neato_custom_cleaning" - SERVICE_NEATO_CUSTOM_CLEANING_SCHEMA = vol.Schema( { vol.Required(ATTR_ENTITY_ID): cv.entity_ids, @@ -126,7 +124,7 @@ async def async_setup_entry(hass, entry, async_add_entities): return entities hass.services.async_register( - DOMAIN, + NEATO_DOMAIN, SERVICE_NEATO_CUSTOM_CLEANING, neato_custom_cleaning_service, schema=SERVICE_NEATO_CUSTOM_CLEANING_SCHEMA, @@ -139,8 +137,7 @@ class NeatoConnectedVacuum(StateVacuumDevice): def __init__(self, neato, robot, mapdata, persistent_maps): """Initialize the Neato Connected Vacuum.""" self.robot = robot - self.neato = neato - self._available = self.neato.logged_in if self.neato is not None else False + self._available = neato.logged_in if neato is not None else False self._mapdata = mapdata self._name = f"{self.robot.name}" self._robot_has_map = self.robot.has_persistent_maps @@ -165,17 +162,14 @@ class NeatoConnectedVacuum(StateVacuumDevice): def update(self): """Update the states of Neato Vacuums.""" - if self.neato is None: - _LOGGER.error("Error while updating vacuum") - self._state = None - self._available = False - return - _LOGGER.debug("Running Neato Vacuums update") try: if self._robot_stats is None: - self._robot_stats = self.robot.get_robot_info().json() - self.neato.update_robots() + self._robot_stats = self.robot.get_general_info().json().get("data") + except NeatoRobotException: + _LOGGER.warning("Couldn't fetch robot information of %s", self._name) + + try: self._state = self.robot.state except NeatoRobotException as ex: if self._available: # print only once when available @@ -323,13 +317,11 @@ class NeatoConnectedVacuum(StateVacuumDevice): @property def device_info(self): """Device info for neato robot.""" - return { - "identifiers": {(NEATO_DOMAIN, self._robot_serial)}, - "name": self._name, - "manufacturer": self._robot_stats["data"]["mfg_name"], - "model": self._robot_stats["data"]["modelName"], - "sw_version": self._state["meta"]["firmware"], - } + info = {"identifiers": {(NEATO_DOMAIN, self._robot_serial)}, "name": self._name} + if self._robot_stats: + info["manufacturer"] = self._robot_stats["battery"]["vendor"] + info["model"] = self._robot_stats["model"] + info["sw_version"] = self._robot_stats["firmware"] def start(self): """Start cleaning or resume cleaning.""" diff --git a/homeassistant/components/nederlandse_spoorwegen/sensor.py b/homeassistant/components/nederlandse_spoorwegen/sensor.py index 0741ed4cb49..0b823962373 100644 --- a/homeassistant/components/nederlandse_spoorwegen/sensor.py +++ b/homeassistant/components/nederlandse_spoorwegen/sensor.py @@ -2,6 +2,7 @@ from datetime import datetime, timedelta import logging +import ns_api import requests import voluptuous as vol @@ -46,7 +47,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the departure sensor.""" - import ns_api nsapi = ns_api.NSAPI(config.get(CONF_EMAIL), config.get(CONF_PASSWORD)) try: diff --git a/homeassistant/components/nello/lock.py b/homeassistant/components/nello/lock.py index 3efe0a9cc5f..19f8e7aa14c 100644 --- a/homeassistant/components/nello/lock.py +++ b/homeassistant/components/nello/lock.py @@ -2,11 +2,12 @@ from itertools import filterfalse import logging +from pynello.private import Nello import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.components.lock import LockDevice, PLATFORM_SCHEMA +from homeassistant.components.lock import PLATFORM_SCHEMA, LockDevice from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -21,7 +22,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Nello lock platform.""" - from pynello.private import Nello nello = Nello(config.get(CONF_USERNAME), config.get(CONF_PASSWORD)) add_entities([NelloLock(lock) for lock in nello.locations], True) diff --git a/homeassistant/components/ness_alarm/__init__.py b/homeassistant/components/ness_alarm/__init__.py index 328d3506a97..7131ac505b5 100644 --- a/homeassistant/components/ness_alarm/__init__.py +++ b/homeassistant/components/ness_alarm/__init__.py @@ -1,17 +1,18 @@ """Support for Ness D8X/D16X devices.""" +from collections import namedtuple import datetime import logging -from collections import namedtuple +from nessclient import ArmingState, Client import voluptuous as vol from homeassistant.components.binary_sensor import DEVICE_CLASSES from homeassistant.const import ( ATTR_CODE, ATTR_STATE, - EVENT_HOMEASSISTANT_STOP, - CONF_SCAN_INTERVAL, CONF_HOST, + CONF_SCAN_INTERVAL, + EVENT_HOMEASSISTANT_STOP, ) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.discovery import async_load_platform @@ -82,7 +83,6 @@ SERVICE_SCHEMA_AUX = vol.Schema( async def async_setup(hass, config): """Set up the Ness Alarm platform.""" - from nessclient import Client, ArmingState conf = config[DOMAIN] diff --git a/homeassistant/components/ness_alarm/alarm_control_panel.py b/homeassistant/components/ness_alarm/alarm_control_panel.py index 1b45c52ab71..f77244a584e 100644 --- a/homeassistant/components/ness_alarm/alarm_control_panel.py +++ b/homeassistant/components/ness_alarm/alarm_control_panel.py @@ -2,7 +2,14 @@ import logging +from nessclient import ArmingState + import homeassistant.components.alarm_control_panel as alarm +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_TRIGGER, +) from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMING, @@ -62,6 +69,11 @@ class NessAlarmPanel(alarm.AlarmControlPanel): """Return the state of the device.""" return self._state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_TRIGGER + async def async_alarm_disarm(self, code=None): """Send disarm command.""" await self._client.disarm(code) @@ -81,7 +93,6 @@ class NessAlarmPanel(alarm.AlarmControlPanel): @callback def _handle_arming_state_change(self, arming_state): """Handle arming state update.""" - from nessclient import ArmingState if arming_state == ArmingState.UNKNOWN: self._state = None diff --git a/homeassistant/components/nest/.translations/da.json b/homeassistant/components/nest/.translations/da.json index 7dfd1c8b250..39b85754c18 100644 --- a/homeassistant/components/nest/.translations/da.json +++ b/homeassistant/components/nest/.translations/da.json @@ -18,14 +18,14 @@ "flow_impl": "Udbyder" }, "description": "V\u00e6lg hvilken godkendelsesudbyder du vil godkende med Nest.", - "title": "Godkendelses udbyder" + "title": "Godkendelsesudbyder" }, "link": { "data": { "code": "PIN-kode" }, "description": "For at forbinde din Nest-konto, [godkend din konto]({url}). \n\nEfter godkendelse skal du kopiere pin koden nedenfor.", - "title": "Link Nest-konto" + "title": "Forbind Nest-konto" } }, "title": "Nest" diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index 32bbd009417..73a28aa121f 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -1,11 +1,11 @@ """Support for Nest devices.""" +from datetime import datetime, timedelta import logging import socket -from datetime import datetime, timedelta import threading from nest import Nest -from nest.nest import AuthorizationError, APIError +from nest.nest import APIError, AuthorizationError import voluptuous as vol from homeassistant import config_entries @@ -20,11 +20,11 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.dispatcher import dispatcher_send, async_dispatcher_connect +from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send from homeassistant.helpers.entity import Entity -from .const import DOMAIN from . import local_auth +from .const import DOMAIN _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) @@ -216,7 +216,7 @@ async def async_setup_entry(hass, entry): structure.set_eta(trip_id, eta_begin, eta_end) else: _LOGGER.info( - "No thermostats found in structure: %s, " "unable to set ETA", + "No thermostats found in structure: %s, unable to set ETA", structure.name, ) diff --git a/homeassistant/components/nest/camera.py b/homeassistant/components/nest/camera.py index efc0bfbc822..34b4f6c5693 100644 --- a/homeassistant/components/nest/camera.py +++ b/homeassistant/components/nest/camera.py @@ -1,11 +1,11 @@ """Support for Nest Cameras.""" -import logging from datetime import timedelta +import logging import requests from homeassistant.components import nest -from homeassistant.components.camera import PLATFORM_SCHEMA, Camera, SUPPORT_ON_OFF +from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_ON_OFF, Camera from homeassistant.util.dt import utcnow _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/nest/climate.py b/homeassistant/components/nest/climate.py index 795ce5c80e9..f75e3a692f3 100644 --- a/homeassistant/components/nest/climate.py +++ b/homeassistant/components/nest/climate.py @@ -8,22 +8,22 @@ from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice from homeassistant.components.climate.const import ( ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, + CURRENT_HVAC_COOL, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, FAN_AUTO, FAN_ON, HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_OFF, - SUPPORT_PRESET_MODE, - SUPPORT_FAN_MODE, - SUPPORT_TARGET_TEMPERATURE, - SUPPORT_TARGET_TEMPERATURE_RANGE, PRESET_AWAY, PRESET_ECO, PRESET_NONE, - CURRENT_HVAC_HEAT, - CURRENT_HVAC_IDLE, - CURRENT_HVAC_COOL, + SUPPORT_FAN_MODE, + SUPPORT_PRESET_MODE, + SUPPORT_TARGET_TEMPERATURE, + SUPPORT_TARGET_TEMPERATURE_RANGE, ) from homeassistant.const import ( ATTR_TEMPERATURE, diff --git a/homeassistant/components/nest/config_flow.py b/homeassistant/components/nest/config_flow.py index b78896b0499..b8fa2ad93f5 100644 --- a/homeassistant/components/nest/config_flow.py +++ b/homeassistant/components/nest/config_flow.py @@ -14,7 +14,6 @@ from homeassistant.util.json import load_json from .const import DOMAIN - DATA_FLOW_IMPL = "nest_flow_implementation" _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/nest/local_auth.py b/homeassistant/components/nest/local_auth.py index 38d1827326d..8b0af5011ec 100644 --- a/homeassistant/components/nest/local_auth.py +++ b/homeassistant/components/nest/local_auth.py @@ -2,9 +2,10 @@ import asyncio from functools import partial -from nest.nest import NestAuth, AUTHORIZE_URL, AuthorizationError +from nest.nest import AUTHORIZE_URL, AuthorizationError, NestAuth from homeassistant.core import callback + from . import config_flow from .const import DOMAIN diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index 4b9f0690ac5..6becedde611 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -1,6 +1,6 @@ """Support for the Netatmo devices.""" -import logging from datetime import timedelta +import logging from urllib.error import HTTPError import pyatmo @@ -8,17 +8,17 @@ import voluptuous as vol from homeassistant.const import ( CONF_API_KEY, - CONF_PASSWORD, - CONF_USERNAME, CONF_DISCOVERY, + CONF_PASSWORD, CONF_URL, + CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, ) from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle -from .const import DOMAIN, DATA_NETATMO_AUTH +from .const import DATA_NETATMO_AUTH, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -30,6 +30,7 @@ CONF_WEBHOOKS = "webhooks" SERVICE_ADDWEBHOOK = "addwebhook" SERVICE_DROPWEBHOOK = "dropwebhook" +SERVICE_SETSCHEDULE = "set_schedule" NETATMO_AUTH = None NETATMO_WEBHOOK_URL = None @@ -63,6 +64,7 @@ ATTR_IS_KNOWN = "is_known" ATTR_FACE_URL = "face_url" ATTR_SNAPSHOT_URL = "snapshot_url" ATTR_VIGNETTE_URL = "vignette_url" +ATTR_SCHEDULE = "schedule" MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) MIN_TIME_BETWEEN_EVENT_UPDATES = timedelta(seconds=5) @@ -87,6 +89,8 @@ SCHEMA_SERVICE_ADDWEBHOOK = vol.Schema({vol.Optional(CONF_URL): cv.string}) SCHEMA_SERVICE_DROPWEBHOOK = vol.Schema({}) +SCHEMA_SERVICE_SETSCHEDULE = vol.Schema({vol.Required(ATTR_SCHEDULE): cv.string}) + def setup(hass, config): """Set up the Netatmo devices.""" @@ -106,6 +110,12 @@ def setup(hass, config): _LOGGER.error("Unable to connect to Netatmo API") return False + try: + home_data = pyatmo.HomeData(auth) + except pyatmo.NoDevice: + home_data = None + _LOGGER.debug("No climate device. Disable %s service", SERVICE_SETSCHEDULE) + # Store config to be used during entry setup hass.data[DATA_NETATMO_AUTH] = auth @@ -151,6 +161,20 @@ def setup(hass, config): schema=SCHEMA_SERVICE_DROPWEBHOOK, ) + def _service_setschedule(service): + """Service to change current home schedule.""" + schedule_name = service.data.get(ATTR_SCHEDULE) + home_data.switchHomeSchedule(schedule=schedule_name) + _LOGGER.info("Set home schedule to %s", schedule_name) + + if home_data is not None: + hass.services.register( + DOMAIN, + SERVICE_SETSCHEDULE, + _service_setschedule, + schema=SCHEMA_SERVICE_SETSCHEDULE, + ) + return True @@ -259,4 +283,4 @@ class CameraData: @Throttle(MIN_TIME_BETWEEN_EVENT_UPDATES) def update_event(self): """Call the Netatmo API to update the events.""" - self.camera_data.updateEvent(home=self.home, cameratype=self.camera_type) + self.camera_data.updateEvent(home=self.home, devicetype=self.camera_type) diff --git a/homeassistant/components/netatmo/binary_sensor.py b/homeassistant/components/netatmo/binary_sensor.py index 1a40d3952e9..06097ed852d 100644 --- a/homeassistant/components/netatmo/binary_sensor.py +++ b/homeassistant/components/netatmo/binary_sensor.py @@ -8,8 +8,8 @@ from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensor from homeassistant.const import CONF_TIMEOUT from homeassistant.helpers import config_validation as cv -from .const import DATA_NETATMO_AUTH from . import CameraData +from .const import DATA_NETATMO_AUTH _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index f3bf6a6784c..1713265a014 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -6,20 +6,20 @@ import requests import voluptuous as vol from homeassistant.components.camera import ( - PLATFORM_SCHEMA, - Camera, - SUPPORT_STREAM, CAMERA_SERVICE_SCHEMA, + PLATFORM_SCHEMA, + SUPPORT_STREAM, + Camera, ) -from homeassistant.const import CONF_VERIFY_SSL, STATE_ON, STATE_OFF +from homeassistant.const import CONF_VERIFY_SSL, STATE_OFF, STATE_ON from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import ( - async_dispatcher_send, async_dispatcher_connect, + async_dispatcher_send, ) -from .const import DATA_NETATMO_AUTH, DOMAIN from . import CameraData +from .const import DATA_NETATMO_AUTH, DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 8ba13a03889..9e320c303c8 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -1,34 +1,34 @@ """Support for Netatmo Smart thermostats.""" from datetime import timedelta import logging -from typing import Optional, List +from typing import List, Optional import pyatmo import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice from homeassistant.components.climate.const import ( + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + DEFAULT_MIN_TEMP, HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_OFF, PRESET_AWAY, PRESET_BOOST, - CURRENT_HVAC_HEAT, - CURRENT_HVAC_IDLE, - SUPPORT_TARGET_TEMPERATURE, SUPPORT_PRESET_MODE, - DEFAULT_MIN_TEMP, + SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ( - TEMP_CELSIUS, + ATTR_BATTERY_LEVEL, ATTR_TEMPERATURE, CONF_NAME, PRECISION_HALVES, STATE_OFF, - ATTR_BATTERY_LEVEL, + TEMP_CELSIUS, ) +import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle from .const import DATA_NETATMO_AUTH diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index f6c08faf8fa..232e99eeae8 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -3,10 +3,10 @@ "name": "Netatmo", "documentation": "https://www.home-assistant.io/integrations/netatmo", "requirements": [ - "pyatmo==2.3.3" + "pyatmo==3.1.0" ], "dependencies": [ "webhook" ], "codeowners": [] -} +} \ No newline at end of file diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index f76062035d2..d4d624061f5 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -1,7 +1,7 @@ """Support for the Netatmo Weather Service.""" +from datetime import timedelta import logging import threading -from datetime import timedelta from time import time import pyatmo @@ -9,19 +9,20 @@ import requests import urllib3 import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, CONF_MODE, - TEMP_CELSIUS, + CONF_NAME, + DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_BATTERY, + TEMP_CELSIUS, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import call_later from homeassistant.util import Throttle + from .const import DATA_NETATMO_AUTH, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -155,14 +156,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for module in all_module_infos.values(): if module["module_name"] not in module_names: continue + _LOGGER.debug( + "Adding module %s %s", module["module_name"], module["id"] + ) for condition in data.station_data.monitoredConditions( moduleId=module["id"] ): - _LOGGER.debug( - "Adding %s %s", - module["module_name"], - data.station_data.moduleById(mid=module["id"]), - ) entities.append(NetatmoSensor(data, module, condition.lower())) return entities @@ -200,18 +199,26 @@ class NetatmoSensor(Entity): def __init__(self, netatmo_data, module_info, sensor_type): """Initialize the sensor.""" self.netatmo_data = netatmo_data - module = self.netatmo_data.station_data.moduleById(mid=module_info["id"]) - if module["type"] == "NHC": + + device = self.netatmo_data.station_data.moduleById(mid=module_info["id"]) + if not device: + # Assume it's a station if module can't be found + device = self.netatmo_data.station_data.stationById(sid=module_info["id"]) + + if device["type"] == "NHC": self.module_name = module_info["station_name"] else: - self.module_name = module_info["module_name"] + self.module_name = ( + f"{module_info['station_name']} {module_info['module_name']}" + ) + self._name = f"{DOMAIN} {self.module_name} {SENSOR_TYPES[sensor_type][0]}" self.type = sensor_type self._state = None self._device_class = SENSOR_TYPES[self.type][3] self._icon = SENSOR_TYPES[self.type][2] self._unit_of_measurement = SENSOR_TYPES[self.type][1] - self._module_type = module["type"] + self._module_type = device["type"] self._module_id = module_info["id"] self._unique_id = f"{self._module_id}-{self.type}" @@ -541,13 +548,18 @@ class NetatmoData: self.data = {} self.station_data = self.data_class(self.auth) self.station = station + self.station_id = None + if station: + station_data = self.station_data.stationByName(self.station) + if station_data: + self.station_id = station_data.get("_id") self._next_update = time() self._update_in_progress = threading.Lock() def get_module_infos(self): """Return all modules available on the API as a dict.""" - if self.station is not None: - return self.station_data.getModules(station=self.station) + if self.station_id is not None: + return self.station_data.getModules(station_id=self.station_id) return self.station_data.getModules() def update(self): @@ -573,7 +585,7 @@ class NetatmoData: return data = self.station_data.lastData( - station=self.station, exclude=3600, byId=True + station=self.station_id, exclude=3600, byId=True ) if not data: self._next_update = time() + NETATMO_UPDATE_INTERVAL diff --git a/homeassistant/components/netatmo/services.yaml b/homeassistant/components/netatmo/services.yaml index a928f4765e0..d8fa223780a 100644 --- a/homeassistant/components/netatmo/services.yaml +++ b/homeassistant/components/netatmo/services.yaml @@ -28,3 +28,10 @@ set_light_off: entity_id: description: Entity id. example: 'camera.living_room' + +set_schedule: + description: Set the home heating schedule + fields: + schedule: + description: Schedule name + example: Standard \ No newline at end of file diff --git a/homeassistant/components/netdata/sensor.py b/homeassistant/components/netdata/sensor.py index aab901506a8..edabef9535c 100644 --- a/homeassistant/components/netdata/sensor.py +++ b/homeassistant/components/netdata/sensor.py @@ -2,6 +2,8 @@ from datetime import timedelta import logging +from netdata import Netdata +from netdata.exceptions import NetdataError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -53,7 +55,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Netdata sensor.""" - from netdata import Netdata name = config.get(CONF_NAME) host = config.get(CONF_HOST) @@ -154,7 +155,6 @@ class NetdataData: @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self): """Get the latest data from the Netdata REST API.""" - from netdata.exceptions import NetdataError try: await self.api.get_allmetrics() diff --git a/homeassistant/components/netgear/device_tracker.py b/homeassistant/components/netgear/device_tracker.py index 2e20f6423a5..d556e83ca13 100644 --- a/homeassistant/components/netgear/device_tracker.py +++ b/homeassistant/components/netgear/device_tracker.py @@ -4,21 +4,21 @@ import logging from pynetgear import Netgear import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import ( - CONF_HOST, - CONF_PASSWORD, - CONF_USERNAME, - CONF_PORT, - CONF_SSL, CONF_DEVICES, CONF_EXCLUDE, + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_SSL, + CONF_USERNAME, ) +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/netgear_lte/__init__.py b/homeassistant/components/netgear_lte/__init__.py index 4758a13c391..ac36cc1eb44 100644 --- a/homeassistant/components/netgear_lte/__init__.py +++ b/homeassistant/components/netgear_lte/__init__.py @@ -8,6 +8,9 @@ import attr import eternalegypt import voluptuous as vol +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import ( CONF_HOST, CONF_MONITORED_CONDITIONS, @@ -17,14 +20,11 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import callback -from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN -from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.helpers import config_validation as cv, discovery from homeassistant.helpers.aiohttp_client import async_create_clientsession from homeassistant.helpers.dispatcher import ( - async_dispatcher_send, async_dispatcher_connect, + async_dispatcher_send, ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval diff --git a/homeassistant/components/netgear_lte/manifest.json b/homeassistant/components/netgear_lte/manifest.json index 7e085d06307..f8c4c39cb83 100644 --- a/homeassistant/components/netgear_lte/manifest.json +++ b/homeassistant/components/netgear_lte/manifest.json @@ -3,7 +3,7 @@ "name": "Netgear lte", "documentation": "https://www.home-assistant.io/integrations/netgear_lte", "requirements": [ - "eternalegypt==0.0.10" + "eternalegypt==0.0.11" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/netgear_lte/notify.py b/homeassistant/components/netgear_lte/notify.py index 9700ee3c715..3e91394aa4f 100644 --- a/homeassistant/components/netgear_lte/notify.py +++ b/homeassistant/components/netgear_lte/notify.py @@ -4,7 +4,7 @@ import logging import attr import eternalegypt -from homeassistant.components.notify import ATTR_TARGET, BaseNotificationService, DOMAIN +from homeassistant.components.notify import ATTR_TARGET, DOMAIN, BaseNotificationService from . import CONF_RECIPIENT, DATA_KEY diff --git a/homeassistant/components/netgear_lte/sensor.py b/homeassistant/components/netgear_lte/sensor.py index 5435df88727..49301a61e4f 100644 --- a/homeassistant/components/netgear_lte/sensor.py +++ b/homeassistant/components/netgear_lte/sensor.py @@ -5,7 +5,7 @@ from homeassistant.components.sensor import DOMAIN from homeassistant.exceptions import PlatformNotReady from . import CONF_MONITORED_CONDITIONS, DATA_KEY, LTEEntity -from .sensor_types import SENSOR_SMS, SENSOR_SMS_TOTAL, SENSOR_USAGE, SENSOR_UNITS +from .sensor_types import SENSOR_SMS, SENSOR_SMS_TOTAL, SENSOR_UNITS, SENSOR_USAGE _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/netio/switch.py b/homeassistant/components/netio/switch.py index 77af9a34d68..4c9b6343f2b 100644 --- a/homeassistant/components/netio/switch.py +++ b/homeassistant/components/netio/switch.py @@ -1,22 +1,23 @@ """The Netio switch component.""" -import logging from collections import namedtuple from datetime import timedelta +import logging +from pynetio import Netio import voluptuous as vol -from homeassistant.core import callback from homeassistant import util from homeassistant.components.http import HomeAssistantView +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import ( CONF_HOST, + CONF_PASSWORD, CONF_PORT, CONF_USERNAME, - CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP, STATE_ON, ) -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -50,7 +51,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Netio platform.""" - from pynetio import Netio host = config.get(CONF_HOST) username = config.get(CONF_USERNAME) diff --git a/homeassistant/components/nextbus/sensor.py b/homeassistant/components/nextbus/sensor.py index 7622bd133f0..5909804ebd1 100644 --- a/homeassistant/components/nextbus/sensor.py +++ b/homeassistant/components/nextbus/sensor.py @@ -1,13 +1,13 @@ """NextBus sensor.""" -import logging from itertools import chain +import logging +from py_nextbus import NextBusClient import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME -from homeassistant.const import DEVICE_CLASS_TIMESTAMP +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 @@ -94,8 +94,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): stop = config[CONF_STOP] name = config.get(CONF_NAME) - from py_nextbus import NextBusClient - client = NextBusClient(output_format="json") # Ensures that the tags provided are valid, also logs out valid values @@ -227,7 +225,9 @@ class NextBusDepartureSensor(Entity): return # Generate list of upcoming times - self._attributes["upcoming"] = ", ".join(p["minutes"] for p in predictions) + self._attributes["upcoming"] = ", ".join( + sorted(p["minutes"] for p in predictions) + ) latest_prediction = maybe_first(predictions) self._state = utc_from_timestamp( diff --git a/homeassistant/components/nfandroidtv/notify.py b/homeassistant/components/nfandroidtv/notify.py index 36eed0a11db..52f0af607bc 100644 --- a/homeassistant/components/nfandroidtv/notify.py +++ b/homeassistant/components/nfandroidtv/notify.py @@ -7,9 +7,6 @@ import requests from requests.auth import HTTPBasicAuth, HTTPDigestAuth import voluptuous as vol -from homeassistant.const import CONF_TIMEOUT, CONF_HOST -import homeassistant.helpers.config_validation as cv - from homeassistant.components.notify import ( ATTR_DATA, ATTR_TITLE, @@ -17,6 +14,8 @@ from homeassistant.components.notify import ( PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.const import CONF_HOST, CONF_TIMEOUT +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/nilu/manifest.json b/homeassistant/components/nilu/manifest.json index fe7a92bc270..77df26312e9 100644 --- a/homeassistant/components/nilu/manifest.json +++ b/homeassistant/components/nilu/manifest.json @@ -6,5 +6,7 @@ "niluclient==0.1.2" ], "dependencies": [], - "codeowners": [] -} + "codeowners": [ + "@hfurubotten" + ] +} \ No newline at end of file diff --git a/homeassistant/components/nissan_leaf/__init__.py b/homeassistant/components/nissan_leaf/__init__.py index 0c72f4f43ea..fba84c936f5 100644 --- a/homeassistant/components/nissan_leaf/__init__.py +++ b/homeassistant/components/nissan_leaf/__init__.py @@ -1,8 +1,9 @@ """Support for the Nissan Leaf Carwings/Nissan Connect API.""" -from datetime import datetime, timedelta import asyncio +from datetime import datetime, timedelta import logging import sys + from pycarwings2 import CarwingsError, Session import voluptuous as vol @@ -123,9 +124,7 @@ def setup(hass, config): # for the charging request to reach the car. result = await hass.async_add_executor_job(data_store.leaf.start_charging) if result: - _LOGGER.debug( - "Start charging sent, " "request updated data in 1 minute" - ) + _LOGGER.debug("Start charging sent, request updated data in 1 minute") check_charge_at = utcnow() + timedelta(minutes=1) data_store.next_update = check_charge_at async_track_point_in_utc_time( @@ -413,7 +412,7 @@ class LeafDataStore: for attempt in range(MAX_RESPONSE_ATTEMPTS): if attempt > 0: _LOGGER.debug( - "Climate data not in yet (%s) (%s). " "Waiting (%s) seconds", + "Climate data not in yet (%s) (%s). Waiting (%s) seconds", self.leaf.vin, attempt, PYCARWINGS2_SLEEP, diff --git a/homeassistant/components/nissan_leaf/sensor.py b/homeassistant/components/nissan_leaf/sensor.py index cd82735d207..6e5d119c7a3 100644 --- a/homeassistant/components/nissan_leaf/sensor.py +++ b/homeassistant/components/nissan_leaf/sensor.py @@ -69,7 +69,7 @@ class LeafRangeSensor(LeafEntity): """Nissan Leaf Range Sensor.""" def __init__(self, car, ac_on): - """Set-up range sensor. Store if AC on.""" + """Set up range sensor. Store if AC on.""" self._ac_on = ac_on super().__init__(car) diff --git a/homeassistant/components/nmap_tracker/device_tracker.py b/homeassistant/components/nmap_tracker/device_tracker.py index 7998f875826..d41e80f17a2 100644 --- a/homeassistant/components/nmap_tracker/device_tracker.py +++ b/homeassistant/components/nmap_tracker/device_tracker.py @@ -1,19 +1,20 @@ """Support for scanning a network with nmap.""" -import logging from collections import namedtuple from datetime import timedelta +import logging from getmac import get_mac_address +from nmap import PortScanner, PortScannerError import voluptuous as vol -import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt_util from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOSTS +import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -91,8 +92,6 @@ class NmapDeviceScanner(DeviceScanner): """ _LOGGER.debug("Scanning...") - from nmap import PortScanner, PortScannerError - scanner = PortScanner() options = self._options diff --git a/homeassistant/components/nmbs/sensor.py b/homeassistant/components/nmbs/sensor.py index 8b2182665f6..35c928deb37 100644 --- a/homeassistant/components/nmbs/sensor.py +++ b/homeassistant/components/nmbs/sensor.py @@ -1,6 +1,7 @@ """Get ride details and liveboard details for NMBS (Belgian railway).""" import logging +from pyrail import iRail import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -64,7 +65,6 @@ def get_ride_duration(departure_time, arrival_time, delay=0): def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the NMBS sensor with iRail API.""" - from pyrail import iRail api_client = iRail() @@ -92,7 +92,7 @@ class NMBSLiveBoard(Entity): """Initialize the sensor for getting liveboard data.""" self._station = live_station self._api_client = api_client - + self._unique_id = f"nmbs_live_{self._station}" self._attrs = {} self._state = None @@ -101,6 +101,11 @@ class NMBSLiveBoard(Entity): """Return the sensor default name.""" return "NMBS Live" + @property + def unique_id(self): + """Return a unique ID.""" + return self._unique_id + @property def icon(self): """Return the default icon or an alert icon if delays.""" diff --git a/homeassistant/components/no_ip/__init__.py b/homeassistant/components/no_ip/__init__.py index 70ac7099d30..12d0fb08ad3 100644 --- a/homeassistant/components/no_ip/__init__.py +++ b/homeassistant/components/no_ip/__init__.py @@ -5,11 +5,11 @@ from datetime import timedelta import logging import aiohttp -from aiohttp.hdrs import USER_AGENT, AUTHORIZATION +from aiohttp.hdrs import AUTHORIZATION, USER_AGENT import async_timeout import voluptuous as vol -from homeassistant.const import CONF_DOMAIN, CONF_TIMEOUT, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_DOMAIN, CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME from homeassistant.helpers.aiohttp_client import SERVER_SOFTWARE import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/noaa_tides/sensor.py b/homeassistant/components/noaa_tides/sensor.py index e5f31dba156..063a163a8ab 100644 --- a/homeassistant/components/noaa_tides/sensor.py +++ b/homeassistant/components/noaa_tides/sensor.py @@ -2,6 +2,7 @@ from datetime import datetime, timedelta import logging +from py_noaa import coops # pylint: disable=import-error import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -109,7 +110,6 @@ class NOAATidesAndCurrentsSensor(Entity): def update(self): """Get the latest data from NOAA Tides and Currents API.""" - from py_noaa import coops # pylint: disable=import-error begin = datetime.now() delta = timedelta(days=2) diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index 6ede7f18da7..8211fdc0828 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -1,20 +1,19 @@ """Provides functionality to notify people.""" import asyncio -import logging from functools import partial +import logging from typing import Optional import voluptuous as vol -from homeassistant.setup import async_prepare_setup_platform -from homeassistant.exceptions import HomeAssistantError -import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_NAME, CONF_PLATFORM +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.setup import async_prepare_setup_platform from homeassistant.util import slugify - # mypy: allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/notify/services.yaml b/homeassistant/components/notify/services.yaml index 1b7944cc7da..23b1c968c4a 100644 --- a/homeassistant/components/notify/services.yaml +++ b/homeassistant/components/notify/services.yaml @@ -16,16 +16,6 @@ notify: description: Extended information for notification. Optional depending on the platform. example: platform specific -html5_dismiss: - description: Dismiss a html5 notification. - fields: - target: - description: An array of targets. Optional. - example: ['my_phone', 'my_tablet'] - data: - description: Extended information of notification. Supports tag. Optional. - example: '{ "tag": "tagname" }' - apns_register: description: Registers a device to receive push notifications. fields: diff --git a/homeassistant/components/notion/.translations/da.json b/homeassistant/components/notion/.translations/da.json index 2373920effe..bf17b41d777 100644 --- a/homeassistant/components/notion/.translations/da.json +++ b/homeassistant/components/notion/.translations/da.json @@ -9,7 +9,7 @@ "user": { "data": { "password": "Adgangskode", - "username": "Brugernavn/e-mail adresse" + "username": "Brugernavn/e-mailadresse" }, "title": "Udfyld dine oplysninger" } diff --git a/homeassistant/components/notion/.translations/hu.json b/homeassistant/components/notion/.translations/hu.json index 2f7664cf74e..79878858ddc 100644 --- a/homeassistant/components/notion/.translations/hu.json +++ b/homeassistant/components/notion/.translations/hu.json @@ -11,7 +11,7 @@ "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v/Email C\u00edm" }, - "title": "T\u00f6ltse ki adatait" + "title": "T\u00f6ltsd ki az adataid" } } } diff --git a/homeassistant/components/notion/.translations/ru.json b/homeassistant/components/notion/.translations/ru.json index 6c1d5f5d8d7..15b540732a7 100644 --- a/homeassistant/components/notion/.translations/ru.json +++ b/homeassistant/components/notion/.translations/ru.json @@ -1,9 +1,9 @@ { "config": { "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.", + "identifier_exists": "\u0423\u0447\u0451\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_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c.", - "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\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u044c\u044e." + "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": { diff --git a/homeassistant/components/notion/binary_sensor.py b/homeassistant/components/notion/binary_sensor.py index 85495c040fa..5079348e821 100644 --- a/homeassistant/components/notion/binary_sensor.py +++ b/homeassistant/components/notion/binary_sensor.py @@ -17,7 +17,6 @@ from . import ( SENSOR_WINDOW_HINGED_VERTICAL, NotionEntity, ) - from .const import DATA_CLIENT, DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/notion/config_flow.py b/homeassistant/components/notion/config_flow.py index affda29e4d6..2af231d582e 100644 --- a/homeassistant/components/notion/config_flow.py +++ b/homeassistant/components/notion/config_flow.py @@ -1,4 +1,6 @@ """Config flow to configure the Notion integration.""" +from aionotion import async_get_client +from aionotion.errors import NotionError import voluptuous as vol from homeassistant import config_entries @@ -40,8 +42,6 @@ class NotionFlowHandler(config_entries.ConfigFlow): async def async_step_user(self, user_input=None): """Handle the start of the config flow.""" - from aionotion import async_get_client - from aionotion.errors import NotionError if not user_input: return await self._show_form() diff --git a/homeassistant/components/nsw_fuel_station/sensor.py b/homeassistant/components/nsw_fuel_station/sensor.py index a84aa554be9..b4cd7bd161e 100644 --- a/homeassistant/components/nsw_fuel_station/sensor.py +++ b/homeassistant/components/nsw_fuel_station/sensor.py @@ -3,11 +3,12 @@ import datetime import logging from typing import Optional +from nsw_fuel import FuelCheckClient, FuelCheckError import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.components.light import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ATTR_ATTRIBUTION +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -52,7 +53,6 @@ NOTIFICATION_TITLE = "NSW Fuel Station Sensor Setup" def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the NSW Fuel Station sensor.""" - from nsw_fuel import FuelCheckClient station_id = config[CONF_STATION_ID] fuel_types = config[CONF_FUEL_TYPES] @@ -97,7 +97,6 @@ class StationPriceData: @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Update the internal data using the API client.""" - from nsw_fuel import FuelCheckError if self._reference_data is None: try: 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 9a9679f9575..a04d2bd69b2 100644 --- a/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py +++ b/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py @@ -3,6 +3,7 @@ from datetime import timedelta import logging from typing import Optional +from aio_geojson_nsw_rfs_incidents import NswRuralFireServiceIncidentsFeedManager import voluptuous as vol from homeassistant.components.geo_location import PLATFORM_SCHEMA, GeolocationEvent @@ -14,11 +15,16 @@ from homeassistant.const import ( CONF_RADIUS, CONF_SCAN_INTERVAL, EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import callback -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send -from homeassistant.helpers.event import track_time_interval +from homeassistant.helpers import ConfigType, aiohttp_client, config_validation as cv +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.typing import HomeAssistantType _LOGGER = logging.getLogger(__name__) @@ -58,7 +64,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform( + hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None +): """Set up the NSW Rural Fire Service Feed platform.""" scan_interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) coordinates = ( @@ -68,30 +76,40 @@ def setup_platform(hass, config, add_entities, discovery_info=None): radius_in_km = config[CONF_RADIUS] categories = config.get(CONF_CATEGORIES) # Initialize the entity manager. - feed = NswRuralFireServiceFeedEntityManager( - hass, add_entities, scan_interval, coordinates, radius_in_km, categories + manager = NswRuralFireServiceFeedEntityManager( + hass, async_add_entities, scan_interval, coordinates, radius_in_km, categories ) - def start_feed_manager(event): + async def start_feed_manager(event): """Start feed manager.""" - feed.startup() + await manager.async_init() - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_feed_manager) + async def stop_feed_manager(event): + """Stop feed manager.""" + await manager.async_stop() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_feed_manager) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_feed_manager) + hass.async_create_task(manager.async_update()) class NswRuralFireServiceFeedEntityManager: """Feed Entity Manager for NSW Rural Fire Service GeoJSON feed.""" def __init__( - self, hass, add_entities, scan_interval, coordinates, radius_in_km, categories + self, + hass, + async_add_entities, + scan_interval, + coordinates, + radius_in_km, + categories, ): """Initialize the Feed Entity Manager.""" - from geojson_client.nsw_rural_fire_service_feed import ( - NswRuralFireServiceFeedManager, - ) - self._hass = hass - self._feed_manager = NswRuralFireServiceFeedManager( + websession = aiohttp_client.async_get_clientsession(hass) + self._feed_manager = NswRuralFireServiceIncidentsFeedManager( + websession, self._generate_entity, self._update_entity, self._remove_entity, @@ -99,37 +117,52 @@ class NswRuralFireServiceFeedEntityManager: filter_radius=radius_in_km, filter_categories=categories, ) - self._add_entities = add_entities + self._async_add_entities = async_add_entities self._scan_interval = scan_interval + self._track_time_remove_callback = None - def startup(self): - """Start up this manager.""" - self._feed_manager.update() - self._init_regular_updates() + async def async_init(self): + """Schedule initial and regular updates based on configured time interval.""" - def _init_regular_updates(self): - """Schedule regular updates at the specified interval.""" - track_time_interval( - self._hass, lambda now: self._feed_manager.update(), self._scan_interval + async def update(event_time): + """Update.""" + await self.async_update() + + # Trigger updates at regular intervals. + self._track_time_remove_callback = async_track_time_interval( + self._hass, update, self._scan_interval ) + _LOGGER.debug("Feed entity manager initialized") + + async def async_update(self): + """Refresh data.""" + await self._feed_manager.update() + _LOGGER.debug("Feed entity manager updated") + + async def async_stop(self): + """Stop this feed entity manager from refreshing.""" + if self._track_time_remove_callback: + self._track_time_remove_callback() + _LOGGER.debug("Feed entity manager stopped") + def get_entry(self, external_id): """Get feed entry by external id.""" return self._feed_manager.feed_entries.get(external_id) - def _generate_entity(self, external_id): + async def _generate_entity(self, external_id): """Generate new entity.""" new_entity = NswRuralFireServiceLocationEvent(self, external_id) # Add new entities to HA. - self._add_entities([new_entity], True) + self._async_add_entities([new_entity], True) - def _update_entity(self, external_id): + async def _update_entity(self, external_id): """Update entity.""" - dispatcher_send(self._hass, SIGNAL_UPDATE_ENTITY.format(external_id)) + async_dispatcher_send(self._hass, SIGNAL_UPDATE_ENTITY.format(external_id)) - def _remove_entity(self, external_id): + async def _remove_entity(self, external_id): """Remove entity.""" - dispatcher_send(self._hass, SIGNAL_DELETE_ENTITY.format(external_id)) + async_dispatcher_send(self._hass, SIGNAL_DELETE_ENTITY.format(external_id)) class NswRuralFireServiceLocationEvent(GeolocationEvent): @@ -169,11 +202,14 @@ class NswRuralFireServiceLocationEvent(GeolocationEvent): self._update_callback, ) + async def async_will_remove_from_hass(self) -> None: + """Call when entity will be removed from hass.""" + self._remove_signal_delete() + self._remove_signal_update() + @callback def _delete_callback(self): """Remove this entity.""" - self._remove_signal_delete() - self._remove_signal_update() self.hass.async_create_task(self.async_remove()) @callback diff --git a/homeassistant/components/nsw_rural_fire_service_feed/manifest.json b/homeassistant/components/nsw_rural_fire_service_feed/manifest.json index 3d16f0a57e3..7dd7d10d6be 100644 --- a/homeassistant/components/nsw_rural_fire_service_feed/manifest.json +++ b/homeassistant/components/nsw_rural_fire_service_feed/manifest.json @@ -3,7 +3,7 @@ "name": "Nsw rural fire service feed", "documentation": "https://www.home-assistant.io/integrations/nsw_rural_fire_service_feed", "requirements": [ - "geojson_client==0.4" + "aio_geojson_nsw_rfs_incidents==0.1" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/nuheat/climate.py b/homeassistant/components/nuheat/climate.py index 5a4e4e233d1..5cf9bd6fc58 100644 --- a/homeassistant/components/nuheat/climate.py +++ b/homeassistant/components/nuheat/climate.py @@ -22,7 +22,7 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle -from . import DOMAIN as NUHEAT_DOMAIN +from . import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -52,7 +52,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return temperature_unit = hass.config.units.temperature_unit - api, serial_numbers = hass.data[NUHEAT_DOMAIN] + api, serial_numbers = hass.data[DOMAIN] thermostats = [ NuHeatThermostat(api, serial_number, temperature_unit) for serial_number in serial_numbers @@ -75,7 +75,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): thermostat.schedule_update_ha_state(True) hass.services.register( - NUHEAT_DOMAIN, + DOMAIN, SERVICE_RESUME_PROGRAM, resume_program_set_service, schema=RESUME_PROGRAM_SCHEMA, diff --git a/homeassistant/components/nuheat/services.yaml b/homeassistant/components/nuheat/services.yaml index e69de29bb2d..6639fcd9898 100644 --- a/homeassistant/components/nuheat/services.yaml +++ b/homeassistant/components/nuheat/services.yaml @@ -0,0 +1,6 @@ +resume_program: + description: Resume the programmed schedule. + fields: + entity_id: + description: Name(s) of entities to change. + example: 'climate.kitchen' diff --git a/homeassistant/components/nuimo_controller/__init__.py b/homeassistant/components/nuimo_controller/__init__.py index 8fa3897b735..013c2caf23d 100644 --- a/homeassistant/components/nuimo_controller/__init__.py +++ b/homeassistant/components/nuimo_controller/__init__.py @@ -3,10 +3,12 @@ import logging import threading import time +# pylint: disable=import-error +from nuimo import NuimoController, NuimoDiscoveryManager import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_MAC, CONF_NAME, EVENT_HOMEASSISTANT_STOP +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -104,8 +106,6 @@ class NuimoThread(threading.Thread): def _attach(self): """Create a Nuimo object from MAC address or discovery.""" - # pylint: disable=import-error - from nuimo import NuimoController, NuimoDiscoveryManager if self._nuimo: self._nuimo.disconnect() diff --git a/homeassistant/components/nuimo_controller/services.yaml b/homeassistant/components/nuimo_controller/services.yaml index e69de29bb2d..ba537544a3b 100644 --- a/homeassistant/components/nuimo_controller/services.yaml +++ b/homeassistant/components/nuimo_controller/services.yaml @@ -0,0 +1,18 @@ +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 \ No newline at end of file diff --git a/homeassistant/components/nuki/services.yaml b/homeassistant/components/nuki/services.yaml index e69de29bb2d..1300b48e0dd 100644 --- a/homeassistant/components/nuki/services.yaml +++ b/homeassistant/components/nuki/services.yaml @@ -0,0 +1,10 @@ +lock_n_go: + description: "Nuki Lock 'n' Go" + fields: + entity_id: + description: Entity id of the Nuki lock. + example: 'lock.front_door' + unlatch: + description: Whether to unlatch the lock. + example: false + diff --git a/homeassistant/components/nut/sensor.py b/homeassistant/components/nut/sensor.py index db485734777..bdf0eaafc99 100644 --- a/homeassistant/components/nut/sensor.py +++ b/homeassistant/components/nut/sensor.py @@ -1,25 +1,26 @@ """Provides a sensor to track various status aspects of a UPS.""" -import logging from datetime import timedelta +import logging +from pynut2.nut2 import PyNUTClient, PyNUTError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( - CONF_HOST, - CONF_PORT, - CONF_NAME, - CONF_USERNAME, - CONF_PASSWORD, - TEMP_CELSIUS, - CONF_RESOURCES, - CONF_ALIAS, ATTR_STATE, - STATE_UNKNOWN, + CONF_ALIAS, + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_RESOURCES, + CONF_USERNAME, POWER_WATT, + STATE_UNKNOWN, + 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 @@ -188,7 +189,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): data.update(no_throttle=True) except data.pynuterror as err: _LOGGER.error( - "Failure while testing NUT status retrieval. " "Cannot continue setup: %s", + "Failure while testing NUT status retrieval. Cannot continue setup: %s", err, ) raise PlatformNotReady @@ -270,7 +271,6 @@ class PyNUTData: def __init__(self, host, port, alias, username, password): """Initialize the data object.""" - from pynut2.nut2 import PyNUTClient, PyNUTError self._host = host self._port = port diff --git a/homeassistant/components/nws/weather.py b/homeassistant/components/nws/weather.py index 23cf84411a3..c22700f1cf8 100644 --- a/homeassistant/components/nws/weather.py +++ b/homeassistant/components/nws/weather.py @@ -9,32 +9,32 @@ from pynws import SimpleNWS import voluptuous as vol from homeassistant.components.weather import ( - WeatherEntity, - PLATFORM_SCHEMA, ATTR_FORECAST_CONDITION, ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME, - ATTR_FORECAST_WIND_SPEED, ATTR_FORECAST_WIND_BEARING, + ATTR_FORECAST_WIND_SPEED, + PLATFORM_SCHEMA, + WeatherEntity, ) from homeassistant.const import ( CONF_API_KEY, - CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE, + CONF_NAME, LENGTH_KILOMETERS, LENGTH_METERS, LENGTH_MILES, PRESSURE_HPA, - PRESSURE_PA, PRESSURE_INHG, + PRESSURE_PA, TEMP_CELSIUS, TEMP_FAHRENHEIT, ) from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util import Throttle from homeassistant.util.distance import convert as convert_distance from homeassistant.util.pressure import convert as convert_pressure diff --git a/homeassistant/components/nx584/alarm_control_panel.py b/homeassistant/components/nx584/alarm_control_panel.py index d3d867ff378..7a064ef0d00 100644 --- a/homeassistant/components/nx584/alarm_control_panel.py +++ b/homeassistant/components/nx584/alarm_control_panel.py @@ -1,11 +1,16 @@ """Support for NX584 alarm control panels.""" import logging +from nx584 import client import requests import voluptuous as vol import homeassistant.components.alarm_control_panel as alarm from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, +) from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -52,7 +57,6 @@ class NX584Alarm(alarm.AlarmControlPanel): def __init__(self, hass, url, name): """Init the nx584 alarm panel.""" - from nx584 import client self._hass = hass self._name = name @@ -79,6 +83,11 @@ class NX584Alarm(alarm.AlarmControlPanel): """Return the state of the device.""" return self._state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY + def update(self): """Process new events from panel.""" try: diff --git a/homeassistant/components/nx584/binary_sensor.py b/homeassistant/components/nx584/binary_sensor.py index 6f1c66d8d87..f6006ff2de4 100644 --- a/homeassistant/components/nx584/binary_sensor.py +++ b/homeassistant/components/nx584/binary_sensor.py @@ -3,13 +3,14 @@ import logging import threading import time +from nx584 import client as nx584_client import requests import voluptuous as vol from homeassistant.components.binary_sensor import ( DEVICE_CLASSES, - BinarySensorDevice, PLATFORM_SCHEMA, + BinarySensorDevice, ) from homeassistant.const import CONF_HOST, CONF_PORT import homeassistant.helpers.config_validation as cv @@ -39,7 +40,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the NX584 binary sensor platform.""" - from nx584 import client as nx584_client host = config.get(CONF_HOST) port = config.get(CONF_PORT) diff --git a/homeassistant/components/nzbget/sensor.py b/homeassistant/components/nzbget/sensor.py index 20b49a492f3..3556c88a6da 100644 --- a/homeassistant/components/nzbget/sensor.py +++ b/homeassistant/components/nzbget/sensor.py @@ -18,6 +18,7 @@ SENSOR_TYPES = { "download_rate": ["DownloadRate", "Speed", "MB/s"], "download_size": ["DownloadedSizeMB", "Size", "MB"], "free_disk_space": ["FreeDiskSpaceMB", "Disk Free", "MB"], + "post_job_count": ["PostJobCount", "Post Processing Jobs", "Jobs"], "post_paused": ["PostPaused", "Post Processing Paused", None], "remaining_size": ["RemainingSizeMB", "Queue Size", "MB"], "uptime": ["UpTimeSec", "Uptime", "min"], diff --git a/homeassistant/components/obihai/sensor.py b/homeassistant/components/obihai/sensor.py index 89bfee7d4ee..13d09de0542 100644 --- a/homeassistant/components/obihai/sensor.py +++ b/homeassistant/components/obihai/sensor.py @@ -1,10 +1,9 @@ """Support for Obihai Sensors.""" +from datetime import timedelta import logging -from datetime import timedelta -import voluptuous as vol - from pyobihai import PyObihai +import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( @@ -13,10 +12,8 @@ from homeassistant.const import ( CONF_USERNAME, DEVICE_CLASS_TIMESTAMP, ) - -from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv - +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/octoprint/__init__.py b/homeassistant/components/octoprint/__init__.py index bc71b1a5911..7564330e499 100644 --- a/homeassistant/components/octoprint/__init__.py +++ b/homeassistant/components/octoprint/__init__.py @@ -2,23 +2,23 @@ import logging import time +from aiohttp.hdrs import CONTENT_TYPE import requests import voluptuous as vol -from aiohttp.hdrs import CONTENT_TYPE from homeassistant.components.discovery import SERVICE_OCTOPRINT from homeassistant.const import ( CONF_API_KEY, + CONF_BINARY_SENSORS, CONF_HOST, - CONTENT_TYPE_JSON, + CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_PATH, CONF_PORT, - CONF_SSL, - TEMP_CELSIUS, - CONF_MONITORED_CONDITIONS, CONF_SENSORS, - CONF_BINARY_SENSORS, + CONF_SSL, + CONTENT_TYPE_JSON, + TEMP_CELSIUS, ) from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/ohmconnect/sensor.py b/homeassistant/components/ohmconnect/sensor.py index a9606e25bad..490ebbe75b3 100644 --- a/homeassistant/components/ohmconnect/sensor.py +++ b/homeassistant/components/ohmconnect/sensor.py @@ -66,9 +66,7 @@ class OhmconnectSensor(Entity): def update(self): """Get the latest data from OhmConnect.""" try: - url = ("https://login.ohmconnect.com" "/verify-ohm-hour/{}").format( - self._ohmid - ) + url = "https://login.ohmconnect.com/verify-ohm-hour/{}".format(self._ohmid) response = requests.get(url, timeout=10) root = ET.fromstring(response.text) diff --git a/homeassistant/components/onboarding/__init__.py b/homeassistant/components/onboarding/__init__.py index 5527f5f2abe..bedfa703a9b 100644 --- a/homeassistant/components/onboarding/__init__.py +++ b/homeassistant/components/onboarding/__init__.py @@ -1,9 +1,10 @@ """Support to help onboard new users.""" from homeassistant.core import callback -from homeassistant.loader import bind_hass from homeassistant.helpers.storage import Store +from homeassistant.loader import bind_hass -from .const import DOMAIN, STEP_USER, STEPS, STEP_INTEGRATION, STEP_CORE_CONFIG +from . import views +from .const import DOMAIN, STEP_CORE_CONFIG, STEP_INTEGRATION, STEP_USER, STEPS STORAGE_KEY = DOMAIN STORAGE_VERSION = 3 @@ -64,8 +65,6 @@ async def async_setup(hass, config): hass.data[DOMAIN] = data - from . import views - await views.async_setup(hass, data, store) return True diff --git a/homeassistant/components/onboarding/manifest.json b/homeassistant/components/onboarding/manifest.json index 2febfc481e0..8e525ff0baa 100644 --- a/homeassistant/components/onboarding/manifest.json +++ b/homeassistant/components/onboarding/manifest.json @@ -3,11 +3,6 @@ "name": "Onboarding", "documentation": "https://www.home-assistant.io/integrations/onboarding", "requirements": [], - "dependencies": [ - "auth", - "http" - ], - "codeowners": [ - "@home-assistant/core" - ] + "dependencies": ["auth", "http", "person"], + "codeowners": ["@home-assistant/core"] } diff --git a/homeassistant/components/onboarding/views.py b/homeassistant/components/onboarding/views.py index 2e79393fe42..8eac430ac49 100644 --- a/homeassistant/components/onboarding/views.py +++ b/homeassistant/components/onboarding/views.py @@ -8,12 +8,12 @@ from homeassistant.components.http.view import HomeAssistantView from homeassistant.core import callback from .const import ( + DEFAULT_AREAS, DOMAIN, + STEP_CORE_CONFIG, + STEP_INTEGRATION, STEP_USER, STEPS, - DEFAULT_AREAS, - STEP_INTEGRATION, - STEP_CORE_CONFIG, ) diff --git a/homeassistant/components/onewire/manifest.json b/homeassistant/components/onewire/manifest.json index 2d8c6c71071..6687a10e3d7 100644 --- a/homeassistant/components/onewire/manifest.json +++ b/homeassistant/components/onewire/manifest.json @@ -2,7 +2,11 @@ "domain": "onewire", "name": "Onewire", "documentation": "https://www.home-assistant.io/integrations/onewire", - "requirements": [], + "requirements": [ + "pyownet==0.10.0.post1" + ], "dependencies": [], - "codeowners": [] + "codeowners": [ + "@garbled1" + ] } diff --git a/homeassistant/components/onewire/sensor.py b/homeassistant/components/onewire/sensor.py index e0a47a45b25..6405cb05adc 100644 --- a/homeassistant/components/onewire/sensor.py +++ b/homeassistant/components/onewire/sensor.py @@ -1,15 +1,16 @@ """Support for 1-Wire environment sensors.""" +from glob import glob +import logging import os import time -import logging -from glob import glob +from pyownet import protocol import voluptuous as vol +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_HOST, CONF_PORT, TEMP_CELSIUS import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.const import TEMP_CELSIUS -from homeassistant.components.sensor import PLATFORM_SCHEMA _LOGGER = logging.getLogger(__name__) @@ -30,33 +31,122 @@ DEVICE_SENSORS = { "28": {"temperature": "temperature"}, "3B": {"temperature": "temperature"}, "42": {"temperature": "temperature"}, + "1D": {"counter_a": "counter.A", "counter_b": "counter.B"}, + "EF": {"HobbyBoard": "special"}, +} + +# EF sensors are usually hobbyboards specialized sensors. +# These can only be read by OWFS. Currently this driver only supports them +# via owserver (network protocol) + +HOBBYBOARD_EF = { + "HobbyBoards_EF": { + "humidity": "humidity/humidity_corrected", + "humidity_raw": "humidity/humidity_raw", + "temperature": "humidity/temperature", + }, + "HB_MOISTURE_METER": { + "moisture_0": "moisture/sensor.0", + "moisture_1": "moisture/sensor.1", + "moisture_2": "moisture/sensor.2", + "moisture_3": "moisture/sensor.3", + }, } SENSOR_TYPES = { "temperature": ["temperature", TEMP_CELSIUS], "humidity": ["humidity", "%"], + "humidity_raw": ["humidity", "%"], "pressure": ["pressure", "mb"], "illuminance": ["illuminance", "lux"], + "wetness_0": ["wetness", "%"], + "wetness_1": ["wetness", "%"], + "wetness_2": ["wetness", "%"], + "wetness_3": ["wetness", "%"], + "moisture_0": ["moisture", "cb"], + "moisture_1": ["moisture", "cb"], + "moisture_2": ["moisture", "cb"], + "moisture_3": ["moisture", "cb"], + "counter_a": ["counter", "count"], + "counter_b": ["counter", "count"], + "HobbyBoard": ["none", "none"], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAMES): {cv.string: cv.string}, vol.Optional(CONF_MOUNT_DIR, default=DEFAULT_MOUNT_DIR): cv.string, + vol.Optional(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=4304): cv.port, } ) +def hb_info_from_type(dev_type="std"): + """Return the proper info array for the device type.""" + if "std" in dev_type: + return DEVICE_SENSORS + if "HobbyBoard" in dev_type: + return HOBBYBOARD_EF + + def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the one wire Sensors.""" - base_dir = config.get(CONF_MOUNT_DIR) + base_dir = config[CONF_MOUNT_DIR] + owport = config[CONF_PORT] + owhost = config.get(CONF_HOST) devs = [] device_names = {} if "names" in config: if isinstance(config["names"], dict): device_names = config["names"] - if base_dir == DEFAULT_MOUNT_DIR: + # We have an owserver on a remote(or local) host/port + if owhost: + try: + owproxy = protocol.proxy(host=owhost, port=owport) + devices = owproxy.dir() + except protocol.Error as exc: + _LOGGER.error( + "Cannot connect to owserver on %s:%d, got: %s", owhost, owport, exc + ) + devices = [] + for device in devices: + _LOGGER.debug("found device: %s", device) + family = owproxy.read(f"{device}family").decode() + dev_type = "std" + if "EF" in family: + dev_type = "HobbyBoard" + family = owproxy.read(f"{device}type").decode() + + if family not in hb_info_from_type(dev_type): + _LOGGER.warning( + "Ignoring unknown family (%s) of sensor found for device: %s", + family, + device, + ) + continue + for sensor_key, sensor_value in hb_info_from_type(dev_type)[family].items(): + if "moisture" in sensor_key: + s_id = sensor_key.split("_")[1] + is_leaf = int( + owproxy.read(f"{device}moisture/is_leaf.{s_id}").decode() + ) + if is_leaf: + sensor_key = f"wetness_{id}" + sensor_id = os.path.split(os.path.split(device)[0])[1] + device_file = os.path.join(os.path.split(device)[0], sensor_value) + devs.append( + OneWireProxy( + device_names.get(sensor_id, sensor_id), + device_file, + sensor_key, + owproxy, + ) + ) + + # We have a raw GPIO ow sensor on a Pi + elif base_dir == DEFAULT_MOUNT_DIR: for device_family in DEVICE_SENSORS: for device_folder in glob(os.path.join(base_dir, device_family + "[.-]*")): sensor_id = os.path.split(device_folder)[1] @@ -68,10 +158,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None): "temperature", ) ) + + # We have an owfs mounted else: for family_file_path in glob(os.path.join(base_dir, "*", "family")): with open(family_file_path, "r") as family_file: family = family_file.read() + if "EF" in family: + continue if family in DEVICE_SENSORS: for sensor_key, sensor_value in DEVICE_SENSORS[family].items(): sensor_id = os.path.split(os.path.split(family_file_path)[0])[1] @@ -121,6 +215,8 @@ class OneWire(Entity): @property def state(self): """Return the state of the sensor.""" + if "count" in self._unit_of_measurement: + return int(self._state) return self._state @property @@ -129,6 +225,34 @@ class OneWire(Entity): return self._unit_of_measurement +class OneWireProxy(OneWire): + """Implementation of a One wire Sensor through owserver.""" + + def __init__(self, name, device_file, sensor_type, owproxy): + """Initialize the onewire sensor via owserver.""" + super().__init__(name, device_file, sensor_type) + self._owproxy = owproxy + + def _read_value_ownet(self): + """Read a value from the owserver.""" + if self._owproxy: + return self._owproxy.read(self._device_file).decode().lstrip() + return None + + def update(self): + """Get the latest data from the device.""" + value = None + value_read = False + try: + value_read = self._read_value_ownet() + except protocol.Error as exc: + _LOGGER.error("Owserver failure in read(), got: %s", exc) + if value_read: + value = round(float(value_read), 1) + + self._state = value + + class OneWireDirect(OneWire): """Implementation of an One wire Sensor directly connected to RPI GPIO.""" diff --git a/homeassistant/components/onkyo/media_player.py b/homeassistant/components/onkyo/media_player.py index 86f0f418c3f..93107b2eb48 100644 --- a/homeassistant/components/onkyo/media_player.py +++ b/homeassistant/components/onkyo/media_player.py @@ -2,12 +2,13 @@ import logging from typing import List -import voluptuous as vol import eiscp from eiscp import eISCP +import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( + DOMAIN, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_SELECT_SOURCE, @@ -16,14 +17,13 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, - DOMAIN, ) from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, - ATTR_ENTITY_ID, ) import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index affbbb62338..3f244530dca 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -278,6 +278,10 @@ class ONVIFHassCamera(Camera): _LOGGER.debug("Retrieving stream uri") + # Fix Onvif setup error on Goke GK7102 based IP camera + # where we need to recreate media_service #26781 + media_service = self._camera.create_media_service() + req = media_service.create_type("GetStreamUri") req.ProfileToken = profiles[self._profile_index].token req.StreamSetup = { diff --git a/homeassistant/components/openalpr_cloud/image_processing.py b/homeassistant/components/openalpr_cloud/image_processing.py index 66081d9b271..64ba0d83844 100644 --- a/homeassistant/components/openalpr_cloud/image_processing.py +++ b/homeassistant/components/openalpr_cloud/image_processing.py @@ -1,26 +1,26 @@ """Component that will help set the OpenALPR cloud for ALPR processing.""" import asyncio -import logging from base64 import b64encode +import logging import aiohttp import async_timeout import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.core import split_entity_id -from homeassistant.const import CONF_API_KEY from homeassistant.components.image_processing import ( - PLATFORM_SCHEMA, CONF_CONFIDENCE, - CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME, + CONF_SOURCE, + PLATFORM_SCHEMA, ) from homeassistant.components.openalpr_local.image_processing import ( ImageProcessingAlprEntity, ) +from homeassistant.const import CONF_API_KEY +from homeassistant.core import split_entity_id from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/openalpr_local/image_processing.py b/homeassistant/components/openalpr_local/image_processing.py index 32a08b53165..df7b235a224 100644 --- a/homeassistant/components/openalpr_local/image_processing.py +++ b/homeassistant/components/openalpr_local/image_processing.py @@ -6,19 +6,19 @@ import re import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.core import split_entity_id, callback -from homeassistant.const import CONF_REGION from homeassistant.components.image_processing import ( - PLATFORM_SCHEMA, - ImageProcessingEntity, + ATTR_CONFIDENCE, + ATTR_ENTITY_ID, CONF_CONFIDENCE, - CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME, - ATTR_ENTITY_ID, - ATTR_CONFIDENCE, + CONF_SOURCE, + PLATFORM_SCHEMA, + ImageProcessingEntity, ) +from homeassistant.const import CONF_REGION +from homeassistant.core import callback, split_entity_id +import homeassistant.helpers.config_validation as cv from homeassistant.util.async_ import run_callback_threadsafe _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/opencv/manifest.json b/homeassistant/components/opencv/manifest.json index bd82da000cf..4fe6026dfef 100644 --- a/homeassistant/components/opencv/manifest.json +++ b/homeassistant/components/opencv/manifest.json @@ -2,10 +2,7 @@ "domain": "opencv", "name": "Opencv", "documentation": "https://www.home-assistant.io/integrations/opencv", - "requirements": [ - "numpy==1.17.3", - "opencv-python-headless==4.1.1.26" - ], + "requirements": ["numpy==1.17.4", "opencv-python-headless==4.1.2.30"], "dependencies": [], "codeowners": [] -} \ No newline at end of file +} diff --git a/homeassistant/components/openexchangerates/sensor.py b/homeassistant/components/openexchangerates/sensor.py index 9b79eb564e0..cc6da709dff 100644 --- a/homeassistant/components/openexchangerates/sensor.py +++ b/homeassistant/components/openexchangerates/sensor.py @@ -7,11 +7,11 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_API_KEY, - CONF_NAME, - CONF_BASE, - CONF_QUOTE, ATTR_ATTRIBUTION, + CONF_API_KEY, + CONF_BASE, + CONF_NAME, + CONF_QUOTE, ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity diff --git a/homeassistant/components/opengarage/cover.py b/homeassistant/components/opengarage/cover.py index 1243a9164fd..26a69fa11af 100644 --- a/homeassistant/components/opengarage/cover.py +++ b/homeassistant/components/opengarage/cover.py @@ -5,20 +5,22 @@ import requests import voluptuous as vol from homeassistant.components.cover import ( - CoverDevice, DEVICE_CLASS_GARAGE, PLATFORM_SCHEMA, - SUPPORT_OPEN, SUPPORT_CLOSE, + SUPPORT_OPEN, + CoverDevice, ) from homeassistant.const import ( - CONF_NAME, - STATE_CLOSED, - STATE_OPEN, CONF_COVERS, CONF_HOST, + CONF_NAME, CONF_PORT, + CONF_SSL, + CONF_VERIFY_SSL, + STATE_CLOSED, STATE_CLOSING, + STATE_OPEN, STATE_OPENING, ) import homeassistant.helpers.config_validation as cv @@ -42,6 +44,8 @@ COVER_SCHEMA = vol.Schema( vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_SSL, default=False): cv.boolean, + vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, } ) @@ -60,6 +64,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): CONF_NAME: device_config.get(CONF_NAME), CONF_HOST: device_config.get(CONF_HOST), CONF_PORT: device_config.get(CONF_PORT), + CONF_SSL: device_config.get(CONF_SSL), + CONF_VERIFY_SSL: device_config.get(CONF_VERIFY_SSL), CONF_DEVICE_KEY: device_config.get(CONF_DEVICE_KEY), } @@ -73,13 +79,16 @@ class OpenGarageCover(CoverDevice): def __init__(self, args): """Initialize the cover.""" - self.opengarage_url = "http://{}:{}".format(args[CONF_HOST], args[CONF_PORT]) + self.opengarage_url = "{}://{}:{}".format( + "https" if args[CONF_SSL] else "http", args[CONF_HOST], args[CONF_PORT] + ) self._name = args[CONF_NAME] self._device_key = args[CONF_DEVICE_KEY] self._state = None self._state_before_move = None self._device_state_attributes = {} self._available = True + self._verify_ssl = args[CONF_VERIFY_SSL] @property def name(self): @@ -155,7 +164,9 @@ class OpenGarageCover(CoverDevice): result = -1 try: result = requests.get( - f"{self.opengarage_url}/cc?dkey={self._device_key}&click=1", timeout=10 + f"{self.opengarage_url}/cc?dkey={self._device_key}&click=1", + timeout=10, + verify=self._verify_ssl, ).json()["result"] except requests.exceptions.RequestException as ex: _LOGGER.error( diff --git a/homeassistant/components/openhome/manifest.json b/homeassistant/components/openhome/manifest.json index 2ec58b86125..7640c388747 100644 --- a/homeassistant/components/openhome/manifest.json +++ b/homeassistant/components/openhome/manifest.json @@ -2,9 +2,7 @@ "domain": "openhome", "name": "Openhome", "documentation": "https://www.home-assistant.io/integrations/openhome", - "requirements": [ - "openhomedevice==0.4.2" - ], + "requirements": ["openhomedevice==0.6.3"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/openhome/media_player.py b/homeassistant/components/openhome/media_player.py index 0443187ac63..222c1d87ec0 100644 --- a/homeassistant/components/openhome/media_player.py +++ b/homeassistant/components/openhome/media_player.py @@ -1,6 +1,8 @@ """Support for Openhome Devices.""" import logging +from openhomedevice.Device import Device + from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, @@ -17,14 +19,7 @@ from homeassistant.components.media_player.const import ( ) from homeassistant.const import STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING -SUPPORT_OPENHOME = ( - SUPPORT_SELECT_SOURCE - | SUPPORT_VOLUME_STEP - | SUPPORT_VOLUME_MUTE - | SUPPORT_VOLUME_SET - | SUPPORT_TURN_OFF - | SUPPORT_TURN_ON -) +SUPPORT_OPENHOME = SUPPORT_SELECT_SOURCE | SUPPORT_TURN_OFF | SUPPORT_TURN_ON _LOGGER = logging.getLogger(__name__) @@ -33,7 +28,6 @@ DEVICES = [] def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Openhome platform.""" - from openhomedevice.Device import Device if not discovery_info: return True @@ -79,14 +73,19 @@ class OpenhomeDevice(MediaPlayerDevice): self._in_standby = self._device.IsInStandby() self._transport_state = self._device.TransportState() self._track_information = self._device.TrackInfo() - self._volume_level = self._device.VolumeLevel() - self._volume_muted = self._device.IsMuted() self._source = self._device.Source() self._name = self._device.Room().decode("utf-8") self._supported_features = SUPPORT_OPENHOME source_index = {} source_names = list() + if self._device.VolumeEnabled(): + self._supported_features |= ( + SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET + ) + self._volume_level = self._device.VolumeLevel() + self._volume_muted = self._device.IsMuted() + for source in self._device.Sources(): source_names.append(source["name"]) source_index[source["name"]] = source["index"] diff --git a/homeassistant/components/opensensemap/air_quality.py b/homeassistant/components/opensensemap/air_quality.py index d525e807aed..cf27f86cc9f 100644 --- a/homeassistant/components/opensensemap/air_quality.py +++ b/homeassistant/components/opensensemap/air_quality.py @@ -2,6 +2,8 @@ from datetime import timedelta import logging +from opensensemap_api import OpenSenseMap +from opensensemap_api.exceptions import OpenSenseMapError import voluptuous as vol from homeassistant.components.air_quality import PLATFORM_SCHEMA, AirQualityEntity @@ -26,7 +28,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the openSenseMap air quality platform.""" - from opensensemap_api import OpenSenseMap name = config.get(CONF_NAME) station_id = config[CONF_STATION_ID] @@ -88,7 +89,6 @@ class OpenSenseMapData: @Throttle(SCAN_INTERVAL) async def async_update(self): """Get the latest data from the Pi-hole.""" - from opensensemap_api.exceptions import OpenSenseMapError try: await self.api.get_data() diff --git a/homeassistant/components/opensky/sensor.py b/homeassistant/components/opensky/sensor.py index 0c17daa0ab4..d916d9f7f29 100644 --- a/homeassistant/components/opensky/sensor.py +++ b/homeassistant/components/opensky/sensor.py @@ -1,26 +1,25 @@ """Sensor for the Open Sky Network.""" -import logging from datetime import timedelta +import logging import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, - CONF_LATITUDE, - CONF_LONGITUDE, - CONF_RADIUS, ATTR_ATTRIBUTION, ATTR_LATITUDE, ATTR_LONGITUDE, + 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 distance as util_distance -from homeassistant.util import location as util_location +from homeassistant.util import distance as util_distance, location as util_location _LOGGER = logging.getLogger(__name__) @@ -41,7 +40,7 @@ EVENT_OPENSKY_EXIT = f"{DOMAIN}_exit" SCAN_INTERVAL = timedelta(seconds=12) # opensky public limit is 10 seconds OPENSKY_ATTRIBUTION = ( - "Information provided by the OpenSky Network " "(https://opensky-network.org)" + "Information provided by the OpenSky Network (https://opensky-network.org)" ) OPENSKY_API_URL = "https://opensky-network.org/api/states/all" OPENSKY_API_FIELDS = [ diff --git a/homeassistant/components/opentherm_gw/.translations/da.json b/homeassistant/components/opentherm_gw/.translations/da.json index 152e38a5bba..743adb715f6 100644 --- a/homeassistant/components/opentherm_gw/.translations/da.json +++ b/homeassistant/components/opentherm_gw/.translations/da.json @@ -3,14 +3,17 @@ "error": { "already_configured": "Gateway allerede konfigureret", "id_exists": "Gateway-id findes allerede", - "serial_error": "Fejl ved tilslutning til enheden" + "serial_error": "Fejl ved tilslutning til enheden", + "timeout": "Forbindelsesfors\u00f8g fik timeout" }, "step": { "init": { "data": { - "device": "Sti eller URL", - "id": "ID", - "name": "Navn" + "device": "Sti eller webadresse", + "floor_temperature": "Gulvklima-temperatur", + "id": "Id", + "name": "Navn", + "precision": "Klimatemperatur-pr\u00e6cision" }, "title": "OpenTherm Gateway" } diff --git a/homeassistant/components/opentherm_gw/__init__.py b/homeassistant/components/opentherm_gw/__init__.py index 643f80ae8f9..c6cf14bfdce 100644 --- a/homeassistant/components/opentherm_gw/__init__.py +++ b/homeassistant/components/opentherm_gw/__init__.py @@ -1,15 +1,16 @@ """Support for OpenTherm Gateway devices.""" +import asyncio +from datetime import date, datetime import logging -from datetime import datetime, date import pyotgw import pyotgw.vars as gw_vars import voluptuous as vol -from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.components.binary_sensor import DOMAIN as COMP_BINARY_SENSOR from homeassistant.components.climate import DOMAIN as COMP_CLIMATE from homeassistant.components.sensor import DOMAIN as COMP_SENSOR +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_DATE, ATTR_ID, @@ -24,14 +25,13 @@ from homeassistant.const import ( PRECISION_TENTHS, PRECISION_WHOLE, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send -import homeassistant.helpers.config_validation as cv - from .const import ( + ATTR_DHW_OVRD, ATTR_GW_ID, ATTR_LEVEL, - ATTR_DHW_OVRD, CONF_CLIMATE, CONF_FLOOR_TEMP, CONF_PRECISION, @@ -41,15 +41,14 @@ from .const import ( SERVICE_RESET_GATEWAY, SERVICE_SET_CLOCK, SERVICE_SET_CONTROL_SETPOINT, - SERVICE_SET_HOT_WATER_OVRD, SERVICE_SET_GPIO_MODE, + SERVICE_SET_HOT_WATER_OVRD, SERVICE_SET_LED_MODE, SERVICE_SET_MAX_MOD, SERVICE_SET_OAT, SERVICE_SET_SB_TEMP, ) - _LOGGER = logging.getLogger(__name__) CLIMATE_SCHEMA = vol.Schema( @@ -344,6 +343,18 @@ def register_services(hass): ) +async def async_unload_entry(hass, entry): + """Cleanup and disconnect from gateway.""" + await asyncio.gather( + hass.config_entries.async_forward_entry_unload(entry, COMP_BINARY_SENSOR), + hass.config_entries.async_forward_entry_unload(entry, COMP_CLIMATE), + hass.config_entries.async_forward_entry_unload(entry, COMP_SENSOR), + ) + gateway = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][entry.data[CONF_ID]] + await gateway.cleanup() + return True + + class OpenThermGatewayDevice: """OpenTherm Gateway device class.""" @@ -358,18 +369,21 @@ class OpenThermGatewayDevice: self.update_signal = f"{DATA_OPENTHERM_GW}_{self.gw_id}_update" self.options_update_signal = f"{DATA_OPENTHERM_GW}_{self.gw_id}_options_update" self.gateway = pyotgw.pyotgw() + self.gw_version = None + + async def cleanup(self, event=None): + """Reset overrides on the gateway.""" + await self.gateway.set_control_setpoint(0) + await self.gateway.set_max_relative_mod("-") + await self.gateway.disconnect() async def connect_and_subscribe(self): """Connect to serial device and subscribe report handler.""" - await self.gateway.connect(self.hass.loop, self.device_path) + self.status = await self.gateway.connect(self.hass.loop, self.device_path) _LOGGER.debug("Connected to OpenTherm Gateway at %s", self.device_path) + self.gw_version = self.status.get(gw_vars.OTGW_BUILD) - async def cleanup(event): - """Reset overrides on the gateway.""" - await self.gateway.set_control_setpoint(0) - await self.gateway.set_max_relative_mod("-") - - self.hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, cleanup) + self.hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, self.cleanup) async def handle_report(status): """Handle reports from the OpenTherm Gateway.""" diff --git a/homeassistant/components/opentherm_gw/binary_sensor.py b/homeassistant/components/opentherm_gw/binary_sensor.py index 36867feda61..eff11554a39 100644 --- a/homeassistant/components/opentherm_gw/binary_sensor.py +++ b/homeassistant/components/opentherm_gw/binary_sensor.py @@ -7,9 +7,9 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import async_generate_entity_id +from . import DOMAIN from .const import BINARY_SENSOR_INFO, DATA_GATEWAYS, DATA_OPENTHERM_GW - _LOGGER = logging.getLogger(__name__) @@ -44,14 +44,27 @@ class OpenThermBinarySensor(BinarySensorDevice): self._state = None self._device_class = device_class self._friendly_name = friendly_name_format.format(gw_dev.name) + self._unsub_updates = None async def async_added_to_hass(self): """Subscribe to updates from the component.""" _LOGGER.debug("Added OpenTherm Gateway binary sensor %s", self._friendly_name) - async_dispatcher_connect( + self._unsub_updates = async_dispatcher_connect( self.hass, self._gateway.update_signal, self.receive_report ) + async def async_will_remove_from_hass(self): + """Unsubscribe from updates from the component.""" + _LOGGER.debug( + "Removing OpenTherm Gateway binary sensor %s", self._friendly_name + ) + self._unsub_updates() + + @property + def entity_registry_enabled_default(self): + """Disable binary_sensors by default.""" + return False + @callback def receive_report(self, status): """Handle status updates from the component.""" @@ -63,6 +76,22 @@ class OpenThermBinarySensor(BinarySensorDevice): """Return the friendly name.""" return self._friendly_name + @property + def device_info(self): + """Return device info.""" + return { + "identifiers": {(DOMAIN, self._gateway.gw_id)}, + "name": self._gateway.name, + "manufacturer": "Schelte Bron", + "model": "OpenTherm Gateway", + "sw_version": self._gateway.gw_version, + } + + @property + def unique_id(self): + """Return a unique ID.""" + return f"{self._gateway.gw_id}-{self._var}" + @property def is_on(self): """Return true if the binary sensor is on.""" diff --git a/homeassistant/components/opentherm_gw/climate.py b/homeassistant/components/opentherm_gw/climate.py index 44f143d64da..2db20662a77 100644 --- a/homeassistant/components/opentherm_gw/climate.py +++ b/homeassistant/components/opentherm_gw/climate.py @@ -3,17 +3,17 @@ import logging from pyotgw import vars as gw_vars -from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate import ENTITY_ID_FORMAT, ClimateDevice from homeassistant.components.climate.const import ( CURRENT_HVAC_COOL, CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, HVAC_MODE_COOL, HVAC_MODE_HEAT, - SUPPORT_TARGET_TEMPERATURE, PRESET_AWAY, PRESET_NONE, SUPPORT_PRESET_MODE, + SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ( ATTR_TEMPERATURE, @@ -25,12 +25,16 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +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 - _LOGGER = logging.getLogger(__name__) +DEFAULT_FLOOR_TEMP = False +DEFAULT_PRECISION = None + SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE @@ -53,9 +57,12 @@ class OpenThermClimate(ClimateDevice): def __init__(self, gw_dev, options): """Initialize the device.""" self._gateway = gw_dev + self.entity_id = async_generate_entity_id( + ENTITY_ID_FORMAT, gw_dev.gw_id, hass=gw_dev.hass + ) self.friendly_name = gw_dev.name - self.floor_temp = options[CONF_FLOOR_TEMP] - self.temp_precision = options.get(CONF_PRECISION) + self.floor_temp = options.get(CONF_FLOOR_TEMP, DEFAULT_FLOOR_TEMP) + self.temp_precision = options.get(CONF_PRECISION, DEFAULT_PRECISION) self._current_operation = None self._current_temperature = None self._hvac_mode = HVAC_MODE_HEAT @@ -65,24 +72,32 @@ class OpenThermClimate(ClimateDevice): self._away_mode_b = None self._away_state_a = False self._away_state_b = False + self._unsub_options = None + self._unsub_updates = None @callback def update_options(self, entry): """Update climate entity options.""" self.floor_temp = entry.options[CONF_FLOOR_TEMP] - self.temp_precision = entry.options.get(CONF_PRECISION) + self.temp_precision = entry.options[CONF_PRECISION] self.async_schedule_update_ha_state() async def async_added_to_hass(self): """Connect to the OpenTherm Gateway device.""" _LOGGER.debug("Added OpenTherm Gateway climate device %s", self.friendly_name) - async_dispatcher_connect( + self._unsub_updates = async_dispatcher_connect( self.hass, self._gateway.update_signal, self.receive_report ) - async_dispatcher_connect( + self._unsub_options = async_dispatcher_connect( self.hass, self._gateway.options_update_signal, self.update_options ) + async def async_will_remove_from_hass(self): + """Unsubscribe from updates from the component.""" + _LOGGER.debug("Removing OpenTherm Gateway climate %s", self.friendly_name) + self._unsub_options() + self._unsub_updates() + @callback def receive_report(self, status): """Receive and handle a new report from the Gateway.""" @@ -136,6 +151,17 @@ class OpenThermClimate(ClimateDevice): """Return the friendly name.""" return self.friendly_name + @property + def device_info(self): + """Return device info.""" + return { + "identifiers": {(DOMAIN, self._gateway.gw_id)}, + "name": self._gateway.name, + "manufacturer": "Schelte Bron", + "model": "OpenTherm Gateway", + "sw_version": self._gateway.gw_version, + } + @property def unique_id(self): """Return a unique ID.""" diff --git a/homeassistant/components/opentherm_gw/config_flow.py b/homeassistant/components/opentherm_gw/config_flow.py index 2d7a65bbd84..b52641105e4 100644 --- a/homeassistant/components/opentherm_gw/config_flow.py +++ b/homeassistant/components/opentherm_gw/config_flow.py @@ -1,8 +1,8 @@ """OpenTherm Gateway config flow.""" import asyncio -from serial import SerialException import pyotgw +from serial import SerialException import voluptuous as vol from homeassistant import config_entries @@ -15,7 +15,6 @@ from homeassistant.const import ( PRECISION_WHOLE, ) from homeassistant.core import callback - import homeassistant.helpers.config_validation as cv from . import DOMAIN diff --git a/homeassistant/components/opentherm_gw/sensor.py b/homeassistant/components/opentherm_gw/sensor.py index c77a73cd180..3739f77e69d 100644 --- a/homeassistant/components/opentherm_gw/sensor.py +++ b/homeassistant/components/opentherm_gw/sensor.py @@ -7,9 +7,9 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity, async_generate_entity_id +from . import DOMAIN from .const import DATA_GATEWAYS, DATA_OPENTHERM_GW, SENSOR_INFO - _LOGGER = logging.getLogger(__name__) @@ -47,14 +47,25 @@ class OpenThermSensor(Entity): self._device_class = device_class self._unit = unit self._friendly_name = friendly_name_format.format(gw_dev.name) + self._unsub_updates = None async def async_added_to_hass(self): """Subscribe to updates from the component.""" _LOGGER.debug("Added OpenTherm Gateway sensor %s", self._friendly_name) - async_dispatcher_connect( + self._unsub_updates = async_dispatcher_connect( self.hass, self._gateway.update_signal, self.receive_report ) + async def async_will_remove_from_hass(self): + """Unsubscribe from updates from the component.""" + _LOGGER.debug("Removing OpenTherm Gateway sensor %s", self._friendly_name) + self._unsub_updates() + + @property + def entity_registry_enabled_default(self): + """Disable sensors by default.""" + return False + @callback def receive_report(self, status): """Handle status updates from the component.""" @@ -69,6 +80,22 @@ class OpenThermSensor(Entity): """Return the friendly name of the sensor.""" return self._friendly_name + @property + def device_info(self): + """Return device info.""" + return { + "identifiers": {(DOMAIN, self._gateway.gw_id)}, + "name": self._gateway.name, + "manufacturer": "Schelte Bron", + "model": "OpenTherm Gateway", + "sw_version": self._gateway.gw_version, + } + + @property + def unique_id(self): + """Return a unique ID.""" + return f"{self._gateway.gw_id}-{self._var}" + @property def device_class(self): """Return the device class.""" diff --git a/homeassistant/components/openuv/.translations/da.json b/homeassistant/components/openuv/.translations/da.json index a783c8646e0..eaf2e127026 100644 --- a/homeassistant/components/openuv/.translations/da.json +++ b/homeassistant/components/openuv/.translations/da.json @@ -2,12 +2,12 @@ "config": { "error": { "identifier_exists": "Koordinater er allerede registreret", - "invalid_api_key": "Ugyldig API n\u00f8gle" + "invalid_api_key": "Ugyldig API-n\u00f8gle" }, "step": { "user": { "data": { - "api_key": "OpenUV API N\u00f8gle", + "api_key": "OpenUV API-n\u00f8gle", "elevation": "Elevation", "latitude": "Breddegrad", "longitude": "L\u00e6ngdegrad" diff --git a/homeassistant/components/openuv/__init__.py b/homeassistant/components/openuv/__init__.py index 16b7a50a4ae..167fcdcd0e6 100644 --- a/homeassistant/components/openuv/__init__.py +++ b/homeassistant/components/openuv/__init__.py @@ -1,7 +1,9 @@ """Support for UV data from openuv.io.""" -import logging import asyncio +import logging +from pyopenuv import Client +from pyopenuv.errors import OpenUvError import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT @@ -164,8 +166,6 @@ async def async_setup(hass, config): async def async_setup_entry(hass, config_entry): """Set up OpenUV as config entry.""" - from pyopenuv import Client - from pyopenuv.errors import OpenUvError _verify_domain_control = verify_domain_control(hass, DOMAIN) @@ -255,7 +255,6 @@ class OpenUV: async def async_update_protection_data(self): """Update binary sensor (protection window) data.""" - from pyopenuv.errors import OpenUvError if TYPE_PROTECTION_WINDOW in self.binary_sensor_conditions: try: @@ -268,7 +267,6 @@ class OpenUV: async def async_update_uv_index_data(self): """Update sensor (uv index, etc) data.""" - from pyopenuv.errors import OpenUvError if any(c in self.sensor_conditions for c in SENSORS): try: diff --git a/homeassistant/components/openuv/config_flow.py b/homeassistant/components/openuv/config_flow.py index 40ec2abf2fe..7dd8ed45a79 100644 --- a/homeassistant/components/openuv/config_flow.py +++ b/homeassistant/components/openuv/config_flow.py @@ -1,5 +1,6 @@ """Config flow to configure the OpenUV component.""" - +from pyopenuv import Client +from pyopenuv.errors import OpenUvError import voluptuous as vol from homeassistant import config_entries @@ -59,8 +60,6 @@ class OpenUvFlowHandler(config_entries.ConfigFlow): async def async_step_user(self, user_input=None): """Handle the start of the config flow.""" - from pyopenuv import Client - from pyopenuv.errors import OpenUvError if not user_input: return await self._show_form() diff --git a/homeassistant/components/openweathermap/weather.py b/homeassistant/components/openweathermap/weather.py index 69ca965d660..ce8676ad440 100644 --- a/homeassistant/components/openweathermap/weather.py +++ b/homeassistant/components/openweathermap/weather.py @@ -272,7 +272,7 @@ class WeatherData: self.latitude, self.longitude ) except APICallError: - _LOGGER.error("Exception when calling OWM web API " "to update forecast") + _LOGGER.error("Exception when calling OWM web API to update forecast") return if fcd is None: diff --git a/homeassistant/components/opple/light.py b/homeassistant/components/opple/light.py index 5a6657a323d..9ee53704d10 100644 --- a/homeassistant/components/opple/light.py +++ b/homeassistant/components/opple/light.py @@ -1,6 +1,7 @@ """Support for the Opple light.""" import logging +from pyoppleio.OppleLightDevice import OppleLightDevice import voluptuous as vol from homeassistant.components.light import ( @@ -46,7 +47,6 @@ class OppleLight(Light): def __init__(self, name, host): """Initialize an Opple light.""" - from pyoppleio.OppleLightDevice import OppleLightDevice self._device = OppleLightDevice(host) diff --git a/homeassistant/components/oru/sensor.py b/homeassistant/components/oru/sensor.py index e68d8e1c45a..32eb5b7569b 100644 --- a/homeassistant/components/oru/sensor.py +++ b/homeassistant/components/oru/sensor.py @@ -2,14 +2,12 @@ from datetime import timedelta import logging +from oru import Meter, MeterError import voluptuous as vol -from oru import Meter -from oru import MeterError - from homeassistant.components.sensor import PLATFORM_SCHEMA -import homeassistant.helpers.config_validation as cv 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__) diff --git a/homeassistant/components/orvibo/switch.py b/homeassistant/components/orvibo/switch.py index 38d0d2c05d4..75a95e053ae 100644 --- a/homeassistant/components/orvibo/switch.py +++ b/homeassistant/components/orvibo/switch.py @@ -1,15 +1,16 @@ """Support for Orvibo S20 Wifi Smart Switches.""" import logging +from orvibo.s20 import S20, S20Exception, discover import voluptuous as vol -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import ( + CONF_DISCOVERY, CONF_HOST, + CONF_MAC, CONF_NAME, CONF_SWITCHES, - CONF_MAC, - CONF_DISCOVERY, ) import homeassistant.helpers.config_validation as cv @@ -37,7 +38,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities_callback, discovery_info=None): """Set up S20 switches.""" - from orvibo.s20 import discover, S20, S20Exception switch_data = {} switches = [] @@ -67,7 +67,6 @@ class S20Switch(SwitchDevice): def __init__(self, name, s20): """Initialize the S20 device.""" - from orvibo.s20 import S20Exception self._name = name self._s20 = s20 diff --git a/homeassistant/components/owlet/__init__.py b/homeassistant/components/owlet/__init__.py index f9543c7fa6e..3882ba4bf7d 100644 --- a/homeassistant/components/owlet/__init__.py +++ b/homeassistant/components/owlet/__init__.py @@ -1,6 +1,7 @@ """Support for Owlet baby monitors.""" import logging +from pyowlet.PyOwlet import PyOwlet import voluptuous as vol from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME @@ -41,7 +42,6 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): """Set up owlet component.""" - from pyowlet.PyOwlet import PyOwlet username = config[DOMAIN][CONF_USERNAME] password = config[DOMAIN][CONF_PASSWORD] @@ -51,7 +51,7 @@ def setup(hass, config): device = PyOwlet(username, password) except KeyError: _LOGGER.error( - "Owlet authentication failed. Please verify your " "credentials are correct" + "Owlet authentication failed. Please verify your credentials are correct" ) return False diff --git a/homeassistant/components/owntracks/.translations/da.json b/homeassistant/components/owntracks/.translations/da.json index bc1328d57e4..110f60193e6 100644 --- a/homeassistant/components/owntracks/.translations/da.json +++ b/homeassistant/components/owntracks/.translations/da.json @@ -4,7 +4,7 @@ "one_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning" }, "create_entry": { - "default": "\n\nP\u00e5 Android skal du \u00e5bne [OwnTracks applikationen]({android_url}), g\u00e5 til indstillinger -> forbindelse. Skift f\u00f8lgende indstillinger: \n - Tilstand: Privat HTTP\n - V\u00e6rt: {webhook_url}\n - Identifikation:\n - Brugernavn: ` ` \n - Enheds-id: ` ` \n\nP\u00e5 iOS skal du \u00e5bne [OwnTracks applikationen]({ios_url}), tryk p\u00e5 (i) ikonet \u00f8verst til venstre -> indstillinger. Skift f\u00f8lgende indstillinger: \n - Tilstand: HTTP\n - URL: {webhook_url}\n - Aktiver godkendelse \n - Bruger ID: ` ` \n\n {secret}\n \n Se [dokumentationen]({docs_url}) for at f\u00e5 flere oplysninger." + "default": "\n\nP\u00e5 Android skal du \u00e5bne [OwnTracks-appen]({android_url}), g\u00e5 til indstillinger -> forbindelse. Skift f\u00f8lgende indstillinger: \n - Tilstand: Privat HTTP\n - V\u00e6rt: {webhook_url}\n - Identifikation:\n - Brugernavn: ` ` \n - Enheds-id: ` ` \n\nP\u00e5 iOS skal du \u00e5bne [OwnTracks-appen]({ios_url}), tryk p\u00e5 (i) ikonet \u00f8verst til venstre -> indstillinger. Skift f\u00f8lgende indstillinger: \n - Tilstand: HTTP\n - URL: {webhook_url}\n - Aktiver godkendelse \n - Bruger ID: ` ` \n\n {secret}\n \n Se [dokumentationen]({docs_url}) for at f\u00e5 flere oplysninger." }, "step": { "user": { diff --git a/homeassistant/components/owntracks/.translations/ko.json b/homeassistant/components/owntracks/.translations/ko.json index d70ca8b114e..ee1507d9e0a 100644 --- a/homeassistant/components/owntracks/.translations/ko.json +++ b/homeassistant/components/owntracks/.translations/ko.json @@ -8,7 +8,7 @@ }, "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" } }, diff --git a/homeassistant/components/owntracks/__init__.py b/homeassistant/components/owntracks/__init__.py index d30e667f368..71494e9e805 100644 --- a/homeassistant/components/owntracks/__init__.py +++ b/homeassistant/components/owntracks/__init__.py @@ -14,8 +14,8 @@ from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.setup import async_when_setup -from .const import DOMAIN from .config_flow import CONF_SECRET +from .const import DOMAIN from .messages import async_handle_message _LOGGER = logging.getLogger(__name__) @@ -118,7 +118,7 @@ async def async_unload_entry(hass, entry): async def async_remove_entry(hass, entry): """Remove an OwnTracks config entry.""" - if not entry.data.get("cloudhook") or "cloud" not in hass.config.components: + if not entry.data.get("cloudhook"): return await hass.components.cloud.async_delete_cloudhook(entry.data[CONF_WEBHOOK_ID]) @@ -233,7 +233,7 @@ class OwnTracksContext: if self.max_gps_accuracy is not None and acc > self.max_gps_accuracy: _LOGGER.info( - "Ignoring %s update because expected GPS " "accuracy %s is not met: %s", + "Ignoring %s update because expected GPS accuracy %s is not met: %s", message["_type"], self.max_gps_accuracy, message, diff --git a/homeassistant/components/owntracks/config_flow.py b/homeassistant/components/owntracks/config_flow.py index 5034114293f..0aba24217cc 100644 --- a/homeassistant/components/owntracks/config_flow.py +++ b/homeassistant/components/owntracks/config_flow.py @@ -1,24 +1,16 @@ """Config flow for OwnTracks.""" +import secrets + from homeassistant import config_entries from homeassistant.const import CONF_WEBHOOK_ID -from homeassistant.auth.util import generate_secret from .const import DOMAIN # noqa pylint: disable=unused-import +from .helper import supports_encryption CONF_SECRET = "secret" CONF_CLOUDHOOK = "cloudhook" -def supports_encryption(): - """Test if we support encryption.""" - try: - import nacl # noqa: F401 pylint: disable=unused-import - - return True - except OSError: - return False - - class OwnTracksFlow(config_entries.ConfigFlow, domain=DOMAIN): """Set up OwnTracks.""" @@ -34,7 +26,7 @@ class OwnTracksFlow(config_entries.ConfigFlow, domain=DOMAIN): webhook_id, webhook_url, cloudhook = await self._get_webhook_id() - secret = generate_secret(16) + secret = secrets.token_hex(16) if supports_encryption(): secret_desc = f"The encryption key is {secret} (on Android under preferences -> advanced)" @@ -62,7 +54,7 @@ class OwnTracksFlow(config_entries.ConfigFlow, domain=DOMAIN): if self._async_current_entries(): return self.async_abort(reason="one_instance_allowed") webhook_id, _webhook_url, cloudhook = await self._get_webhook_id() - secret = generate_secret(16) + secret = secrets.token_hex(16) return self.async_create_entry( title="OwnTracks", data={ @@ -75,10 +67,7 @@ class OwnTracksFlow(config_entries.ConfigFlow, domain=DOMAIN): async def _get_webhook_id(self): """Generate webhook ID.""" webhook_id = self.hass.components.webhook.async_generate_id() - if ( - "cloud" in self.hass.config.components - and self.hass.components.cloud.async_active_subscription() - ): + if self.hass.components.cloud.async_active_subscription(): webhook_url = await self.hass.components.cloud.async_create_cloudhook( webhook_id ) diff --git a/homeassistant/components/owntracks/device_tracker.py b/homeassistant/components/owntracks/device_tracker.py index 6d3254eda99..00fa023d6c1 100644 --- a/homeassistant/components/owntracks/device_tracker.py +++ b/homeassistant/components/owntracks/device_tracker.py @@ -1,21 +1,22 @@ """Device tracker platform that adds support for OwnTracks over MQTT.""" import logging -from homeassistant.core import callback +from homeassistant.components.device_tracker.config_entry import TrackerEntity +from homeassistant.components.device_tracker.const import ( + ATTR_SOURCE_TYPE, + ENTITY_ID_FORMAT, + SOURCE_TYPE_GPS, +) from homeassistant.const import ( + ATTR_BATTERY_LEVEL, ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, - ATTR_BATTERY_LEVEL, ) -from homeassistant.components.device_tracker.const import ( - ENTITY_ID_FORMAT, - ATTR_SOURCE_TYPE, - SOURCE_TYPE_GPS, -) -from homeassistant.components.device_tracker.config_entry import TrackerEntity -from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.core import callback from homeassistant.helpers import device_registry +from homeassistant.helpers.restore_state import RestoreEntity + from . import DOMAIN as OT_DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/owntracks/helper.py b/homeassistant/components/owntracks/helper.py new file mode 100644 index 00000000000..b6ed307112c --- /dev/null +++ b/homeassistant/components/owntracks/helper.py @@ -0,0 +1,10 @@ +"""Helper for OwnTracks.""" +try: + import nacl +except ImportError: + nacl = None + + +def supports_encryption() -> bool: + """Test if we support encryption.""" + return nacl is not None diff --git a/homeassistant/components/owntracks/manifest.json b/homeassistant/components/owntracks/manifest.json index 529d7990a86..63fdfb94cf7 100644 --- a/homeassistant/components/owntracks/manifest.json +++ b/homeassistant/components/owntracks/manifest.json @@ -3,14 +3,8 @@ "name": "Owntracks", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/owntracks", - "requirements": [ - "PyNaCl==1.3.0" - ], - "dependencies": [ - "webhook" - ], - "after_dependencies": [ - "mqtt" - ], + "requirements": ["PyNaCl==1.3.0"], + "dependencies": ["webhook"], + "after_dependencies": ["mqtt", "cloud"], "codeowners": [] } diff --git a/homeassistant/components/owntracks/messages.py b/homeassistant/components/owntracks/messages.py index 0cb65c774b5..d357843c42e 100644 --- a/homeassistant/components/owntracks/messages.py +++ b/homeassistant/components/owntracks/messages.py @@ -2,15 +2,18 @@ import json import logging +from nacl.encoding import Base64Encoder +from nacl.secret import SecretBox + from homeassistant.components import zone as zone_comp from homeassistant.components.device_tracker import ( - SOURCE_TYPE_GPS, SOURCE_TYPE_BLUETOOTH_LE, + SOURCE_TYPE_GPS, ) - from homeassistant.const import STATE_HOME from homeassistant.util import decorator, slugify +from .helper import supports_encryption _LOGGER = logging.getLogger(__name__) @@ -22,8 +25,6 @@ def get_cipher(): Async friendly. """ - from nacl.secret import SecretBox - from nacl.encoding import Base64Encoder def decrypt(ciphertext, key): """Decrypt ciphertext using key.""" @@ -105,7 +106,11 @@ def _set_gps_from_zone(kwargs, location, zone): def _decrypt_payload(secret, topic, ciphertext): """Decrypt encrypted payload.""" try: - keylen, decrypt = get_cipher() + if supports_encryption(): + keylen, decrypt = get_cipher() + else: + _LOGGER.warning("Ignoring encrypted payload because nacl not installed") + return None except OSError: _LOGGER.warning("Ignoring encrypted payload because nacl not installed") return None diff --git a/homeassistant/components/pcal9535a/binary_sensor.py b/homeassistant/components/pcal9535a/binary_sensor.py index fd4e92ccf03..236fd47af73 100644 --- a/homeassistant/components/pcal9535a/binary_sensor.py +++ b/homeassistant/components/pcal9535a/binary_sensor.py @@ -1,10 +1,10 @@ """Support for binary sensor using I2C PCAL9535A chip.""" import logging -import voluptuous as vol from pcal9535a import PCAL9535A +import voluptuous as vol -from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice from homeassistant.const import DEVICE_DEFAULT_NAME import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/pcal9535a/switch.py b/homeassistant/components/pcal9535a/switch.py index faebce5d67e..87c8ced1b0d 100644 --- a/homeassistant/components/pcal9535a/switch.py +++ b/homeassistant/components/pcal9535a/switch.py @@ -1,8 +1,8 @@ """Support for switch sensor using I2C PCAL9535A chip.""" import logging -import voluptuous as vol from pcal9535a import PCAL9535A +import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import DEVICE_DEFAULT_NAME diff --git a/homeassistant/components/pencom/switch.py b/homeassistant/components/pencom/switch.py index 60e7ef30837..36266feaa6e 100644 --- a/homeassistant/components/pencom/switch.py +++ b/homeassistant/components/pencom/switch.py @@ -5,10 +5,11 @@ http://home-assistant.io/components/switch.pencom """ import logging +from pencompy.pencompy import Pencompy import voluptuous as vol -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA -from homeassistant.const import CONF_HOST, CONF_PORT, CONF_NAME +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv @@ -39,7 +40,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Pencom relay platform (pencompy).""" - from pencompy.pencompy import Pencompy # Assign configuration variables. host = config[CONF_HOST] diff --git a/homeassistant/components/persistent_notification/__init__.py b/homeassistant/components/persistent_notification/__init__.py index 33f17b18a80..0311bd4d30d 100644 --- a/homeassistant/components/persistent_notification/__init__.py +++ b/homeassistant/components/persistent_notification/__init__.py @@ -14,7 +14,6 @@ from homeassistant.loader import bind_hass from homeassistant.util import slugify import homeassistant.util.dt as dt_util - # mypy: allow-untyped-calls, allow-untyped-defs ATTR_CREATED_AT = "created_at" diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index 832853c670d..2e347cf4d49 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -7,27 +7,27 @@ import uuid import voluptuous as vol +from homeassistant.auth import EVENT_USER_REMOVED from homeassistant.components import websocket_api from homeassistant.components.device_tracker import ( - DOMAIN as DEVICE_TRACKER_DOMAIN, ATTR_SOURCE_TYPE, + DOMAIN as DEVICE_TRACKER_DOMAIN, SOURCE_TYPE_GPS, ) from homeassistant.const import ( + ATTR_GPS_ACCURACY, ATTR_ID, ATTR_LATITUDE, ATTR_LONGITUDE, - ATTR_GPS_ACCURACY, CONF_ID, CONF_NAME, EVENT_HOMEASSISTANT_START, - STATE_UNKNOWN, - STATE_UNAVAILABLE, STATE_HOME, STATE_NOT_HOME, + STATE_UNAVAILABLE, + STATE_UNKNOWN, ) -from homeassistant.core import callback, Event, State -from homeassistant.auth import EVENT_USER_REMOVED +from homeassistant.core import Event, State, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import async_track_state_change diff --git a/homeassistant/components/person/manifest.json b/homeassistant/components/person/manifest.json index cf50b8029c2..afcd428d6af 100644 --- a/homeassistant/components/person/manifest.json +++ b/homeassistant/components/person/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/person", "requirements": [], "dependencies": [], + "after_dependencies": ["device_tracker"], "codeowners": [] } diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py index 579dc253603..fe6d7edf804 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -2,11 +2,14 @@ from datetime import timedelta import logging +from haphilipsjs import PhilipsTV import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( + MEDIA_TYPE_CHANNEL, SUPPORT_NEXT_TRACK, + SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, @@ -14,8 +17,6 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, - MEDIA_TYPE_CHANNEL, - SUPPORT_PLAY_MEDIA, ) from homeassistant.const import ( CONF_API_VERSION, @@ -70,20 +71,18 @@ def _inverted(data): def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Philips TV platform.""" - import haphilipsjs - 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 = haphilipsjs.PhilipsTV(host, api_version) + tvapi = PhilipsTV(host, api_version) on_script = Script(hass, turn_on_action) if turn_on_action else None - add_entities([PhilipsTV(tvapi, name, on_script)]) + add_entities([PhilipsTVMediaPlayer(tvapi, name, on_script)]) -class PhilipsTV(MediaPlayerDevice): +class PhilipsTVMediaPlayer(MediaPlayerDevice): """Representation of a Philips TV exposing the JointSpace API.""" def __init__(self, tv, name, on_script): diff --git a/homeassistant/components/pi_hole/__init__.py b/homeassistant/components/pi_hole/__init__.py index 95351083b5a..ed6144af47e 100644 --- a/homeassistant/components/pi_hole/__init__.py +++ b/homeassistant/components/pi_hole/__init__.py @@ -1,110 +1,222 @@ """The pi_hole component.""" import logging -import voluptuous as vol from hole import Hole from hole.exceptions import HoleError +import voluptuous as vol +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import ( + CONF_API_KEY, CONF_HOST, CONF_NAME, - CONF_API_KEY, CONF_SSL, CONF_VERIFY_SSL, ) -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.discovery import async_load_platform from homeassistant.util import Throttle from .const import ( - DOMAIN, CONF_LOCATION, - DEFAULT_HOST, + CONF_SLUG, DEFAULT_LOCATION, DEFAULT_NAME, DEFAULT_SSL, DEFAULT_VERIFY_SSL, + DOMAIN, MIN_TIME_BETWEEN_UPDATES, SERVICE_DISABLE, SERVICE_DISABLE_ATTR_DURATION, + SERVICE_DISABLE_ATTR_NAME, SERVICE_ENABLE, + SERVICE_ENABLE_ATTR_NAME, ) + +def ensure_unique_names_and_slugs(config): + """Ensure that each configuration dict contains a unique `name` value.""" + names = {} + slugs = {} + for conf in config: + if conf[CONF_NAME] not in names and conf[CONF_SLUG] not in slugs: + names[conf[CONF_NAME]] = conf[CONF_HOST] + slugs[conf[CONF_SLUG]] = conf[CONF_HOST] + else: + raise vol.Invalid( + "Duplicate name '{}' (or slug '{}') for '{}' (already in use by '{}'). Each configured Pi-hole must have a unique name.".format( + conf[CONF_NAME], + conf[CONF_SLUG], + conf[CONF_HOST], + names.get(conf[CONF_NAME], slugs[conf[CONF_SLUG]]), + ) + ) + return config + + +def coerce_slug(config): + """Coerce the name of the Pi-Hole into a slug.""" + config[CONF_SLUG] = cv.slugify(config[CONF_NAME]) + return config + + LOGGER = logging.getLogger(__name__) +PI_HOLE_SCHEMA = vol.Schema( + vol.All( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_API_KEY): cv.string, + vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, + vol.Optional(CONF_LOCATION, default=DEFAULT_LOCATION): cv.string, + vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, + }, + coerce_slug, + ) +) + CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( - { - vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_API_KEY): cv.string, - vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, - vol.Optional(CONF_LOCATION, default=DEFAULT_LOCATION): cv.string, - vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, - } + vol.All(cv.ensure_list, [PI_HOLE_SCHEMA], ensure_unique_names_and_slugs) ) }, extra=vol.ALLOW_EXTRA, ) -SERVICE_DISABLE_SCHEMA = vol.Schema( - { - vol.Required(SERVICE_DISABLE_ATTR_DURATION): vol.All( - cv.time_period_str, cv.positive_timedelta - ) - } -) - async def async_setup(hass, config): """Set up the pi_hole integration.""" - conf = config[DOMAIN] - name = conf[CONF_NAME] - host = conf[CONF_HOST] - use_tls = conf[CONF_SSL] - verify_tls = conf[CONF_VERIFY_SSL] - location = conf[CONF_LOCATION] - api_key = conf.get(CONF_API_KEY) + def get_data(): + """Retrive component data.""" + return hass.data[DOMAIN] - LOGGER.debug("Setting up %s integration with host %s", DOMAIN, host) + def ensure_api_token(call_data): + """Ensure the Pi-Hole to be enabled/disabled has a api_token configured.""" + data = get_data() + if SERVICE_DISABLE_ATTR_NAME not in call_data: + for slug in data: + call_data[SERVICE_DISABLE_ATTR_NAME] = data[slug].name + ensure_api_token(call_data) - session = async_get_clientsession(hass, verify_tls) - pi_hole = PiHoleData( - Hole( - host, hass.loop, session, location=location, tls=use_tls, api_token=api_key - ), - name, + call_data[SERVICE_DISABLE_ATTR_NAME] = None + else: + slug = cv.slugify(call_data[SERVICE_DISABLE_ATTR_NAME]) + + if (data[slug]).api.api_token is None: + raise vol.Invalid( + "Pi-hole '{}' must have an api_key provided in configuration to be enabled.".format( + pi_hole.name + ) + ) + + return call_data + + service_disable_schema = vol.Schema( # pylint: disable=invalid-name + vol.All( + { + vol.Required(SERVICE_DISABLE_ATTR_DURATION): vol.All( + cv.time_period_str, cv.positive_timedelta + ), + vol.Optional(SERVICE_DISABLE_ATTR_NAME): vol.In( + [conf[CONF_NAME] for conf in config[DOMAIN]], msg="Unknown Pi-Hole", + ), + }, + ensure_api_token, + ) ) - await pi_hole.async_update() + service_enable_schema = vol.Schema( + { + vol.Optional(SERVICE_ENABLE_ATTR_NAME): vol.In( + [conf[CONF_NAME] for conf in config[DOMAIN]], msg="Unknown Pi-Hole" + ) + } + ) - hass.data[DOMAIN] = pi_hole + hass.data[DOMAIN] = {} - async def handle_disable(call): - if api_key is None: - raise vol.Invalid("Pi-hole api_key must be provided in configuration") + for conf in config[DOMAIN]: + name = conf[CONF_NAME] + slug = conf[CONF_SLUG] + host = conf[CONF_HOST] + use_tls = conf[CONF_SSL] + verify_tls = conf[CONF_VERIFY_SSL] + location = conf[CONF_LOCATION] + api_key = conf.get(CONF_API_KEY) + LOGGER.debug("Setting up %s integration with host %s", DOMAIN, host) + + session = async_get_clientsession(hass, verify_tls) + pi_hole = PiHoleData( + Hole( + host, + hass.loop, + session, + location=location, + tls=use_tls, + api_token=api_key, + ), + name, + ) + + await pi_hole.async_update() + + hass.data[DOMAIN][slug] = pi_hole + + async def disable_service_handler(call): + """Handle the service call to disable a single Pi-Hole or all configured Pi-Holes.""" duration = call.data[SERVICE_DISABLE_ATTR_DURATION].total_seconds() + name = call.data.get(SERVICE_DISABLE_ATTR_NAME) - LOGGER.debug("Disabling %s %s for %d seconds", DOMAIN, host, duration) - await pi_hole.api.disable(duration) + async def do_disable(name): + """Disable the named Pi-Hole.""" + slug = cv.slugify(name) + pi_hole = hass.data[DOMAIN][slug] - async def handle_enable(call): - if api_key is None: - raise vol.Invalid("Pi-hole api_key must be provided in configuration") + LOGGER.debug( + "Disabling Pi-hole '%s' (%s) for %d seconds", + name, + pi_hole.api.host, + duration, + ) + await pi_hole.api.disable(duration) - LOGGER.debug("Enabling %s %s", DOMAIN, host) - await pi_hole.api.enable() + if name is not None: + await do_disable(name) + else: + for pi_hole in hass.data[DOMAIN].values(): + await do_disable(pi_hole.name) + + async def enable_service_handler(call): + """Handle the service call to enable a single Pi-Hole or all configured Pi-Holes.""" + + name = call.data.get(SERVICE_ENABLE_ATTR_NAME) + + async def do_enable(name): + """Enable the named Pi-Hole.""" + slug = cv.slugify(name) + pi_hole = hass.data[DOMAIN][slug] + + LOGGER.debug("Enabling Pi-hole '%s' (%s)", name, pi_hole.api.host) + await pi_hole.api.enable() + + if name is not None: + await do_enable(name) + else: + for pi_hole in hass.data[DOMAIN].values(): + await do_enable(pi_hole.name) hass.services.async_register( - DOMAIN, SERVICE_DISABLE, handle_disable, schema=SERVICE_DISABLE_SCHEMA + DOMAIN, SERVICE_DISABLE, disable_service_handler, schema=service_disable_schema ) - hass.services.async_register(DOMAIN, SERVICE_ENABLE, handle_enable) + hass.services.async_register( + DOMAIN, SERVICE_ENABLE, enable_service_handler, schema=service_enable_schema + ) hass.async_create_task(async_load_platform(hass, SENSOR_DOMAIN, DOMAIN, {}, config)) diff --git a/homeassistant/components/pi_hole/const.py b/homeassistant/components/pi_hole/const.py index 54220547950..0ae62b31865 100644 --- a/homeassistant/components/pi_hole/const.py +++ b/homeassistant/components/pi_hole/const.py @@ -4,8 +4,8 @@ from datetime import timedelta DOMAIN = "pi_hole" CONF_LOCATION = "location" +CONF_SLUG = "slug" -DEFAULT_HOST = "pi.hole" DEFAULT_LOCATION = "admin" DEFAULT_METHOD = "GET" DEFAULT_NAME = "Pi-Hole" @@ -13,8 +13,10 @@ DEFAULT_SSL = False DEFAULT_VERIFY_SSL = True SERVICE_DISABLE = "disable" -SERVICE_ENABLE = "enable" SERVICE_DISABLE_ATTR_DURATION = "duration" +SERVICE_DISABLE_ATTR_NAME = "name" +SERVICE_ENABLE = "enable" +SERVICE_ENABLE_ATTR_NAME = SERVICE_DISABLE_ATTR_NAME ATTR_BLOCKED_DOMAINS = "domains_blocked" diff --git a/homeassistant/components/pi_hole/sensor.py b/homeassistant/components/pi_hole/sensor.py index 4e80e9767a6..c01a0167e53 100644 --- a/homeassistant/components/pi_hole/sensor.py +++ b/homeassistant/components/pi_hole/sensor.py @@ -4,10 +4,10 @@ import logging from homeassistant.helpers.entity import Entity from .const import ( - DOMAIN as PIHOLE_DOMAIN, ATTR_BLOCKED_DOMAINS, - SENSOR_LIST, + DOMAIN as PIHOLE_DOMAIN, SENSOR_DICT, + SENSOR_LIST, ) LOGGER = logging.getLogger(__name__) @@ -18,10 +18,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= if discovery_info is None: return - pi_hole = hass.data[PIHOLE_DOMAIN] - sensors = [] - sensors = [PiHoleSensor(pi_hole, sensor_name) for sensor_name in SENSOR_LIST] + for pi_hole in hass.data[PIHOLE_DOMAIN].values(): + for sensor in [ + PiHoleSensor(pi_hole, sensor_name) for sensor_name in SENSOR_LIST + ]: + sensors.append(sensor) async_add_entities(sensors, True) diff --git a/homeassistant/components/pi_hole/services.yaml b/homeassistant/components/pi_hole/services.yaml index b16ed21a5d3..e3cc8624e36 100644 --- a/homeassistant/components/pi_hole/services.yaml +++ b/homeassistant/components/pi_hole/services.yaml @@ -1,8 +1,15 @@ disable: - description: Disable Pi-hole for an amount of time + description: Disable configured Pi-hole(s) for an amount of time fields: duration: description: Time that the Pi-hole should be disabled for example: "00:00:15" + name: + description: "[Optional] When multiple Pi-holes are configured, the name of the one to disable. If omitted, all configured Pi-holes will be disabled." + example: "Pi-Hole" enable: - description: Enable Pi-hole \ No newline at end of file + description: Enable configured Pi-hole(s) + fields: + name: + description: "[Optional] When multiple Pi-holes are configured, the name of the one to enable. If omitted, all configured Pi-holes will be enabled." + example: "Pi-Hole" \ No newline at end of file diff --git a/homeassistant/components/pilight/__init__.py b/homeassistant/components/pilight/__init__.py index 2688b15e837..50ee1b248b0 100644 --- a/homeassistant/components/pilight/__init__.py +++ b/homeassistant/components/pilight/__init__.py @@ -1,23 +1,24 @@ """Component to create an interface to a Pilight daemon.""" -import logging +from datetime import timedelta import functools +import logging import socket import threading -from datetime import timedelta +from pilight import pilight import voluptuous as vol -from homeassistant.helpers.event import track_point_in_utc_time -from homeassistant.util import dt as dt_util -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( - EVENT_HOMEASSISTANT_START, - EVENT_HOMEASSISTANT_STOP, CONF_HOST, CONF_PORT, - CONF_WHITELIST, CONF_PROTOCOL, + CONF_WHITELIST, + EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP, ) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import track_point_in_utc_time +from homeassistant.util import dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -59,7 +60,6 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): """Set up the Pilight component.""" - from pilight import pilight host = config[DOMAIN][CONF_HOST] port = config[DOMAIN][CONF_PORT] diff --git a/homeassistant/components/pilight/binary_sensor.py b/homeassistant/components/pilight/binary_sensor.py index a8f7d84b9b1..ae6d562725d 100644 --- a/homeassistant/components/pilight/binary_sensor.py +++ b/homeassistant/components/pilight/binary_sensor.py @@ -3,6 +3,7 @@ import datetime import logging import voluptuous as vol + from homeassistant.components import pilight from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice from homeassistant.const import ( @@ -16,7 +17,6 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.event import track_point_in_time from homeassistant.util import dt as dt_util - _LOGGER = logging.getLogger(__name__) CONF_VARIABLE = "variable" diff --git a/homeassistant/components/pilight/sensor.py b/homeassistant/components/pilight/sensor.py index 006bebf74bb..e8c7b4bd4b6 100644 --- a/homeassistant/components/pilight/sensor.py +++ b/homeassistant/components/pilight/sensor.py @@ -3,11 +3,11 @@ import logging import voluptuous as vol -from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT, CONF_PAYLOAD -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.helpers.entity import Entity from homeassistant.components import pilight +from homeassistant.components.sensor import PLATFORM_SCHEMA +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__) diff --git a/homeassistant/components/pilight/services.yaml b/homeassistant/components/pilight/services.yaml index e69de29bb2d..cc6141fdd91 100644 --- a/homeassistant/components/pilight/services.yaml +++ b/homeassistant/components/pilight/services.yaml @@ -0,0 +1,6 @@ +send: + description: Send RF code to Pilight device + fields: + protocol: + description: 'Protocol that Pilight recognizes. See https://manual.pilight.org/protocols/index.html for supported protocols and additional parameters that each protocol supports' + example: 'lirc' diff --git a/homeassistant/components/pilight/switch.py b/homeassistant/components/pilight/switch.py index fb95b91dfd8..8be199921dc 100644 --- a/homeassistant/components/pilight/switch.py +++ b/homeassistant/components/pilight/switch.py @@ -3,17 +3,17 @@ import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components import pilight -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import ( - CONF_NAME, CONF_ID, - CONF_SWITCHES, - CONF_STATE, + CONF_NAME, CONF_PROTOCOL, + CONF_STATE, + CONF_SWITCHES, STATE_ON, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/ping/binary_sensor.py b/homeassistant/components/ping/binary_sensor.py index fe4c12d6738..4d9a99c678e 100644 --- a/homeassistant/components/ping/binary_sensor.py +++ b/homeassistant/components/ping/binary_sensor.py @@ -1,15 +1,15 @@ """Tracks the latency of a host by sending ICMP echo requests (ping).""" -import logging -import subprocess -import re -import sys from datetime import timedelta +import logging +import re +import subprocess +import sys import voluptuous as vol +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice +from homeassistant.const import CONF_HOST, CONF_NAME import homeassistant.helpers.config_validation as cv -from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, CONF_HOST _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/ping/device_tracker.py b/homeassistant/components/ping/device_tracker.py index 9bdc38e065b..c4d88f6061c 100644 --- a/homeassistant/components/ping/device_tracker.py +++ b/homeassistant/components/ping/device_tracker.py @@ -1,20 +1,19 @@ """Tracks devices by sending a ICMP echo request (ping).""" +from datetime import timedelta import logging import subprocess import sys -from datetime import timedelta import voluptuous as vol -import homeassistant.helpers.config_validation as cv +from homeassistant import const, util from homeassistant.components.device_tracker import PLATFORM_SCHEMA from homeassistant.components.device_tracker.const import ( CONF_SCAN_INTERVAL, SCAN_INTERVAL, SOURCE_TYPE_ROUTER, ) -from homeassistant import util -from homeassistant import const +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/pioneer/media_player.py b/homeassistant/components/pioneer/media_player.py index 51f55d4e851..3e71b54c9fa 100644 --- a/homeassistant/components/pioneer/media_player.py +++ b/homeassistant/components/pioneer/media_player.py @@ -4,7 +4,7 @@ import telnetlib import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( SUPPORT_PAUSE, SUPPORT_PLAY, diff --git a/homeassistant/components/pjlink/media_player.py b/homeassistant/components/pjlink/media_player.py index ea35fe7fb75..e93e6e5fb20 100644 --- a/homeassistant/components/pjlink/media_player.py +++ b/homeassistant/components/pjlink/media_player.py @@ -1,9 +1,11 @@ """Support for controlling projector via the PJLink protocol.""" import logging +from pypjlink import MUTE_AUDIO, Projector +from pypjlink.projector import ProjectorError import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, @@ -90,7 +92,6 @@ class PjLinkDevice(MediaPlayerDevice): def projector(self): """Create PJLink Projector instance.""" - from pypjlink import Projector projector = Projector.from_address(self._host, self._port, self._encoding) projector.authenticate(self._password) @@ -98,7 +99,6 @@ class PjLinkDevice(MediaPlayerDevice): def update(self): """Get the latest state from the device.""" - from pypjlink.projector import ProjectorError with self.projector() as projector: try: @@ -171,8 +171,6 @@ class PjLinkDevice(MediaPlayerDevice): def mute_volume(self, mute): """Mute (true) of unmute (false) media player.""" with self.projector() as projector: - from pypjlink import MUTE_AUDIO - projector.set_mute(MUTE_AUDIO, mute) def select_source(self, source): diff --git a/homeassistant/components/plaato/.translations/da.json b/homeassistant/components/plaato/.translations/da.json index 12e95b25e0f..c4dc5ae178d 100644 --- a/homeassistant/components/plaato/.translations/da.json +++ b/homeassistant/components/plaato/.translations/da.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "not_internet_accessible": "Din Home Assistant instans skal v\u00e6re tilg\u00e6ngelig fra internettet for at modtage meddelelser fra Plaato Airlock.", - "one_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning." + "not_internet_accessible": "Din Home Assistant-instans skal v\u00e6re tilg\u00e6ngelig fra internettet for at modtage meddelelser fra Plaato Airlock.", + "one_instance_allowed": "Kun en enkelt instans er n\u00f8dvendig." }, "create_entry": { - "default": "For at sende begivenheder til Home Assistant skal du konfigurere webhook funktionen i Plaato Airlock.\n\n Udfyld f\u00f8lgende oplysninger: \n\n - URL: `{webhook_url}`\n - Metode: POST\n \n Se [dokumentationen]({docs_url}) for yderligere oplysninger." + "default": "For at sende h\u00e6ndelser til Home Assistant skal du konfigurere webhook-funktionen i Plaato Airlock.\n\n Udfyld f\u00f8lgende oplysninger: \n\n - Webadresse: `{webhook_url}`\n - Metode: POST\n \nSe [dokumentationen]({docs_url}) for yderligere oplysninger." }, "step": { "user": { diff --git a/homeassistant/components/plaato/.translations/ko.json b/homeassistant/components/plaato/.translations/ko.json index 50a51dff873..619fdcf736f 100644 --- a/homeassistant/components/plaato/.translations/ko.json +++ b/homeassistant/components/plaato/.translations/ko.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "Plaato Airlock \uc744 \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "Plaato Airlock \uc744 \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "Plaato Webhook \uc124\uc815" } }, diff --git a/homeassistant/components/plaato/__init__.py b/homeassistant/components/plaato/__init__.py index 49b749b8de6..0dd57f75812 100644 --- a/homeassistant/components/plaato/__init__.py +++ b/homeassistant/components/plaato/__init__.py @@ -15,6 +15,7 @@ from homeassistant.const import ( ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send + from .const import DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/plaato/config_flow.py b/homeassistant/components/plaato/config_flow.py index 59cb270c616..3c616c822fb 100644 --- a/homeassistant/components/plaato/config_flow.py +++ b/homeassistant/components/plaato/config_flow.py @@ -1,5 +1,6 @@ """Config flow for GPSLogger.""" from homeassistant.helpers import config_entry_flow + from .const import DOMAIN config_entry_flow.register_webhook_flow( diff --git a/homeassistant/components/plaato/sensor.py b/homeassistant/components/plaato/sensor.py index f8e6a3e9fa7..e7c8033f2ac 100644 --- a/homeassistant/components/plaato/sensor.py +++ b/homeassistant/components/plaato/sensor.py @@ -2,8 +2,10 @@ import logging -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.entity import Entity from . import ( diff --git a/homeassistant/components/plant/__init__.py b/homeassistant/components/plant/__init__.py index a516e06d55b..cc405dcad1f 100644 --- a/homeassistant/components/plant/__init__.py +++ b/homeassistant/components/plant/__init__.py @@ -6,6 +6,7 @@ import logging import voluptuous as vol from homeassistant.components import group +from homeassistant.components.recorder.models import States from homeassistant.components.recorder.util import execute, session_scope from homeassistant.const import ( ATTR_TEMPERATURE, @@ -288,7 +289,6 @@ class Plant(Entity): This only needs to be done once during startup. """ - from homeassistant.components.recorder.models import States start_date = datetime.now() - timedelta(days=self._conf_check_days) entity_id = self._readingmap.get(READING_BRIGHTNESS) diff --git a/homeassistant/components/plant/manifest.json b/homeassistant/components/plant/manifest.json index 721a57e7822..c1e009ccec3 100644 --- a/homeassistant/components/plant/manifest.json +++ b/homeassistant/components/plant/manifest.json @@ -3,11 +3,7 @@ "name": "Plant", "documentation": "https://www.home-assistant.io/integrations/plant", "requirements": [], - "dependencies": [ - "group", - "zone" - ], - "codeowners": [ - "@ChristianKuehnel" - ] + "dependencies": ["group", "zone"], + "after_dependencies": ["recorder"], + "codeowners": ["@ChristianKuehnel"] } diff --git a/homeassistant/components/plex/.translations/bg.json b/homeassistant/components/plex/.translations/bg.json index 9a2ffe299c8..adfdd98ebaf 100644 --- a/homeassistant/components/plex/.translations/bg.json +++ b/homeassistant/components/plex/.translations/bg.json @@ -6,6 +6,7 @@ "already_in_progress": "Plex \u0441\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430", "discovery_no_file": "\u041d\u0435 \u0435 \u043d\u0430\u043c\u0435\u0440\u0435\u043d \u0441\u0442\u0430\u0440 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u043e\u043d\u0435\u043d \u0444\u0430\u0439\u043b", "invalid_import": "\u0418\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u0430\u043d\u0430\u0442\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0435 \u043d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0430", + "non-interactive": "\u041d\u0435\u0438\u043d\u0442\u0435\u0440\u0430\u043a\u0442\u0438\u0432\u0435\u043d \u0438\u043c\u043f\u043e\u0440\u0442", "token_request_timeout": "\u0418\u0437\u0442\u0435\u0447\u0435 \u0432\u0440\u0435\u043c\u0435\u0442\u043e \u0437\u0430 \u043f\u043e\u043b\u0443\u0447\u0430\u0432\u0430\u043d\u0435 \u043d\u0430 \u043a\u043e\u0434 \u0437\u0430 \u0434\u043e\u0441\u0442\u044a\u043f", "unknown": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043f\u043e\u0440\u0430\u0434\u0438 \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, @@ -42,7 +43,7 @@ "manual_setup": "\u0420\u044a\u0447\u043d\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430", "token": "Plex \u043a\u043e\u0434" }, - "description": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043a\u043e\u0434 \u0437\u0430 Plex \u0437\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0438\u043b\u0438 \u0440\u044a\u0447\u043d\u043e \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0441\u044a\u0440\u0432\u044a\u0440.", + "description": "\u041f\u0440\u043e\u0434\u044a\u043b\u0436\u0435\u0442\u0435 \u0441 \u043e\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 plex.tv \u0438\u043b\u0438 \u0440\u044a\u0447\u043d\u043e \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435 \u0441\u044a\u0440\u0432\u044a\u0440.", "title": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 Plex \u0441\u044a\u0440\u0432\u044a\u0440" } }, diff --git a/homeassistant/components/plex/.translations/ca.json b/homeassistant/components/plex/.translations/ca.json index 9eb6d16639f..63cf65b8d6c 100644 --- a/homeassistant/components/plex/.translations/ca.json +++ b/homeassistant/components/plex/.translations/ca.json @@ -6,6 +6,7 @@ "already_in_progress": "S\u2019est\u00e0 configurant Plex", "discovery_no_file": "No s'ha trobat cap fitxer de configuraci\u00f3 heretat", "invalid_import": "La configuraci\u00f3 importada \u00e9s inv\u00e0lida", + "non-interactive": "Importaci\u00f3 no interactiva", "token_request_timeout": "S'ha acabat el temps d'espera durant l'obtenci\u00f3 del testimoni.", "unknown": "Ha fallat per motiu desconegut" }, diff --git a/homeassistant/components/plex/.translations/da.json b/homeassistant/components/plex/.translations/da.json index 99d5d4d1685..18dbbb840c3 100644 --- a/homeassistant/components/plex/.translations/da.json +++ b/homeassistant/components/plex/.translations/da.json @@ -4,8 +4,9 @@ "all_configured": "Alle linkede servere er allerede konfigureret", "already_configured": "Denne Plex-server er allerede konfigureret", "already_in_progress": "Plex konfigureres", - "discovery_no_file": "Der blev ikke fundet nogen legacy konfigurationsfil", + "discovery_no_file": "Der blev ikke fundet nogen \u00e6ldre konfigurationsfil", "invalid_import": "Importeret konfiguration er ugyldig", + "non-interactive": "Ikke-interaktiv import", "token_request_timeout": "Timeout ved hentning af token", "unknown": "Mislykkedes af ukendt \u00e5rsag" }, @@ -34,13 +35,13 @@ "title": "V\u00e6lg Plex-server" }, "start_website_auth": { - "description": "Forts\u00e6t for at autorisere p\u00e5 plex.tv.", - "title": "Tilslut Plex-server" + "description": "Forts\u00e6t for at godkende p\u00e5 plex.tv.", + "title": "Forbind Plex-server" }, "user": { "data": { "manual_setup": "Manuel ops\u00e6tning", - "token": "Plex token" + "token": "Plex-token" }, "description": "Indtast et Plex-token til automatisk ops\u00e6tning eller konfigurerer en server manuelt.", "title": "Tilslut Plex-server" @@ -53,7 +54,7 @@ "plex_mp_settings": { "data": { "show_all_controls": "Vis alle kontrolelementer", - "use_episode_art": "Brug episode kunst" + "use_episode_art": "Brug episodekunst" }, "description": "Indstillinger for Plex-medieafspillere" } diff --git a/homeassistant/components/plex/.translations/en.json b/homeassistant/components/plex/.translations/en.json index bf927b7f1be..31211182f47 100644 --- a/homeassistant/components/plex/.translations/en.json +++ b/homeassistant/components/plex/.translations/en.json @@ -6,6 +6,7 @@ "already_in_progress": "Plex is being configured", "discovery_no_file": "No legacy config file found", "invalid_import": "Imported configuration is invalid", + "non-interactive": "Non-interactive import", "token_request_timeout": "Timed out obtaining token", "unknown": "Failed for unknown reason" }, diff --git a/homeassistant/components/plex/.translations/es.json b/homeassistant/components/plex/.translations/es.json index 261ca951490..53dd3228288 100644 --- a/homeassistant/components/plex/.translations/es.json +++ b/homeassistant/components/plex/.translations/es.json @@ -6,6 +6,7 @@ "already_in_progress": "Plex se est\u00e1 configurando", "discovery_no_file": "No se ha encontrado ning\u00fan archivo de configuraci\u00f3n antiguo", "invalid_import": "La configuraci\u00f3n importada no es v\u00e1lida", + "non-interactive": "Importaci\u00f3n no interactiva", "token_request_timeout": "Tiempo de espera agotado para la obtenci\u00f3n del token", "unknown": "Fall\u00f3 por razones desconocidas" }, diff --git a/homeassistant/components/plex/.translations/fr.json b/homeassistant/components/plex/.translations/fr.json index 2eef7a5e9a2..bcd53d2ffae 100644 --- a/homeassistant/components/plex/.translations/fr.json +++ b/homeassistant/components/plex/.translations/fr.json @@ -6,6 +6,7 @@ "already_in_progress": "Plex en cours de configuration", "discovery_no_file": "Aucun fichier de configuration h\u00e9rit\u00e9 trouv\u00e9", "invalid_import": "La configuration import\u00e9e est invalide", + "non-interactive": "Importation non interactive", "token_request_timeout": "D\u00e9lai d'obtention du jeton", "unknown": "\u00c9chec pour une raison inconnue" }, diff --git a/homeassistant/components/plex/.translations/it.json b/homeassistant/components/plex/.translations/it.json index 8f61f968dba..06c20660fef 100644 --- a/homeassistant/components/plex/.translations/it.json +++ b/homeassistant/components/plex/.translations/it.json @@ -6,6 +6,7 @@ "already_in_progress": "Plex \u00e8 in fase di configurazione", "discovery_no_file": "Nessun file di configurazione legacy trovato", "invalid_import": "La configurazione importata non \u00e8 valida", + "non-interactive": "Importazione non interattiva", "token_request_timeout": "Timeout per l'ottenimento del token", "unknown": "Non riuscito per motivo sconosciuto" }, diff --git a/homeassistant/components/plex/.translations/ko.json b/homeassistant/components/plex/.translations/ko.json index f8e78945802..cf5a7946b9d 100644 --- a/homeassistant/components/plex/.translations/ko.json +++ b/homeassistant/components/plex/.translations/ko.json @@ -6,6 +6,7 @@ "already_in_progress": "Plex \ub97c \uad6c\uc131 \uc911\uc785\ub2c8\ub2e4", "discovery_no_file": "\ub808\uac70\uc2dc \uad6c\uc131 \ud30c\uc77c\uc744 \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", "invalid_import": "\uac00\uc838\uc628 \uad6c\uc131 \ub0b4\uc6a9\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "non-interactive": "\ube44 \ub300\ud654\ud615 \uac00\uc838\uc624\uae30", "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" }, diff --git a/homeassistant/components/plex/.translations/lb.json b/homeassistant/components/plex/.translations/lb.json index 7b0f7232976..c6fcabc40d7 100644 --- a/homeassistant/components/plex/.translations/lb.json +++ b/homeassistant/components/plex/.translations/lb.json @@ -6,6 +6,7 @@ "already_in_progress": "Plex g\u00ebtt konfigur\u00e9iert", "discovery_no_file": "Kee Konfiguratioun Fichier am ale Format fonnt.", "invalid_import": "D\u00e9i importiert Konfiguratioun ass ong\u00eblteg", + "non-interactive": "Net interaktiven Import", "token_request_timeout": "Z\u00e4it Iwwerschreidung beim kr\u00e9ien vum Jeton", "unknown": "Onbekannte Feeler opgetrueden" }, diff --git a/homeassistant/components/plex/.translations/nl.json b/homeassistant/components/plex/.translations/nl.json index c971ebb4762..515ee8798c7 100644 --- a/homeassistant/components/plex/.translations/nl.json +++ b/homeassistant/components/plex/.translations/nl.json @@ -6,6 +6,7 @@ "already_in_progress": "Plex wordt geconfigureerd", "discovery_no_file": "Geen legacy configuratiebestand gevonden", "invalid_import": "Ge\u00efmporteerde configuratie is ongeldig", + "non-interactive": "Niet-interactieve import", "token_request_timeout": "Time-out verkrijgen van token", "unknown": "Mislukt om onbekende reden" }, diff --git a/homeassistant/components/plex/.translations/no.json b/homeassistant/components/plex/.translations/no.json index 8ebd2b69bb9..cc6dac8a35b 100644 --- a/homeassistant/components/plex/.translations/no.json +++ b/homeassistant/components/plex/.translations/no.json @@ -6,6 +6,7 @@ "already_in_progress": "Plex blir konfigurert", "discovery_no_file": "Ingen eldre konfigurasjonsfil ble funnet", "invalid_import": "Den importerte konfigurasjonen er ugyldig", + "non-interactive": "Ikke-interaktiv import", "token_request_timeout": "Tidsavbrudd ved innhenting av token", "unknown": "Mislyktes av ukjent \u00e5rsak" }, diff --git a/homeassistant/components/plex/.translations/pl.json b/homeassistant/components/plex/.translations/pl.json index b4ed6134106..d752899b9f0 100644 --- a/homeassistant/components/plex/.translations/pl.json +++ b/homeassistant/components/plex/.translations/pl.json @@ -6,6 +6,7 @@ "already_in_progress": "Plex jest konfigurowany", "discovery_no_file": "Nie znaleziono pliku konfiguracyjnego", "invalid_import": "Zaimportowana konfiguracja jest nieprawid\u0142owa", + "non-interactive": "Nieinteraktywny import", "token_request_timeout": "Przekroczono limit czasu na uzyskanie tokena.", "unknown": "Nieznany b\u0142\u0105d" }, diff --git a/homeassistant/components/plex/.translations/pt-BR.json b/homeassistant/components/plex/.translations/pt-BR.json index 9a759e309c2..be97c7fdcb7 100644 --- a/homeassistant/components/plex/.translations/pt-BR.json +++ b/homeassistant/components/plex/.translations/pt-BR.json @@ -1,4 +1,9 @@ { + "config": { + "abort": { + "non-interactive": "Importa\u00e7\u00e3o n\u00e3o interativa" + } + }, "options": { "step": { "plex_mp_settings": { diff --git a/homeassistant/components/plex/.translations/ru.json b/homeassistant/components/plex/.translations/ru.json index bce55d35baa..334a4e353d4 100644 --- a/homeassistant/components/plex/.translations/ru.json +++ b/homeassistant/components/plex/.translations/ru.json @@ -6,12 +6,13 @@ "already_in_progress": "\u0412\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430.", "discovery_no_file": "\u0421\u0442\u0430\u0440\u044b\u0439 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u044b\u0439 \u0444\u0430\u0439\u043b \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d.", "invalid_import": "\u0418\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0435\u0432\u0435\u0440\u043d\u0430.", + "non-interactive": "\u041d\u0435\u0438\u043d\u0442\u0435\u0440\u0430\u043a\u0442\u0438\u0432\u043d\u044b\u0439 \u0438\u043c\u043f\u043e\u0440\u0442.", "token_request_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0442\u043e\u043a\u0435\u043d\u0430.", "unknown": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u043e\u0439 \u043f\u0440\u0438\u0447\u0438\u043d\u0435." }, "error": { "faulty_credentials": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", - "no_servers": "\u041d\u0435\u0442 \u0441\u0435\u0440\u0432\u0435\u0440\u043e\u0432, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0445 \u0441 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u044c\u044e.", + "no_servers": "\u041d\u0435\u0442 \u0441\u0435\u0440\u0432\u0435\u0440\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.", "no_token": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u043e\u043a\u0435\u043d \u0438\u043b\u0438 \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0440\u0443\u0447\u043d\u0443\u044e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443.", "not_found": "\u0421\u0435\u0440\u0432\u0435\u0440 Plex \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d." }, diff --git a/homeassistant/components/plex/.translations/sl.json b/homeassistant/components/plex/.translations/sl.json index 7426e7f95ed..1ff93cff650 100644 --- a/homeassistant/components/plex/.translations/sl.json +++ b/homeassistant/components/plex/.translations/sl.json @@ -6,6 +6,7 @@ "already_in_progress": "Plex se konfigurira", "discovery_no_file": "Podatkovne konfiguracijske datoteke ni bilo mogo\u010de najti", "invalid_import": "Uvo\u017eena konfiguracija ni veljavna", + "non-interactive": "Neinteraktivni uvoz", "token_request_timeout": "Potekla \u010dasovna omejitev za pridobitev \u017eetona", "unknown": "Ni uspelo iz neznanega razloga" }, diff --git a/homeassistant/components/plex/.translations/zh-Hant.json b/homeassistant/components/plex/.translations/zh-Hant.json index 2d4ce1ea6aa..5c05d2104f9 100644 --- a/homeassistant/components/plex/.translations/zh-Hant.json +++ b/homeassistant/components/plex/.translations/zh-Hant.json @@ -6,6 +6,7 @@ "already_in_progress": "Plex \u5df2\u7d93\u8a2d\u5b9a", "discovery_no_file": "\u627e\u4e0d\u5230\u820a\u7248\u8a2d\u5b9a\u6a94\u6848", "invalid_import": "\u532f\u5165\u4e4b\u8a2d\u5b9a\u7121\u6548", + "non-interactive": "\u7121\u4e92\u52d5\u532f\u5165", "token_request_timeout": "\u53d6\u5f97\u5bc6\u9470\u903e\u6642", "unknown": "\u672a\u77e5\u539f\u56e0\u5931\u6557" }, diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index 4a575722826..89659769192 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -1,5 +1,6 @@ """Support to embed Plex.""" import asyncio +import functools import logging import plexapi.exceptions @@ -26,16 +27,17 @@ from homeassistant.helpers.dispatcher import ( ) from .const import ( - CONF_USE_EPISODE_ART, - CONF_SHOW_ALL_CONTROLS, CONF_SERVER, CONF_SERVER_IDENTIFIER, + CONF_SHOW_ALL_CONTROLS, + CONF_USE_EPISODE_ART, DEFAULT_PORT, DEFAULT_SSL, DEFAULT_VERIFY_SSL, DISPATCHERS, DOMAIN as PLEX_DOMAIN, PLATFORMS, + PLATFORMS_COMPLETED, PLEX_MEDIA_PLAYER_OPTIONS, PLEX_SERVER_CONFIG, PLEX_UPDATE_PLATFORMS_SIGNAL, @@ -71,18 +73,21 @@ CONFIG_SCHEMA = vol.Schema({PLEX_DOMAIN: SERVER_CONFIG_SCHEMA}, extra=vol.ALLOW_ _LOGGER = logging.getLogger(__package__) -def setup(hass, config): +async def async_setup(hass, config): """Set up the Plex component.""" - hass.data.setdefault(PLEX_DOMAIN, {SERVERS: {}, DISPATCHERS: {}, WEBSOCKETS: {}}) + hass.data.setdefault( + PLEX_DOMAIN, + {SERVERS: {}, DISPATCHERS: {}, WEBSOCKETS: {}, PLATFORMS_COMPLETED: {}}, + ) plex_config = config.get(PLEX_DOMAIN, {}) if plex_config: - _setup_plex(hass, plex_config) + _async_setup_plex(hass, plex_config) return True -def _setup_plex(hass, config): +def _async_setup_plex(hass, config): """Pass configuration to a config flow.""" server_config = dict(config) if MP_DOMAIN in server_config: @@ -140,11 +145,7 @@ async def async_setup_entry(hass, entry): ) server_id = plex_server.machine_identifier hass.data[PLEX_DOMAIN][SERVERS][server_id] = plex_server - - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.data[PLEX_DOMAIN][PLATFORMS_COMPLETED][server_id] = set() entry.add_update_listener(async_options_updated) @@ -164,9 +165,13 @@ async def async_setup_entry(hass, entry): websocket = PlexWebsocket( plex_server.plex_server, update_plex, session=session, verify_ssl=verify_ssl ) - hass.loop.create_task(websocket.listen()) hass.data[PLEX_DOMAIN][WEBSOCKETS][server_id] = websocket + def start_websocket_session(platform, _): + hass.data[PLEX_DOMAIN][PLATFORMS_COMPLETED][server_id].add(platform) + if hass.data[PLEX_DOMAIN][PLATFORMS_COMPLETED][server_id] == PLATFORMS: + hass.loop.create_task(websocket.listen()) + def close_websocket_session(_): websocket.close() @@ -175,6 +180,12 @@ async def async_setup_entry(hass, entry): ) hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub) + for platform in PLATFORMS: + task = hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, platform) + ) + task.add_done_callback(functools.partial(start_websocket_session, platform)) + return True diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py index cb79c08b16e..d38d13c847e 100644 --- a/homeassistant/components/plex/config_flow.py +++ b/homeassistant/components/plex/config_flow.py @@ -8,12 +8,12 @@ from plexauth import PlexAuth import requests.exceptions import voluptuous as vol -from homeassistant.components.http.view import HomeAssistantView -from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant import config_entries +from homeassistant.components.http.view import HomeAssistantView from homeassistant.components.media_player import DOMAIN as MP_DOMAIN -from homeassistant.const import CONF_URL, CONF_TOKEN, CONF_SSL, CONF_VERIFY_SSL +from homeassistant.const import CONF_SSL, CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL from homeassistant.core import callback +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util.json import load_json from .const import ( # pylint: disable=unused-import @@ -22,16 +22,16 @@ from .const import ( # pylint: disable=unused-import CONF_CLIENT_IDENTIFIER, CONF_SERVER, CONF_SERVER_IDENTIFIER, - CONF_USE_EPISODE_ART, CONF_SHOW_ALL_CONTROLS, + CONF_USE_EPISODE_ART, DEFAULT_VERIFY_SSL, DOMAIN, PLEX_CONFIG_FILE, PLEX_SERVER_CONFIG, X_PLEX_DEVICE_NAME, - X_PLEX_VERSION, - X_PLEX_PRODUCT, X_PLEX_PLATFORM, + X_PLEX_PRODUCT, + X_PLEX_VERSION, ) from .errors import NoServersFound, ServerNotSpecified from .server import PlexServer @@ -80,12 +80,17 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Validate a provided configuration.""" errors = {} self.current_login = server_config + is_importing = ( + self.context["source"] # pylint: disable=no-member + == config_entries.SOURCE_IMPORT + ) plex_server = PlexServer(self.hass, server_config) try: await self.hass.async_add_executor_job(plex_server.connect) except NoServersFound: + _LOGGER.error("No servers linked to Plex account") errors["base"] = "no_servers" except (plexapi.exceptions.BadRequest, plexapi.exceptions.Unauthorized): _LOGGER.error("Invalid credentials provided, config not created") @@ -98,6 +103,11 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "not_found" except ServerNotSpecified as available_servers: + if is_importing: + _LOGGER.warning( + "Imported configuration has multiple available Plex servers. Specify server in configuration or add a new Integration." + ) + return self.async_abort(reason="non-interactive") self.available_servers = available_servers.args[0] return await self.async_step_select_server() @@ -106,12 +116,17 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="unknown") if errors: + if is_importing: + return self.async_abort(reason="non-interactive") return self.async_show_form(step_id="start_website_auth", errors=errors) server_id = plex_server.machine_identifier for entry in self._async_current_entries(): if entry.data[CONF_SERVER_IDENTIFIER] == server_id: + _LOGGER.debug( + "Plex server already configured: %s", entry.data[CONF_SERVER] + ) return self.async_abort(reason="already_configured") url = plex_server.url_in_use diff --git a/homeassistant/components/plex/const.py b/homeassistant/components/plex/const.py index d3c79e60bc4..ad62bade1fd 100644 --- a/homeassistant/components/plex/const.py +++ b/homeassistant/components/plex/const.py @@ -9,7 +9,8 @@ DEFAULT_SSL = False DEFAULT_VERIFY_SSL = True DISPATCHERS = "dispatchers" -PLATFORMS = ["media_player", "sensor"] +PLATFORMS = frozenset(["media_player", "sensor"]) +PLATFORMS_COMPLETED = "platforms_completed" SERVERS = "servers" WEBSOCKETS = "websockets" diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index 29bdbf34b60..922a9c14288 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -6,7 +6,7 @@ "requirements": [ "plexapi==3.3.0", "plexauth==0.0.5", - "plexwebsocket==0.0.5" + "plexwebsocket==0.0.6" ], "dependencies": [ "http" diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index d6720fd9e95..ad5fb2f73f1 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -68,6 +68,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): hass, PLEX_NEW_MP_SIGNAL.format(server_id), async_new_media_players ) hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub) + _LOGGER.debug("New entity listener created") @callback diff --git a/homeassistant/components/plex/sensor.py b/homeassistant/components/plex/sensor.py index 2a994b08a7b..2aed57946eb 100644 --- a/homeassistant/components/plex/sensor.py +++ b/homeassistant/components/plex/sensor.py @@ -86,6 +86,11 @@ class PlexSensor(Entity): """Return the unit this state is expressed in.""" return "Watching" + @property + def icon(self): + """Return the icon of the sensor.""" + return "mdi:plex" + @property def device_state_attributes(self): """Return the state attributes.""" diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index 69838fbf27f..46602cf6552 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -77,7 +77,7 @@ class PlexServer: self.server_choice = ( self._server_name if self._server_name else available_servers[0][0] ) - self._plex_server = account.resource(self.server_choice).connect() + self._plex_server = account.resource(self.server_choice).connect(timeout=10) def _connect_with_url(): session = None @@ -192,7 +192,7 @@ class PlexServer: @property def url_in_use(self): """Return URL used for connected Plex server.""" - return self._plex_server._baseurl # pylint: disable=W0212 + return self._plex_server._baseurl # pylint: disable=protected-access @property def use_episode_art(self): diff --git a/homeassistant/components/plex/strings.json b/homeassistant/components/plex/strings.json index aff79acc2ed..b6491db350c 100644 --- a/homeassistant/components/plex/strings.json +++ b/homeassistant/components/plex/strings.json @@ -25,6 +25,7 @@ "already_in_progress": "Plex is being configured", "discovery_no_file": "No legacy config file found", "invalid_import": "Imported configuration is invalid", + "non-interactive": "Non-interactive import", "token_request_timeout": "Timed out obtaining token", "unknown": "Failed for unknown reason" } diff --git a/homeassistant/components/plugwise/climate.py b/homeassistant/components/plugwise/climate.py index fa1ac86941b..bc303caeca8 100644 --- a/homeassistant/components/plugwise/climate.py +++ b/homeassistant/components/plugwise/climate.py @@ -2,18 +2,17 @@ import logging -import voluptuous as vol import haanna +import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice from homeassistant.components.climate.const import ( - CURRENT_HVAC_HEAT, CURRENT_HVAC_COOL, + CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, + HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, - HVAC_MODE_AUTO, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, ) @@ -27,6 +26,7 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.exceptions import PlatformNotReady +import homeassistant.helpers.config_validation as cv SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE diff --git a/homeassistant/components/plum_lightpad/__init__.py b/homeassistant/components/plum_lightpad/__init__.py index 67a3b60e8ba..bfdf67a0f40 100644 --- a/homeassistant/components/plum_lightpad/__init__.py +++ b/homeassistant/components/plum_lightpad/__init__.py @@ -2,6 +2,7 @@ import asyncio import logging +from plumlightpad import Plum import voluptuous as vol from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP @@ -30,7 +31,6 @@ PLUM_DATA = "plum" async def async_setup(hass, config): """Plum Lightpad Platform initialization.""" - from plumlightpad import Plum conf = config[DOMAIN] plum = Plum(conf[CONF_USERNAME], conf[CONF_PASSWORD]) diff --git a/homeassistant/components/point/.translations/da.json b/homeassistant/components/point/.translations/da.json index 109bcbe6c37..4b6017ddd01 100644 --- a/homeassistant/components/point/.translations/da.json +++ b/homeassistant/components/point/.translations/da.json @@ -24,7 +24,7 @@ "flow_impl": "Udbyder" }, "description": "V\u00e6lg hvilken godkendelsesudbyder du vil godkende med Point.", - "title": "Godkendelses udbyder" + "title": "Godkendelsesudbyder" } }, "title": "Minut Point" diff --git a/homeassistant/components/point/.translations/ru.json b/homeassistant/components/point/.translations/ru.json index 48751096948..b0fc5a61f72 100644 --- a/homeassistant/components/point/.translations/ru.json +++ b/homeassistant/components/point/.translations/ru.json @@ -16,7 +16,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\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Minut, \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.", + "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 Minut, \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.", "title": "Minut Point" }, "user": { diff --git a/homeassistant/components/point/__init__.py b/homeassistant/components/point/__init__.py index 9c5ec4d5529..9abae9ab025 100644 --- a/homeassistant/components/point/__init__.py +++ b/homeassistant/components/point/__init__.py @@ -2,6 +2,7 @@ import asyncio import logging +from pypoint import PointSession import voluptuous as vol from homeassistant import config_entries @@ -71,7 +72,6 @@ async def async_setup(hass, config): async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): """Set up Point from a config entry.""" - from pypoint import PointSession def token_saver(token): _LOGGER.debug("Saving updated token") diff --git a/homeassistant/components/point/alarm_control_panel.py b/homeassistant/components/point/alarm_control_panel.py index f9e725f6c8e..e86b3dd42e8 100644 --- a/homeassistant/components/point/alarm_control_panel.py +++ b/homeassistant/components/point/alarm_control_panel.py @@ -2,6 +2,7 @@ import logging from homeassistant.components.alarm_control_panel import DOMAIN, AlarmControlPanel +from homeassistant.components.alarm_control_panel.const import SUPPORT_ALARM_ARM_AWAY from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_DISARMED, @@ -88,6 +89,11 @@ class MinutPointAlarmControl(AlarmControlPanel): """Return state of the device.""" return EVENT_MAP.get(self._home["alarm_status"], STATE_ALARM_ARMED_AWAY) + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_AWAY + @property def changed_by(self): """Return the user the last change was triggered by.""" diff --git a/homeassistant/components/point/config_flow.py b/homeassistant/components/point/config_flow.py index f354411ab42..3312931085e 100644 --- a/homeassistant/components/point/config_flow.py +++ b/homeassistant/components/point/config_flow.py @@ -4,6 +4,7 @@ from collections import OrderedDict import logging import async_timeout +from pypoint import PointSession import voluptuous as vol from homeassistant import config_entries @@ -109,7 +110,6 @@ class PointFlowHandler(config_entries.ConfigFlow): async def _get_authorization_url(self): """Create Minut Point session and get authorization url.""" - from pypoint import PointSession flow = self.hass.data[DATA_FLOW_IMPL][self.flow_impl] client_id = flow[CLIENT_ID] @@ -138,7 +138,6 @@ class PointFlowHandler(config_entries.ConfigFlow): async def _async_create_session(self, code): """Create point session and entries.""" - from pypoint import PointSession flow = self.hass.data[DATA_FLOW_IMPL][DOMAIN] client_id = flow[CLIENT_ID] diff --git a/homeassistant/components/point/manifest.json b/homeassistant/components/point/manifest.json index 4c29f37e67c..1c74052ee7e 100644 --- a/homeassistant/components/point/manifest.json +++ b/homeassistant/components/point/manifest.json @@ -3,13 +3,7 @@ "name": "Point", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/point", - "requirements": [ - "pypoint==1.1.2" - ], - "dependencies": [ - "webhook" - ], - "codeowners": [ - "@fredrike" - ] + "requirements": ["pypoint==1.1.2"], + "dependencies": ["webhook", "http"], + "codeowners": ["@fredrike"] } diff --git a/homeassistant/components/postnl/sensor.py b/homeassistant/components/postnl/sensor.py index 6155f58519a..2e1f8176835 100644 --- a/homeassistant/components/postnl/sensor.py +++ b/homeassistant/components/postnl/sensor.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +from postnl_api import PostNL_API, UnauthorizedException import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -36,7 +37,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the PostNL sensor platform.""" - from postnl_api import PostNL_API, UnauthorizedException username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) diff --git a/homeassistant/components/prezzibenzina/sensor.py b/homeassistant/components/prezzibenzina/sensor.py index f1f41ba46ba..c985f96e6c6 100644 --- a/homeassistant/components/prezzibenzina/sensor.py +++ b/homeassistant/components/prezzibenzina/sensor.py @@ -3,6 +3,7 @@ import datetime as dt from datetime import timedelta import logging +from prezzibenzina import PrezziBenzinaPy import voluptuous as vol from homeassistant.const import ATTR_ATTRIBUTION, ATTR_TIME, CONF_NAME @@ -43,7 +44,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the PrezziBenzina sensor platform.""" - from prezzibenzina import PrezziBenzinaPy station = config[CONF_STATION] name = config.get(CONF_NAME) diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py index 8eeb9325bc0..71d56cda18a 100644 --- a/homeassistant/components/prometheus/__init__.py +++ b/homeassistant/components/prometheus/__init__.py @@ -284,15 +284,6 @@ class PrometheusMetrics: ) metric.labels(**self._labels(state)).set(current_temp) - metric = self._metric( - "climate_state", self.prometheus_cli.Gauge, "State of the thermostat (0/1)" - ) - try: - value = self.state_as_number(state) - metric.labels(**self._labels(state)).set(value) - except ValueError: - pass - def _handle_sensor(self, state): unit = self._unit_string(state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)) diff --git a/homeassistant/components/prowl/notify.py b/homeassistant/components/prowl/notify.py index ab6d1b498a0..d5167ebfdc9 100644 --- a/homeassistant/components/prowl/notify.py +++ b/homeassistant/components/prowl/notify.py @@ -5,10 +5,6 @@ import logging import async_timeout import voluptuous as vol -from homeassistant.const import CONF_API_KEY -from homeassistant.helpers.aiohttp_client import async_get_clientsession -import homeassistant.helpers.config_validation as cv - from homeassistant.components.notify import ( ATTR_DATA, ATTR_TITLE, @@ -16,6 +12,9 @@ from homeassistant.components.notify import ( PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.const import CONF_API_KEY +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) _RESOURCE = "https://api.prowlapp.com/publicapi/" @@ -60,7 +59,7 @@ class ProwlNotificationService(BaseNotificationService): if response.status != 200 or "error" in result: _LOGGER.error( - "Prowl service returned http " "status %d, response %s", + "Prowl service returned http status %d, response %s", response.status, result, ) diff --git a/homeassistant/components/proximity/__init__.py b/homeassistant/components/proximity/__init__.py index 1f86958d08e..7e5f6436757 100644 --- a/homeassistant/components/proximity/__init__.py +++ b/homeassistant/components/proximity/__init__.py @@ -10,7 +10,6 @@ from homeassistant.helpers.event import track_state_change from homeassistant.util.distance import convert from homeassistant.util.location import distance - # mypy: allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) @@ -269,7 +268,7 @@ class Proximity(Entity): self.nearest = entity_name self.schedule_update_ha_state() _LOGGER.debug( - "proximity.%s update entity: distance=%s: direction=%s: " "device=%s", + "proximity.%s update entity: distance=%s: direction=%s: device=%s", self.friendly_name, round(dist_to_zone), direction_of_travel, diff --git a/homeassistant/components/proxmoxve/__init__.py b/homeassistant/components/proxmoxve/__init__.py new file mode 100644 index 00000000000..246dc2d48ad --- /dev/null +++ b/homeassistant/components/proxmoxve/__init__.py @@ -0,0 +1,154 @@ +"""Support for Proxmox VE.""" +from enum import Enum +import logging +import time + +from proxmoxer import ProxmoxAPI +from proxmoxer.backends.https import AuthenticationError +import voluptuous as vol + +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, + CONF_VERIFY_SSL, +) +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "proxmoxve" +PROXMOX_CLIENTS = "proxmox_clients" +CONF_REALM = "realm" +CONF_NODE = "node" +CONF_NODES = "nodes" +CONF_VMS = "vms" +CONF_CONTAINERS = "containers" + +DEFAULT_PORT = 8006 +DEFAULT_REALM = "pam" +DEFAULT_VERIFY_SSL = True + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.All( + cv.ensure_list, + [ + vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_REALM, default=DEFAULT_REALM): cv.string, + vol.Optional( + CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL + ): cv.boolean, + vol.Required(CONF_NODES): vol.All( + cv.ensure_list, + [ + vol.Schema( + { + vol.Required(CONF_NODE): cv.string, + vol.Optional(CONF_VMS, default=[]): [ + cv.positive_int + ], + vol.Optional(CONF_CONTAINERS, default=[]): [ + cv.positive_int + ], + } + ) + ], + ), + } + ) + ], + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +def setup(hass, config): + """Set up the component.""" + + # Create API Clients for later use + hass.data[PROXMOX_CLIENTS] = {} + for entry in config[DOMAIN]: + host = entry[CONF_HOST] + port = entry[CONF_PORT] + user = entry[CONF_USERNAME] + realm = entry[CONF_REALM] + password = entry[CONF_PASSWORD] + verify_ssl = entry[CONF_VERIFY_SSL] + + try: + # Construct an API client with the given data for the given host + proxmox_client = ProxmoxClient( + host, port, user, realm, password, verify_ssl + ) + proxmox_client.build_client() + except AuthenticationError: + _LOGGER.warning( + "Invalid credentials for proxmox instance %s:%d", host, port + ) + continue + + hass.data[PROXMOX_CLIENTS][f"{host}:{port}"] = proxmox_client + + if hass.data[PROXMOX_CLIENTS]: + hass.helpers.discovery.load_platform( + "binary_sensor", DOMAIN, {"entries": config[DOMAIN]}, config + ) + return True + + return False + + +class ProxmoxItemType(Enum): + """Represents the different types of machines in Proxmox.""" + + qemu = 0 + lxc = 1 + + +class ProxmoxClient: + """A wrapper for the proxmoxer ProxmoxAPI client.""" + + def __init__(self, host, port, user, realm, password, verify_ssl): + """Initialize the ProxmoxClient.""" + + self._host = host + self._port = port + self._user = user + self._realm = realm + self._password = password + self._verify_ssl = verify_ssl + + self._proxmox = None + self._connection_start_time = None + + def build_client(self): + """Construct the ProxmoxAPI client.""" + + self._proxmox = ProxmoxAPI( + self._host, + port=self._port, + user=f"{self._user}@{self._realm}", + password=self._password, + verify_ssl=self._verify_ssl, + ) + + self._connection_start_time = time.time() + + def get_api_client(self): + """Return the ProxmoxAPI client and rebuild it if necessary.""" + + connection_age = time.time() - self._connection_start_time + + # Workaround for the Proxmoxer bug where the connection stops working after some time + if connection_age > 30 * 60: + self.build_client() + + return self._proxmox diff --git a/homeassistant/components/proxmoxve/binary_sensor.py b/homeassistant/components/proxmoxve/binary_sensor.py new file mode 100644 index 00000000000..15b1f1483e1 --- /dev/null +++ b/homeassistant/components/proxmoxve/binary_sensor.py @@ -0,0 +1,112 @@ +"""Binary sensor to read Proxmox VE data.""" +import logging + +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.const import ATTR_ATTRIBUTION, CONF_HOST, CONF_PORT + +from . import CONF_CONTAINERS, CONF_NODES, CONF_VMS, PROXMOX_CLIENTS, ProxmoxItemType + +ATTRIBUTION = "Data provided by Proxmox VE" +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the sensor platform.""" + + sensors = [] + + for entry in discovery_info["entries"]: + port = entry[CONF_PORT] + + for node in entry[CONF_NODES]: + for virtual_machine in node[CONF_VMS]: + sensors.append( + ProxmoxBinarySensor( + hass.data[PROXMOX_CLIENTS][f"{entry[CONF_HOST]}:{port}"], + node["node"], + ProxmoxItemType.qemu, + virtual_machine, + ) + ) + + for container in node[CONF_CONTAINERS]: + sensors.append( + ProxmoxBinarySensor( + hass.data[PROXMOX_CLIENTS][f"{entry[CONF_HOST]}:{port}"], + node["node"], + ProxmoxItemType.lxc, + container, + ) + ) + + add_entities(sensors, True) + + +class ProxmoxBinarySensor(BinarySensorDevice): + """A binary sensor for reading Proxmox VE data.""" + + def __init__(self, proxmox_client, item_node, item_type, item_id): + """Initialize the binary sensor.""" + self._proxmox_client = proxmox_client + self._item_node = item_node + self._item_type = item_type + self._item_id = item_id + + self._vmname = None + self._name = None + + self._state = None + + @property + def name(self): + """Return the name of the entity.""" + return self._name + + @property + def is_on(self): + """Return true if VM/container is running.""" + return self._state + + @property + def device_state_attributes(self): + """Return device attributes of the entity.""" + return { + "node": self._item_node, + "vmid": self._item_id, + "vmname": self._vmname, + "type": self._item_type.name, + ATTR_ATTRIBUTION: ATTRIBUTION, + } + + def update(self): + """Check if the VM/Container is running.""" + item = self.poll_item() + + if item is None: + _LOGGER.warning("Failed to poll VM/container %s", self._item_id) + return + + self._state = item["status"] == "running" + + def poll_item(self): + """Find the VM/Container with the set item_id.""" + items = ( + self._proxmox_client.get_api_client() + .nodes(self._item_node) + .get(self._item_type.name) + ) + item = next( + (item for item in items if item["vmid"] == str(self._item_id)), None + ) + + if item is None: + _LOGGER.warning("Couldn't find VM/Container with the ID %s", self._item_id) + return None + + if self._vmname is None: + self._vmname = item["name"] + + if self._name is None: + self._name = f"{self._item_node} {self._vmname} running" + + return item diff --git a/homeassistant/components/proxmoxve/manifest.json b/homeassistant/components/proxmoxve/manifest.json new file mode 100644 index 00000000000..9c03038a630 --- /dev/null +++ b/homeassistant/components/proxmoxve/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "proxmoxve", + "name": "Proxmox VE", + "documentation": "https://www.home-assistant.io/integrations/proxmoxve", + "dependencies": [], + "codeowners": ["@k4ds3"], + "requirements": ["proxmoxer==1.0.3"] + } \ No newline at end of file diff --git a/homeassistant/components/proxy/camera.py b/homeassistant/components/proxy/camera.py index 90487120ffe..893fadfe178 100644 --- a/homeassistant/components/proxy/camera.py +++ b/homeassistant/components/proxy/camera.py @@ -7,7 +7,13 @@ import logging from PIL import Image import voluptuous as vol -from homeassistant.components.camera import PLATFORM_SCHEMA, Camera +from homeassistant.components.camera import ( + PLATFORM_SCHEMA, + Camera, + async_get_image, + async_get_mjpeg_stream, + async_get_still_stream, +) from homeassistant.const import CONF_ENTITY_ID, CONF_MODE, CONF_NAME from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv @@ -227,7 +233,7 @@ class ProxyCamera(Camera): return self._last_image self._last_image_time = now - image = await self.hass.components.camera.async_get_image(self._proxied_camera) + image = await async_get_image(self.hass, self._proxied_camera) if not image: _LOGGER.error("Error getting original camera image") return self._last_image @@ -247,12 +253,12 @@ class ProxyCamera(Camera): async def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from camera images.""" if not self._stream_opts: - return await self.hass.components.camera.async_get_mjpeg_stream( - request, self._proxied_camera + return await async_get_mjpeg_stream( + self.hass, request, self._proxied_camera ) - return await self.hass.components.camera.async_get_still_stream( - request, self._async_stream_image, self.content_type, self.frame_interval + return await async_get_still_stream( + request, self._async_stream_image, self.content_type, self.frame_interval, ) @property @@ -263,9 +269,7 @@ class ProxyCamera(Camera): async def _async_stream_image(self): """Return a still image response from the camera.""" try: - image = await self.hass.components.camera.async_get_image( - self._proxied_camera - ) + image = await async_get_image(self.hass, self._proxied_camera) if not image: return None except HomeAssistantError: diff --git a/homeassistant/components/proxy/manifest.json b/homeassistant/components/proxy/manifest.json index 39f7b9064fc..5344ea6fe83 100644 --- a/homeassistant/components/proxy/manifest.json +++ b/homeassistant/components/proxy/manifest.json @@ -2,9 +2,7 @@ "domain": "proxy", "name": "Proxy", "documentation": "https://www.home-assistant.io/integrations/proxy", - "requirements": [ - "pillow==6.2.1" - ], + "requirements": ["pillow==6.2.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/ps4/.translations/bg.json b/homeassistant/components/ps4/.translations/bg.json index 4bea40b206a..fabd9032dc0 100644 --- a/homeassistant/components/ps4/.translations/bg.json +++ b/homeassistant/components/ps4/.translations/bg.json @@ -4,8 +4,8 @@ "credential_error": "\u0413\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u0438\u0437\u0432\u043b\u0438\u0447\u0430\u043d\u0435 \u043d\u0430 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u043e\u043d\u043d\u0438 \u0434\u0430\u043d\u043d\u0438.", "devices_configured": "\u0412\u0441\u0438\u0447\u043a\u0438 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0441\u0430 \u0432\u0435\u0447\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0438.", "no_devices_found": "\u041d\u0435 \u0441\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 PlayStation 4 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430.", - "port_987_bind_error": "\u041d\u0435\u0432\u044a\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442 \u0437\u0430 \u0440\u0435\u0437\u0435\u0440\u0438\u0432\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043f\u043e\u0440\u0442 987.", - "port_997_bind_error": "\u041d\u0435\u0432\u044a\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442 \u0437\u0430 \u0440\u0435\u0437\u0435\u0440\u0438\u0432\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043f\u043e\u0440\u0442 997." + "port_987_bind_error": "\u041d\u0435\u0432\u044a\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442 \u0437\u0430 \u0440\u0435\u0437\u0435\u0440\u0438\u0432\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043f\u043e\u0440\u0442 987. \u041c\u043e\u043b\u044f, \u043f\u0440\u043e\u0447\u0435\u0442\u0435\u0442\u0435 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u0438\u0442\u0435] (https://www.home-assistant.io/components/ps4/) \u0437\u0430 \u0434\u043e\u043f\u044a\u043b\u043d\u0438\u0442\u0435\u043b\u043d\u0430 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f.", + "port_997_bind_error": "\u041d\u0435\u0432\u044a\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442 \u0437\u0430 \u0440\u0435\u0437\u0435\u0440\u0438\u0432\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043f\u043e\u0440\u0442 997. \u041c\u043e\u043b\u044f, \u043f\u0440\u043e\u0447\u0435\u0442\u0435\u0442\u0435 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430](https://www.home-assistant.io/components/ps4/) \u0437\u0430 \u0434\u043e\u043f\u044a\u043b\u043d\u0438\u0442\u0435\u043b\u043d\u0430 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f." }, "error": { "credential_timeout": "\u0412\u0440\u0435\u043c\u0435\u0442\u043e \u0437\u0430 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 \u0443\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u0437\u0430 \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0438\u0437\u0442\u0435\u0447\u0435. \u041d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0435 \"\u0418\u0437\u043f\u0440\u0430\u0449\u0430\u043d\u0435\" \u0437\u0430 \u0434\u0430 \u0440\u0435\u0441\u0442\u0430\u0440\u0442\u0438\u0440\u0430\u0442\u0435.", @@ -25,7 +25,7 @@ "name": "\u0418\u043c\u0435", "region": "\u0420\u0435\u0433\u0438\u043e\u043d" }, - "description": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u0432\u0430\u0448\u0430\u0442\u0430 PlayStation 4 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f. \u0417\u0430 \u201ePIN\u201c \u043e\u0442\u0438\u0434\u0435\u0442\u0435 \u0432 \u201eSettings\u201c \u043d\u0430 \u0432\u0430\u0448\u0430\u0442\u0430 PlayStation 4 \u043a\u043e\u043d\u0437\u043e\u043b\u0430. \u0421\u043b\u0435\u0434 \u0442\u043e\u0432\u0430 \u043f\u0440\u0435\u043c\u0438\u043d\u0435\u0442\u0435 \u043a\u044a\u043c \u201eMobile App Connection Settings\u201c \u0438 \u0438\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u201eAdd Device\u201c. \u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043f\u043e\u043a\u0430\u0437\u0430\u043d\u0438\u044f PIN \u043a\u043e\u0434.", + "description": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u0432\u0430\u0448\u0430\u0442\u0430 PlayStation 4 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f. \u0417\u0430 \u201ePIN\u201c \u043e\u0442\u0438\u0434\u0435\u0442\u0435 \u0432 \u201eSettings\u201c \u043d\u0430 \u0412\u0430\u0448\u0430\u0442\u0430 PlayStation 4 \u043a\u043e\u043d\u0437\u043e\u043b\u0430. \u0421\u043b\u0435\u0434 \u0442\u043e\u0432\u0430 \u043f\u0440\u0435\u043c\u0438\u043d\u0435\u0442\u0435 \u043a\u044a\u043c \u201eMobile App Connection Settings\u201c \u0438 \u0438\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u201eAdd Device\u201c. \u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043f\u043e\u043a\u0430\u0437\u0430\u043d\u0438\u044f PIN \u043a\u043e\u0434. \u041c\u043e\u043b\u044f, \u043f\u0440\u043e\u0447\u0435\u0442\u0435\u0442\u0435 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430](https://www.home-assistant.io/components/ps4/) \u0437\u0430 \u0434\u043e\u043f\u044a\u043b\u043d\u0438\u0442\u0435\u043b\u043d\u0430 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f.", "title": "PlayStation 4" }, "mode": { diff --git a/homeassistant/components/ps4/.translations/da.json b/homeassistant/components/ps4/.translations/da.json index e9aca23bb43..cef13db3150 100644 --- a/homeassistant/components/ps4/.translations/da.json +++ b/homeassistant/components/ps4/.translations/da.json @@ -3,19 +3,19 @@ "abort": { "credential_error": "Fejl ved hentning af legitimationsoplysninger.", "devices_configured": "Alle de fundne enheder er allerede konfigureret.", - "no_devices_found": "Ingen PlayStation 4 enheder fundet p\u00e5 netv\u00e6rket.", + "no_devices_found": "Der blev ikke fundet nogen PlayStation 4-enheder p\u00e5 netv\u00e6rket.", "port_987_bind_error": "Kunne ikke binde til port 987. Se [dokumentationen](https://www.home-assistant.io/components/ps4/) for yderligere oplysninger.", "port_997_bind_error": "Kunne ikke binde til port 997. Se [dokumentationen](https://www.home-assistant.io/components/ps4/) for yderligere oplysninger." }, "error": { "credential_timeout": "Tjenesten for legitimationsoplysninger fik timeout. Tryk p\u00e5 send for at genstarte.", "login_failed": "Kunne ikke parre med PlayStation 4. Kontroller PIN er korrekt.", - "no_ipaddress": "Indtast IP adressen p\u00e5 den PlayStation 4 du gerne vil konfigurere.", + "no_ipaddress": "Indtast IP-adressen p\u00e5 den PlayStation 4, du gerne vil konfigurere.", "not_ready": "PlayStation 4 er ikke t\u00e6ndt eller tilsluttet til netv\u00e6rket." }, "step": { "creds": { - "description": "Legitimationsoplysninger er n\u00f8dvendige. Tryk p\u00e5 'Send' og derefter i PS4 2nd Screen App, v\u00e6lg opdater enheder og v\u00e6lg 'Home-Assistant' -enheden for at forts\u00e6tte.", + "description": "Der kr\u00e6ves legitimationsoplysninger. Tryk p\u00e5 'Indsend' og derefter i PS4 2. sk\u00e6rm-app, opdater enheder, og v\u00e6lg 'Home Assistant'-enhed for at forts\u00e6tte.", "title": "PlayStation 4" }, "link": { @@ -25,15 +25,15 @@ "name": "Navn", "region": "Omr\u00e5de" }, - "description": "Indtast dine PlayStation 4 oplysninger. For 'PIN' skal du navigere til 'Indstillinger' p\u00e5 din PlayStation 4 konsol. G\u00e5 derefter til 'Indstillinger for mobilapp-forbindelse' og v\u00e6lg 'Tilf\u00f8j enhed'. Indtast den PIN der vises.", + "description": "Indtast dine PlayStation 4-oplysninger. For 'PIN' skal du navigere til 'Indstillinger' p\u00e5 din PlayStation 4-konsol. Naviger derefter til 'Mobile App Connection Settings' og v\u00e6lg 'Add Device'. Indtast den pinkode, der vises. Se [dokumentation](https://www.home-assistant.io/components/ps4/) for yderligere oplysninger.", "title": "PlayStation 4" }, "mode": { "data": { - "ip_address": "IP adresse (Efterlad tom, hvis du bruger Auto Discovery).", + "ip_address": "IP-adresse (lad det v\u00e6re tomt, hvis du bruger automatisk registrering).", "mode": "Konfigurationstilstand" }, - "description": "V\u00e6lg tilstand til konfiguration. IP-adressefeltet kan st\u00e5 tomt hvis du v\u00e6lger Auto Discovery, da enheder automatisk bliver fundet.", + "description": "V\u00e6lg tilstand for konfiguration. IP-adressefeltet kan v\u00e6re tomt, hvis du v\u00e6lger automatisk registrering, da enheder automatisk bliver fundet.", "title": "PlayStation 4" } }, diff --git a/homeassistant/components/ps4/.translations/ko.json b/homeassistant/components/ps4/.translations/ko.json index 25f64cd21e9..46bbd6b309c 100644 --- a/homeassistant/components/ps4/.translations/ko.json +++ b/homeassistant/components/ps4/.translations/ko.json @@ -2,7 +2,7 @@ "config": { "abort": { "credential_error": "\uc790\uaca9 \uc99d\uba85\uc744 \uac00\uc838\uc624\ub294 \uc911 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", - "devices_configured": "\ubc1c\uacac \ub41c \ubaa8\ub4e0 \uae30\uae30\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "devices_configured": "\ubc1c\uacac\ub41c \ubaa8\ub4e0 \uae30\uae30\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\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." diff --git a/homeassistant/components/ps4/.translations/ru.json b/homeassistant/components/ps4/.translations/ru.json index bf2484d0254..c7ac8d76cf1 100644 --- a/homeassistant/components/ps4/.translations/ru.json +++ b/homeassistant/components/ps4/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "credential_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0438 \u0443\u0447\u0435\u0442\u043d\u044b\u0445 \u0434\u0430\u043d\u043d\u044b\u0445.", + "credential_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0438 \u0443\u0447\u0451\u0442\u043d\u044b\u0445 \u0434\u0430\u043d\u043d\u044b\u0445.", "devices_configured": "\u0412\u0441\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043d\u044b\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b.", "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 PlayStation 4 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", "port_987_bind_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u043f\u043e\u0440\u0442\u0443 987. \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](https://www.home-assistant.io/components/ps4/).", diff --git a/homeassistant/components/ps4/__init__.py b/homeassistant/components/ps4/__init__.py index 205059be608..05e3422fe74 100644 --- a/homeassistant/components/ps4/__init__.py +++ b/homeassistant/components/ps4/__init__.py @@ -2,9 +2,9 @@ import logging import os -import voluptuous as vol from pyps4_2ndscreen.ddp import async_create_ddp_endpoint from pyps4_2ndscreen.media_art import COUNTRIES +import voluptuous as vol from homeassistant.components.media_player.const import ( ATTR_MEDIA_CONTENT_TYPE, @@ -20,7 +20,7 @@ from homeassistant.const import ( ) from homeassistant.core import split_entity_id from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import entity_registry, config_validation as cv +from homeassistant.helpers import config_validation as cv, entity_registry from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import location from homeassistant.util.json import load_json, save_json diff --git a/homeassistant/components/ps4/manifest.json b/homeassistant/components/ps4/manifest.json index 361711c400c..c7e6d1d9ba7 100644 --- a/homeassistant/components/ps4/manifest.json +++ b/homeassistant/components/ps4/manifest.json @@ -3,11 +3,7 @@ "name": "Ps4", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ps4", - "requirements": [ - "pyps4-2ndscreen==1.0.1" - ], + "requirements": ["pyps4-2ndscreen==1.0.4"], "dependencies": [], - "codeowners": [ - "@ktnrg45" - ] + "codeowners": ["@ktnrg45"] } diff --git a/homeassistant/components/ps4/media_player.py b/homeassistant/components/ps4/media_player.py index 91d3a5b13c7..35cdbab2534 100644 --- a/homeassistant/components/ps4/media_player.py +++ b/homeassistant/components/ps4/media_player.py @@ -1,19 +1,18 @@ """Support for PlayStation 4 consoles.""" -import logging import asyncio +import logging +from pyps4_2ndscreen.errors import NotReady, PSDataIncomplete import pyps4_2ndscreen.ps4 as pyps4 -from pyps4_2ndscreen.errors import NotReady -from homeassistant.core import callback from homeassistant.components.media_player import ENTITY_IMAGE_URL, MediaPlayerDevice from homeassistant.components.media_player.const import ( ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_TITLE, - MEDIA_TYPE_GAME, MEDIA_TYPE_APP, - SUPPORT_SELECT_SOURCE, + MEDIA_TYPE_GAME, SUPPORT_PAUSE, + SUPPORT_SELECT_SOURCE, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, @@ -26,9 +25,10 @@ from homeassistant.const import ( CONF_REGION, CONF_TOKEN, STATE_IDLE, - STATE_STANDBY, STATE_PLAYING, + STATE_STANDBY, ) +from homeassistant.core import callback from homeassistant.helpers import device_registry, entity_registry from .const import ( @@ -254,7 +254,6 @@ class PS4Device(MediaPlayerDevice): async def async_get_title_data(self, title_id, name): """Get PS Store Data.""" - from pyps4_2ndscreen.errors import PSDataIncomplete app_name = None art = None diff --git a/homeassistant/components/pulseaudio_loopback/switch.py b/homeassistant/components/pulseaudio_loopback/switch.py index b3bddbf1263..a10c5995d63 100644 --- a/homeassistant/components/pulseaudio_loopback/switch.py +++ b/homeassistant/components/pulseaudio_loopback/switch.py @@ -1,14 +1,14 @@ """Switch logic for loading/unloading pulseaudio loopback modules.""" +from datetime import timedelta import logging import re import socket -from datetime import timedelta import voluptuous as vol from homeassistant import util -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, CONF_HOST, CONF_PORT +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -22,7 +22,7 @@ CONF_TCP_TIMEOUT = "tcp_timeout" DEFAULT_BUFFER_SIZE = 1024 DEFAULT_HOST = "localhost" DEFAULT_NAME = "paloopback" -DEFAULT_PORT = 4712 +DEFAULT_PORT = 4713 DEFAULT_TCP_TIMEOUT = 3 IGNORED_SWITCH_WARN = "Switch is already in the desired state. Ignoring." diff --git a/homeassistant/components/push/camera.py b/homeassistant/components/push/camera.py index 9d53bd7b033..f78966253b7 100644 --- a/homeassistant/components/push/camera.py +++ b/homeassistant/components/push/camera.py @@ -1,22 +1,22 @@ """Camera platform that receives images through HTTP POST.""" -import logging import asyncio - from collections import deque from datetime import timedelta -import voluptuous as vol +import logging + import aiohttp import async_timeout +import voluptuous as vol from homeassistant.components.camera import ( - Camera, PLATFORM_SCHEMA, STATE_IDLE, STATE_RECORDING, + Camera, ) from homeassistant.components.camera.const import DOMAIN -from homeassistant.core import callback from homeassistant.const import CONF_NAME, CONF_TIMEOUT, CONF_WEBHOOK_ID +from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.event import async_track_point_in_utc_time import homeassistant.util.dt as dt_util diff --git a/homeassistant/components/pushbullet/notify.py b/homeassistant/components/pushbullet/notify.py index 76c1e14e5a5..28cb08cc69c 100644 --- a/homeassistant/components/pushbullet/notify.py +++ b/homeassistant/components/pushbullet/notify.py @@ -2,14 +2,9 @@ import logging import mimetypes -from pushbullet import PushBullet -from pushbullet import InvalidKeyError -from pushbullet import PushError +from pushbullet import InvalidKeyError, PushBullet, PushError import voluptuous as vol -from homeassistant.const import CONF_API_KEY -import homeassistant.helpers.config_validation as cv - from homeassistant.components.notify import ( ATTR_DATA, ATTR_TARGET, @@ -18,6 +13,8 @@ from homeassistant.components.notify import ( PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.const import CONF_API_KEY +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/pushbullet/sensor.py b/homeassistant/components/pushbullet/sensor.py index 600b38b6eaf..771ba55586c 100644 --- a/homeassistant/components/pushbullet/sensor.py +++ b/homeassistant/components/pushbullet/sensor.py @@ -2,13 +2,11 @@ import logging import threading -from pushbullet import PushBullet -from pushbullet import InvalidKeyError -from pushbullet import Listener +from pushbullet import InvalidKeyError, Listener, PushBullet import voluptuous as vol -from homeassistant.const import CONF_API_KEY, CONF_MONITORED_CONDITIONS from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_API_KEY, CONF_MONITORED_CONDITIONS import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity diff --git a/homeassistant/components/pushetta/notify.py b/homeassistant/components/pushetta/notify.py index b8911039f3f..c9b008524d6 100644 --- a/homeassistant/components/pushetta/notify.py +++ b/homeassistant/components/pushetta/notify.py @@ -1,17 +1,17 @@ """Pushetta platform for notify component.""" import logging +from pushetta import Pushetta, exceptions as pushetta_exceptions import voluptuous as vol -from homeassistant.const import CONF_API_KEY -import homeassistant.helpers.config_validation as cv - from homeassistant.components.notify import ( ATTR_TITLE, ATTR_TITLE_DEFAULT, PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.const import CONF_API_KEY +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -44,7 +44,6 @@ class PushettaNotificationService(BaseNotificationService): def __init__(self, api_key, channel_name, send_test_msg): """Initialize the service.""" - from pushetta import Pushetta self._api_key = api_key self._channel_name = channel_name @@ -56,15 +55,14 @@ class PushettaNotificationService(BaseNotificationService): def send_message(self, message="", **kwargs): """Send a message to a user.""" - from pushetta import exceptions title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) try: self.pushetta.pushMessage(self._channel_name, f"{title} {message}") - except exceptions.TokenValidationError: + except pushetta_exceptions.TokenValidationError: _LOGGER.error("Please check your access token") self.is_valid = False - except exceptions.ChannelNotFoundError: + except pushetta_exceptions.ChannelNotFoundError: _LOGGER.error("Channel '%s' not found", self._channel_name) self.is_valid = False diff --git a/homeassistant/components/pushover/notify.py b/homeassistant/components/pushover/notify.py index 3f78897838d..064ad91b6b9 100644 --- a/homeassistant/components/pushover/notify.py +++ b/homeassistant/components/pushover/notify.py @@ -1,12 +1,9 @@ """Pushover platform for notify component.""" import logging +from pushover import Client, InitError, RequestError import requests import voluptuous as vol -from pushover import InitError, Client, RequestError - -from homeassistant.const import CONF_API_KEY -import homeassistant.helpers.config_validation as cv from homeassistant.components.notify import ( ATTR_DATA, @@ -16,6 +13,8 @@ from homeassistant.components.notify import ( PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.const import CONF_API_KEY +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/pushsafer/notify.py b/homeassistant/components/pushsafer/notify.py index 758a3390286..436191ab864 100644 --- a/homeassistant/components/pushsafer/notify.py +++ b/homeassistant/components/pushsafer/notify.py @@ -7,8 +7,6 @@ import requests from requests.auth import HTTPBasicAuth import voluptuous as vol -import homeassistant.helpers.config_validation as cv - from homeassistant.components.notify import ( ATTR_DATA, ATTR_TARGET, @@ -17,6 +15,7 @@ from homeassistant.components.notify import ( PLATFORM_SCHEMA, BaseNotificationService, ) +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) _RESOURCE = "https://www.pushsafer.com/api" diff --git a/homeassistant/components/pvoutput/sensor.py b/homeassistant/components/pvoutput/sensor.py index 90084ab7999..169086af3fc 100644 --- a/homeassistant/components/pvoutput/sensor.py +++ b/homeassistant/components/pvoutput/sensor.py @@ -1,22 +1,22 @@ """Support for getting collected information from PVOutput.""" -import logging from collections import namedtuple from datetime import timedelta +import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity -from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.components.rest.sensor import RestData +from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - ATTR_TEMPERATURE, - CONF_API_KEY, - CONF_NAME, ATTR_DATE, + ATTR_TEMPERATURE, ATTR_TIME, ATTR_VOLTAGE, + CONF_API_KEY, + CONF_NAME, ) +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" diff --git a/homeassistant/components/pyload/sensor.py b/homeassistant/components/pyload/sensor.py index 8ffe1ece4a2..fd4461e3e1b 100644 --- a/homeassistant/components/pyload/sensor.py +++ b/homeassistant/components/pyload/sensor.py @@ -8,14 +8,14 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_SSL, CONF_HOST, + CONF_MONITORED_VARIABLES, CONF_NAME, - CONF_PORT, CONF_PASSWORD, + CONF_PORT, + CONF_SSL, CONF_USERNAME, CONTENT_TYPE_JSON, - CONF_MONITORED_VARIABLES, ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity diff --git a/homeassistant/components/python_script/__init__.py b/homeassistant/components/python_script/__init__.py index af0865bc685..0c5886e177c 100644 --- a/homeassistant/components/python_script/__init__.py +++ b/homeassistant/components/python_script/__init__.py @@ -5,6 +5,15 @@ import logging import os import time +from RestrictedPython import compile_restricted_exec +from RestrictedPython.Eval import default_guarded_getitem +from RestrictedPython.Guards import ( + full_write_guard, + guarded_iter_unpack_sequence, + guarded_unpack_sequence, + safe_builtins, +) +from RestrictedPython.Utilities import utility_builtins import voluptuous as vol from homeassistant.const import SERVICE_RELOAD @@ -12,8 +21,8 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.service import async_set_service_schema from homeassistant.loader import bind_hass from homeassistant.util import sanitize_filename -from homeassistant.util.yaml.loader import load_yaml import homeassistant.util.dt as dt_util +from homeassistant.util.yaml.loader import load_yaml _LOGGER = logging.getLogger(__name__) @@ -122,15 +131,6 @@ def execute_script(hass, name, data=None): @bind_hass def execute(hass, filename, source, data=None): """Execute Python source.""" - from RestrictedPython import compile_restricted_exec - from RestrictedPython.Guards import ( - safe_builtins, - full_write_guard, - guarded_iter_unpack_sequence, - guarded_unpack_sequence, - ) - from RestrictedPython.Utilities import utility_builtins - from RestrictedPython.Eval import default_guarded_getitem compiled = compile_restricted_exec(source, filename=filename) @@ -147,7 +147,6 @@ def execute(hass, filename, source, data=None): def protected_getattr(obj, name, default=None): """Restricted method to get attributes.""" - # pylint: disable=too-many-boolean-expressions if name.startswith("async_"): raise ScriptError("Not allowed to access async methods") if ( @@ -176,6 +175,7 @@ def execute(hass, filename, source, data=None): builtins["sorted"] = sorted builtins["time"] = TimeWrapper() builtins["dt_util"] = dt_util + logger = logging.getLogger(f"{__name__}.{filename}") restricted_globals = { "__builtins__": builtins, "_print_": StubPrinter, @@ -185,14 +185,15 @@ def execute(hass, filename, source, data=None): "_getitem_": default_guarded_getitem, "_iter_unpack_sequence_": guarded_iter_unpack_sequence, "_unpack_sequence_": guarded_unpack_sequence, + "hass": hass, + "data": data or {}, + "logger": logger, } - logger = logging.getLogger(f"{__name__}.{filename}") - local = {"hass": hass, "data": data or {}, "logger": logger} try: _LOGGER.info("Executing %s: %s", filename, data) # pylint: disable=exec-used - exec(compiled.code, restricted_globals, local) + exec(compiled.code, restricted_globals) except ScriptError as err: logger.error("Error executing script: %s", err) except Exception as err: # pylint: disable=broad-except @@ -224,7 +225,7 @@ class TimeWrapper: if not TimeWrapper.warned: TimeWrapper.warned = True _LOGGER.warning( - "Using time.sleep can reduce the performance of " "Home Assistant" + "Using time.sleep can reduce the performance of Home Assistant" ) time.sleep(*args, **kwargs) diff --git a/homeassistant/components/qbittorrent/manifest.json b/homeassistant/components/qbittorrent/manifest.json index c41d4ba46d3..2125e8cfe7b 100644 --- a/homeassistant/components/qbittorrent/manifest.json +++ b/homeassistant/components/qbittorrent/manifest.json @@ -2,9 +2,7 @@ "domain": "qbittorrent", "name": "Qbittorrent", "documentation": "https://www.home-assistant.io/integrations/qbittorrent", - "requirements": [ - "python-qbittorrent==0.3.1" - ], + "requirements": ["python-qbittorrent==0.4.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/qbittorrent/sensor.py b/homeassistant/components/qbittorrent/sensor.py index f00b392065c..9544d74b1cd 100644 --- a/homeassistant/components/qbittorrent/sensor.py +++ b/homeassistant/components/qbittorrent/sensor.py @@ -1,9 +1,9 @@ """Support for monitoring the qBittorrent API.""" import logging -import voluptuous as vol - +from qbittorrent.client import Client, LoginRequired from requests.exceptions import RequestException +import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( @@ -13,9 +13,9 @@ from homeassistant.const import ( CONF_USERNAME, STATE_IDLE, ) -from homeassistant.helpers.entity import Entity -import homeassistant.helpers.config_validation as cv from homeassistant.exceptions import PlatformNotReady +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -43,7 +43,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the qBittorrent sensors.""" - from qbittorrent.client import Client, LoginRequired try: client = Client(config[CONF_URL]) @@ -108,7 +107,7 @@ class QBittorrentSensor(Entity): def update(self): """Get the latest data from qBittorrent and updates the state.""" try: - data = self.client.sync() + data = self.client.sync_main_data() self._available = True except RequestException: _LOGGER.error("Connection lost") diff --git a/homeassistant/components/qnap/sensor.py b/homeassistant/components/qnap/sensor.py index efbb1ac26ca..c3863bd0077 100644 --- a/homeassistant/components/qnap/sensor.py +++ b/homeassistant/components/qnap/sensor.py @@ -1,26 +1,27 @@ """Support for QNAP NAS Sensors.""" -import logging from datetime import timedelta +import logging +from qnapstats import QNAPStats import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.helpers.entity import Entity from homeassistant.const import ( + ATTR_NAME, CONF_HOST, - CONF_USERNAME, + CONF_MONITORED_CONDITIONS, CONF_PASSWORD, CONF_PORT, CONF_SSL, - ATTR_NAME, - CONF_VERIFY_SSL, CONF_TIMEOUT, - CONF_MONITORED_CONDITIONS, + CONF_USERNAME, + CONF_VERIFY_SSL, TEMP_CELSIUS, ) -from homeassistant.util import Throttle 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__) @@ -170,7 +171,6 @@ class QNAPStatsAPI: def __init__(self, config): """Initialize the API wrapper.""" - from qnapstats import QNAPStats protocol = "https" if config.get(CONF_SSL) else "http" self._api = QNAPStats( diff --git a/homeassistant/components/quantum_gateway/device_tracker.py b/homeassistant/components/quantum_gateway/device_tracker.py index ea3979e7757..58151fa02ce 100644 --- a/homeassistant/components/quantum_gateway/device_tracker.py +++ b/homeassistant/components/quantum_gateway/device_tracker.py @@ -1,6 +1,7 @@ """Support for Verizon FiOS Quantum Gateways.""" import logging +from quantum_gateway import QuantumGatewayScanner from requests.exceptions import RequestException import voluptuous as vol @@ -37,7 +38,6 @@ class QuantumGatewayDeviceScanner(DeviceScanner): def __init__(self, config): """Initialize the scanner.""" - from quantum_gateway import QuantumGatewayScanner self.host = config[CONF_HOST] self.password = config[CONF_PASSWORD] @@ -54,7 +54,7 @@ class QuantumGatewayDeviceScanner(DeviceScanner): _LOGGER.error("Unable to connect to gateway. Check host.") if not self.success_init: - _LOGGER.error("Unable to login to gateway. Check password and " "host.") + _LOGGER.error("Unable to login to gateway. Check password and host.") def scan_devices(self): """Scan for new devices and return a list of found MACs.""" diff --git a/homeassistant/components/qwikswitch/__init__.py b/homeassistant/components/qwikswitch/__init__.py index 1ae92b0a18a..33392c51be8 100644 --- a/homeassistant/components/qwikswitch/__init__.py +++ b/homeassistant/components/qwikswitch/__init__.py @@ -1,6 +1,8 @@ """Support for Qwikswitch devices.""" import logging +from pyqwikswitch.async_ import QSUsb +from pyqwikswitch.qwikswitch import CMD_BUTTONS, QS_CMD, QS_ID, SENSORS, QSType import voluptuous as vol from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA @@ -128,8 +130,6 @@ class QSToggleEntity(QSEntity): async def async_setup(hass, config): """Qwiskswitch component setup.""" - from pyqwikswitch.async_ import QSUsb - from pyqwikswitch.qwikswitch import CMD_BUTTONS, QS_CMD, QS_ID, QSType, SENSORS # Add cmd's to in /&listen packets will fire events # By default only buttons of type [TOGGLE,SCENE EXE,LEVEL] diff --git a/homeassistant/components/qwikswitch/binary_sensor.py b/homeassistant/components/qwikswitch/binary_sensor.py index a5b142e19ae..054028b5629 100644 --- a/homeassistant/components/qwikswitch/binary_sensor.py +++ b/homeassistant/components/qwikswitch/binary_sensor.py @@ -1,6 +1,8 @@ """Support for Qwikswitch Binary Sensors.""" import logging +from pyqwikswitch.qwikswitch import SENSORS + from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.core import callback @@ -27,7 +29,6 @@ class QSBinarySensor(QSEntity, BinarySensorDevice): def __init__(self, sensor): """Initialize the sensor.""" - from pyqwikswitch.qwikswitch import SENSORS super().__init__(sensor["id"], sensor["name"]) self.channel = sensor["channel"] diff --git a/homeassistant/components/qwikswitch/sensor.py b/homeassistant/components/qwikswitch/sensor.py index 01964fc7831..4674da420b2 100644 --- a/homeassistant/components/qwikswitch/sensor.py +++ b/homeassistant/components/qwikswitch/sensor.py @@ -1,6 +1,8 @@ """Support for Qwikswitch Sensors.""" import logging +from pyqwikswitch.qwikswitch import SENSORS + from homeassistant.core import callback from . import DOMAIN as QWIKSWITCH, QSEntity @@ -26,7 +28,6 @@ class QSSensor(QSEntity): def __init__(self, sensor): """Initialize the sensor.""" - from pyqwikswitch.qwikswitch import SENSORS super().__init__(sensor["id"], sensor["name"]) self.channel = sensor["channel"] diff --git a/homeassistant/components/rachio/__init__.py b/homeassistant/components/rachio/__init__.py index 2030512ab31..1b24f4e0071 100644 --- a/homeassistant/components/rachio/__init__.py +++ b/homeassistant/components/rachio/__init__.py @@ -1,14 +1,16 @@ """Integration with the Rachio Iro sprinkler system controller.""" import asyncio import logging +import secrets from typing import Optional from aiohttp import web +from rachiopy import Rachio import voluptuous as vol -from homeassistant.auth.util import generate_secret + from homeassistant.components.http import HomeAssistantView from homeassistant.const import CONF_API_KEY, EVENT_HOMEASSISTANT_STOP, URL_API -from homeassistant.helpers import discovery, config_validation as cv +from homeassistant.helpers import config_validation as cv, discovery from homeassistant.helpers.dispatcher import async_dispatcher_send _LOGGER = logging.getLogger(__name__) @@ -102,7 +104,6 @@ SIGNAL_RACHIO_SCHEDULE_UPDATE = SIGNAL_RACHIO_UPDATE + "_schedule" def setup(hass, config) -> bool: """Set up the Rachio component.""" - from rachiopy import Rachio # Listen for incoming webhook connections hass.http.register_view(RachioWebhookView()) @@ -114,7 +115,7 @@ def setup(hass, config) -> bool: # Get the URL of this server custom_url = config[DOMAIN].get(CONF_CUSTOM_URL) hass_url = hass.config.api.base_url if custom_url is None else custom_url - rachio.webhook_auth = generate_secret() + rachio.webhook_auth = secrets.token_hex() rachio.webhook_url = hass_url + WEBHOOK_PATH # Get the API user @@ -284,7 +285,6 @@ class RachioWebhookView(HomeAssistantView): url = WEBHOOK_PATH name = url[1:].replace("/", ":") - # pylint: disable=no-self-use @asyncio.coroutine async def post(self, request) -> web.Response: """Handle webhook calls from the server.""" diff --git a/homeassistant/components/rachio/binary_sensor.py b/homeassistant/components/rachio/binary_sensor.py index f74e3ca1802..f13eba59ac9 100644 --- a/homeassistant/components/rachio/binary_sensor.py +++ b/homeassistant/components/rachio/binary_sensor.py @@ -63,7 +63,7 @@ class RachioControllerBinarySensor(BinarySensorDevice): return # For this device - self._handle_update() + self._handle_update(args, kwargs) @abstractmethod def _poll_update(self, data=None) -> bool: @@ -119,9 +119,9 @@ class RachioControllerOnlineBinarySensor(RachioControllerBinarySensor): def _handle_update(self, *args, **kwargs) -> None: """Handle an update to the state of this sensor.""" - if args[0][KEY_SUBTYPE] == SUBTYPE_ONLINE: + if args[0][0][KEY_SUBTYPE] == SUBTYPE_ONLINE: self._state = True - elif args[0][KEY_SUBTYPE] == SUBTYPE_OFFLINE: + elif args[0][0][KEY_SUBTYPE] == SUBTYPE_OFFLINE: self._state = False self.schedule_update_ha_state() diff --git a/homeassistant/components/rachio/manifest.json b/homeassistant/components/rachio/manifest.json index 79e3677d65e..fae640f9262 100644 --- a/homeassistant/components/rachio/manifest.json +++ b/homeassistant/components/rachio/manifest.json @@ -2,9 +2,7 @@ "domain": "rachio", "name": "Rachio", "documentation": "https://www.home-assistant.io/integrations/rachio", - "requirements": [ - "rachiopy==0.1.3" - ], - "dependencies": [], + "requirements": ["rachiopy==0.1.3"], + "dependencies": ["http"], "codeowners": [] } diff --git a/homeassistant/components/rachio/switch.py b/homeassistant/components/rachio/switch.py index 80c227a6df6..a3a4f6bcca1 100644 --- a/homeassistant/components/rachio/switch.py +++ b/homeassistant/components/rachio/switch.py @@ -107,7 +107,7 @@ class RachioStandbySwitch(RachioSwitch): dispatcher_connect( hass, SIGNAL_RACHIO_CONTROLLER_UPDATE, self._handle_any_update ) - super().__init__(controller, poll=False) + super().__init__(controller, poll=True) self._poll_update(controller.init_data) @property @@ -134,9 +134,9 @@ class RachioStandbySwitch(RachioSwitch): def _handle_update(self, *args, **kwargs) -> None: """Update the state using webhook data.""" - if args[0][KEY_SUBTYPE] == SUBTYPE_SLEEP_MODE_ON: + if args[0][0][KEY_SUBTYPE] == SUBTYPE_SLEEP_MODE_ON: self._state = True - elif args[0][KEY_SUBTYPE] == SUBTYPE_SLEEP_MODE_OFF: + elif args[0][0][KEY_SUBTYPE] == SUBTYPE_SLEEP_MODE_OFF: self._state = False self.schedule_update_ha_state() diff --git a/homeassistant/components/radarr/sensor.py b/homeassistant/components/radarr/sensor.py index 0dcdcf1514f..79e45ffd9a8 100644 --- a/homeassistant/components/radarr/sensor.py +++ b/homeassistant/components/radarr/sensor.py @@ -1,21 +1,22 @@ """Support for Radarr.""" +from datetime import datetime, timedelta import logging import time -from datetime import datetime, timedelta +from pytz import timezone import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv +from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_API_KEY, CONF_HOST, - CONF_PORT, CONF_MONITORED_CONDITIONS, + CONF_PORT, CONF_SSL, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.components.sensor import PLATFORM_SCHEMA _LOGGER = logging.getLogger(__name__) @@ -79,7 +80,6 @@ class RadarrSensor(Entity): def __init__(self, hass, conf, sensor_type): """Create Radarr entity.""" - from pytz import timezone self.conf = conf self.host = conf.get(CONF_HOST) diff --git a/homeassistant/components/radiotherm/climate.py b/homeassistant/components/radiotherm/climate.py index a007dd673ac..5ac64fd64e9 100644 --- a/homeassistant/components/radiotherm/climate.py +++ b/homeassistant/components/radiotherm/climate.py @@ -1,32 +1,32 @@ """Support for Radio Thermostat wifi-enabled home thermostats.""" import logging -import voluptuous as vol import radiotherm +import voluptuous as vol -from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice from homeassistant.components.climate.const import ( + CURRENT_HVAC_COOL, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + FAN_OFF, + FAN_ON, HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_OFF, - FAN_ON, - FAN_OFF, - CURRENT_HVAC_IDLE, - CURRENT_HVAC_HEAT, - CURRENT_HVAC_COOL, - SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE, + SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ( ATTR_TEMPERATURE, CONF_HOST, PRECISION_HALVES, - TEMP_FAHRENHEIT, STATE_ON, + TEMP_FAHRENHEIT, ) -from homeassistant.util import dt as dt_util import homeassistant.helpers.config_validation as cv +from homeassistant.util import dt as dt_util _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/rainbird/__init__.py b/homeassistant/components/rainbird/__init__.py index 0b51be1f258..83c358c480b 100644 --- a/homeassistant/components/rainbird/__init__.py +++ b/homeassistant/components/rainbird/__init__.py @@ -75,7 +75,7 @@ def _setup_controller(hass, controller_config, config): position = len(hass.data[DATA_RAINBIRD]) try: controller.get_serial_number() - except Exception as exc: # pylint: disable=W0703 + except Exception as exc: # pylint: disable=broad-except _LOGGER.error("Unable to setup controller: %s", exc) return False hass.data[DATA_RAINBIRD].append(controller) diff --git a/homeassistant/components/raincloud/__init__.py b/homeassistant/components/raincloud/__init__.py index 77bdcc5aa2f..dd851c0b3e3 100644 --- a/homeassistant/components/raincloud/__init__.py +++ b/homeassistant/components/raincloud/__init__.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +from raincloudy.core import RainCloudy from requests.exceptions import ConnectTimeout, HTTPError import voluptuous as vol @@ -96,8 +97,6 @@ def setup(hass, config): scan_interval = conf.get(CONF_SCAN_INTERVAL) try: - from raincloudy.core import RainCloudy - raincloud = RainCloudy(username=username, password=password) if not raincloud.is_connected: raise HTTPError diff --git a/homeassistant/components/rainmachine/.translations/da.json b/homeassistant/components/rainmachine/.translations/da.json index 61d29894fe2..34f4fff4ed0 100644 --- a/homeassistant/components/rainmachine/.translations/da.json +++ b/homeassistant/components/rainmachine/.translations/da.json @@ -8,7 +8,7 @@ "user": { "data": { "ip_address": "V\u00e6rtsnavn eller IP-adresse", - "password": "Password", + "password": "Adgangskode", "port": "Port" }, "title": "Udfyld dine oplysninger" diff --git a/homeassistant/components/rainmachine/.translations/ru.json b/homeassistant/components/rainmachine/.translations/ru.json index afaa55424d2..ca535663f54 100644 --- a/homeassistant/components/rainmachine/.translations/ru.json +++ b/homeassistant/components/rainmachine/.translations/ru.json @@ -1,8 +1,8 @@ { "config": { "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_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435." + "identifier_exists": "\u0423\u0447\u0451\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_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435." }, "step": { "user": { diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index a530223cb05..5dffecb0488 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -1,8 +1,10 @@ """Support for RainMachine devices.""" import asyncio -import logging from datetime import timedelta +import logging +from regenmaschine import login +from regenmaschine.errors import RainMachineError import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT @@ -10,12 +12,12 @@ from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_BINARY_SENSORS, CONF_IP_ADDRESS, + CONF_MONITORED_CONDITIONS, CONF_PASSWORD, CONF_PORT, CONF_SCAN_INTERVAL, CONF_SENSORS, CONF_SSL, - CONF_MONITORED_CONDITIONS, CONF_SWITCHES, ) from homeassistant.exceptions import ConfigEntryNotReady @@ -211,8 +213,6 @@ async def async_setup(hass, config): async def async_setup_entry(hass, config_entry): """Set up RainMachine as config entry.""" - from regenmaschine import login - from regenmaschine.errors import RainMachineError _verify_domain_control = verify_domain_control(hass, DOMAIN) @@ -377,7 +377,6 @@ class RainMachine: async def async_update(self): """Update sensor/binary sensor data.""" - from regenmaschine.errors import RainMachineError tasks = {} diff --git a/homeassistant/components/rainmachine/config_flow.py b/homeassistant/components/rainmachine/config_flow.py index 3600324cb12..4753335da78 100644 --- a/homeassistant/components/rainmachine/config_flow.py +++ b/homeassistant/components/rainmachine/config_flow.py @@ -2,10 +2,11 @@ from collections import OrderedDict +from regenmaschine import login +from regenmaschine.errors import RainMachineError import voluptuous as vol from homeassistant import config_entries -from homeassistant.core import callback from homeassistant.const import ( CONF_IP_ADDRESS, CONF_PASSWORD, @@ -13,6 +14,7 @@ from homeassistant.const import ( CONF_SCAN_INTERVAL, CONF_SSL, ) +from homeassistant.core import callback from homeassistant.helpers import aiohttp_client from .const import DEFAULT_PORT, DEFAULT_SCAN_INTERVAL, DEFAULT_SSL, DOMAIN @@ -55,8 +57,6 @@ class RainMachineFlowHandler(config_entries.ConfigFlow): async def async_step_user(self, user_input=None): """Handle the start of the config flow.""" - from regenmaschine import login - from regenmaschine.errors import RainMachineError if not user_input: return await self._show_form() diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index 870c7317f25..69c0e4da52d 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -2,6 +2,8 @@ from datetime import datetime import logging +from regenmaschine.errors import RequestError + from homeassistant.components.switch import SwitchDevice from homeassistant.const import ATTR_ID from homeassistant.core import callback @@ -181,7 +183,6 @@ class RainMachineProgram(RainMachineSwitch): async def async_turn_off(self, **kwargs) -> None: """Turn the program off.""" - from regenmaschine.errors import RequestError try: await self.rainmachine.client.programs.stop(self._rainmachine_entity_id) @@ -193,7 +194,6 @@ class RainMachineProgram(RainMachineSwitch): async def async_turn_on(self, **kwargs) -> None: """Turn the program on.""" - from regenmaschine.errors import RequestError try: await self.rainmachine.client.programs.start(self._rainmachine_entity_id) @@ -205,7 +205,6 @@ class RainMachineProgram(RainMachineSwitch): async def async_update(self) -> None: """Update info for the program.""" - from regenmaschine.errors import RequestError try: self._obj = await self.rainmachine.client.programs.get( @@ -265,7 +264,6 @@ class RainMachineZone(RainMachineSwitch): async def async_turn_off(self, **kwargs) -> None: """Turn the zone off.""" - from regenmaschine.errors import RequestError try: await self.rainmachine.client.zones.stop(self._rainmachine_entity_id) @@ -274,7 +272,6 @@ class RainMachineZone(RainMachineSwitch): async def async_turn_on(self, **kwargs) -> None: """Turn the zone on.""" - from regenmaschine.errors import RequestError try: await self.rainmachine.client.zones.start( @@ -285,7 +282,6 @@ class RainMachineZone(RainMachineSwitch): async def async_update(self) -> None: """Update info for the zone.""" - from regenmaschine.errors import RequestError try: self._obj = await self.rainmachine.client.zones.get( diff --git a/homeassistant/components/random/binary_sensor.py b/homeassistant/components/random/binary_sensor.py index 8c2608ad81d..e502439b28c 100644 --- a/homeassistant/components/random/binary_sensor.py +++ b/homeassistant/components/random/binary_sensor.py @@ -1,15 +1,16 @@ """Support for showing random states.""" import logging +from random import getrandbits import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.binary_sensor import ( - BinarySensorDevice, - PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA, + PLATFORM_SCHEMA, + BinarySensorDevice, ) -from homeassistant.const import CONF_NAME, CONF_DEVICE_CLASS +from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -57,6 +58,5 @@ class RandomSensor(BinarySensorDevice): async def async_update(self): """Get new state and update the sensor's state.""" - from random import getrandbits self._state = bool(getrandbits(1)) diff --git a/homeassistant/components/random/sensor.py b/homeassistant/components/random/sensor.py index 5d4e3d0d57a..4ebd710f103 100644 --- a/homeassistant/components/random/sensor.py +++ b/homeassistant/components/random/sensor.py @@ -1,5 +1,6 @@ """Support for showing random numbers.""" import logging +from random import randrange import voluptuous as vol @@ -82,6 +83,5 @@ class RandomSensor(Entity): async def async_update(self): """Get a new number and updates the states.""" - from random import randrange self._state = randrange(self._minimum, self._maximum + 1) diff --git a/homeassistant/components/raspyrfm/switch.py b/homeassistant/components/raspyrfm/switch.py index 53cb1dbdcb5..ec07119b96a 100644 --- a/homeassistant/components/raspyrfm/switch.py +++ b/homeassistant/components/raspyrfm/switch.py @@ -1,6 +1,15 @@ """Support for switches that can be controlled using the RaspyRFM rc module.""" import logging +from raspyrfm_client import RaspyRFMClient +from raspyrfm_client.device_implementations.controlunit.actions import Action +from raspyrfm_client.device_implementations.controlunit.controlunit_constants import ( + ControlUnitModel, +) +from raspyrfm_client.device_implementations.gateway.manufacturer.gateway_constants import ( + GatewayModel, +) +from raspyrfm_client.device_implementations.manufacturer_constants import Manufacturer import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice @@ -46,16 +55,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the RaspyRFM switch.""" - from raspyrfm_client import RaspyRFMClient - from raspyrfm_client.device_implementations.controlunit.controlunit_constants import ( - ControlUnitModel, - ) - from raspyrfm_client.device_implementations.gateway.manufacturer.gateway_constants import ( - GatewayModel, - ) - from raspyrfm_client.device_implementations.manufacturer_constants import ( - Manufacturer, - ) gateway_manufacturer = config.get( CONF_GATEWAY_MANUFACTURER, Manufacturer.SEEGEL_SYSTEME.value @@ -123,7 +122,6 @@ class RaspyRFMSwitch(SwitchDevice): def turn_on(self, **kwargs): """Turn the switch on.""" - from raspyrfm_client.device_implementations.controlunit.actions import Action self._raspyrfm_client.send(self._gateway, self._controlunit, Action.ON) self._state = True @@ -131,7 +129,6 @@ class RaspyRFMSwitch(SwitchDevice): def turn_off(self, **kwargs): """Turn the switch off.""" - from raspyrfm_client.device_implementations.controlunit.actions import Action if Action.OFF in self._controlunit.get_supported_actions(): self._raspyrfm_client.send(self._gateway, self._controlunit, Action.OFF) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 10b1d04304f..ab56a5fc33b 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -5,18 +5,19 @@ import concurrent.futures from datetime import datetime, timedelta import logging import queue +from sqlite3 import Connection import threading import time from typing import Any, Dict, Optional -from sqlite3 import Connection -import voluptuous as vol -from sqlalchemy import exc, create_engine +from sqlalchemy import create_engine, exc from sqlalchemy.engine import Engine from sqlalchemy.event import listens_for from sqlalchemy.orm import scoped_session, sessionmaker from sqlalchemy.pool import StaticPool +import voluptuous as vol +from homeassistant.components import persistent_notification from homeassistant.const import ( ATTR_ENTITY_ID, CONF_DOMAINS, @@ -29,7 +30,6 @@ from homeassistant.const import ( EVENT_TIME_CHANGED, MATCH_ALL, ) -from homeassistant.components import persistent_notification from homeassistant.core import CoreState, HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import generate_filter @@ -228,7 +228,7 @@ class Recorder(threading.Thread): _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)", + "Error during connection setup: %s (retrying in %s seconds)", err, CONNECT_RETRY_WAIT, ) diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json index 4ad000866db..ad0ac979e03 100644 --- a/homeassistant/components/recorder/manifest.json +++ b/homeassistant/components/recorder/manifest.json @@ -2,9 +2,7 @@ "domain": "recorder", "name": "Recorder", "documentation": "https://www.home-assistant.io/integrations/recorder", - "requirements": [ - "sqlalchemy==1.3.11" - ], + "requirements": ["sqlalchemy==1.3.12"], "dependencies": [], "codeowners": [] -} \ No newline at end of file +} diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 33a01ea1ac0..3a5ef2729be 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -6,7 +6,7 @@ from sqlalchemy import Table, text from sqlalchemy.engine import reflection from sqlalchemy.exc import OperationalError, SQLAlchemyError -from .models import SchemaChanges, SCHEMA_VERSION, Base +from .models import SCHEMA_VERSION, Base, SchemaChanges from .util import session_scope _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index b512bfc8204..f3e80a9a739 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -1,6 +1,6 @@ """Models for SQLAlchemy.""" -import json from datetime import datetime +import json import logging from sqlalchemy import ( @@ -17,9 +17,9 @@ from sqlalchemy import ( from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm.session import Session -import homeassistant.util.dt as dt_util from homeassistant.core import Context, Event, EventOrigin, State, split_entity_id from homeassistant.helpers.json import JSONEncoder +import homeassistant.util.dt as dt_util # SQLAlchemy Schema # pylint: disable=invalid-name diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index 089476245fe..b4b1f612fac 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -5,8 +5,8 @@ import logging from sqlalchemy.exc import SQLAlchemyError import homeassistant.util.dt as dt_util -from .models import Events, States +from .models import Events, States from .util import session_scope _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index 8cfcafea79d..693d88ae795 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -28,7 +28,7 @@ def session_scope(*, hass=None, session=None): if session.transaction: need_rollback = True session.commit() - except Exception as err: # pylint: disable=broad-except + except Exception as err: _LOGGER.error("Error executing query: %s", err) if need_rollback: session.rollback() diff --git a/homeassistant/components/recswitch/switch.py b/homeassistant/components/recswitch/switch.py index aa93693a36d..c242f23dfdd 100644 --- a/homeassistant/components/recswitch/switch.py +++ b/homeassistant/components/recswitch/switch.py @@ -2,13 +2,13 @@ import logging +from pyrecswitch import RSNetwork, RSNetworkError import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME import homeassistant.helpers.config_validation as cv - _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "RecSwitch {0}" @@ -26,7 +26,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the device.""" - from pyrecswitch import RSNetwork host = config[CONF_HOST] mac_address = config[CONF_MAC] @@ -78,7 +77,6 @@ class RecSwitchSwitch(SwitchDevice): async def async_set_gpio_status(self, status): """Set the switch status.""" - from pyrecswitch import RSNetworkError try: ret = await self.device.set_gpio_status(status) @@ -88,7 +86,6 @@ class RecSwitchSwitch(SwitchDevice): async def async_update(self): """Update the current switch status.""" - from pyrecswitch import RSNetworkError try: ret = await self.device.get_gpio_status() diff --git a/homeassistant/components/reddit/sensor.py b/homeassistant/components/reddit/sensor.py index 82f622b968e..ed24dfe47df 100644 --- a/homeassistant/components/reddit/sensor.py +++ b/homeassistant/components/reddit/sensor.py @@ -6,7 +6,7 @@ import praw import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_MAXIMUM +from homeassistant.const import CONF_MAXIMUM, CONF_PASSWORD, CONF_USERNAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity diff --git a/homeassistant/components/remember_the_milk/__init__.py b/homeassistant/components/remember_the_milk/__init__.py index fdfbdfd5cdc..02875cb8aa9 100644 --- a/homeassistant/components/remember_the_milk/__init__.py +++ b/homeassistant/components/remember_the_milk/__init__.py @@ -166,7 +166,7 @@ class RememberTheMilkConfiguration: self._config = json.load(config_file) except ValueError: _LOGGER.error( - "Failed to load configuration file, creating a " "new one: %s", + "Failed to load configuration file, creating a new one: %s", self._config_file_path, ) self._config = dict() @@ -258,7 +258,7 @@ class RememberTheMilk(Entity): valid = self._rtm_api.token_valid() if not valid: _LOGGER.error( - "Token for account %s is invalid. You need to " "register again!", + "Token for account %s is invalid. You need to register again!", self.name, ) self._rtm_config.delete_token(self._name) @@ -306,14 +306,14 @@ class RememberTheMilk(Entity): timeline=timeline, ) _LOGGER.debug( - "Updated task with id '%s' in account " "%s to name %s", + "Updated task with id '%s' in account %s to name %s", hass_id, self.name, task_name, ) except RtmRequestFailedException as rtm_exception: _LOGGER.error( - "Error creating new Remember The Milk task for " "account %s: %s", + "Error creating new Remember The Milk task for account %s: %s", self._name, rtm_exception, ) @@ -347,7 +347,7 @@ class RememberTheMilk(Entity): ) except RtmRequestFailedException as rtm_exception: _LOGGER.error( - "Error creating new Remember The Milk task for " "account %s: %s", + "Error creating new Remember The Milk task for account %s: %s", self._name, rtm_exception, ) diff --git a/homeassistant/components/remote/__init__.py b/homeassistant/components/remote/__init__.py index 71059b98f35..b88629ea468 100644 --- a/homeassistant/components/remote/__init__.py +++ b/homeassistant/components/remote/__init__.py @@ -5,23 +5,22 @@ import logging import voluptuous as vol -from homeassistant.loader import bind_hass -from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.entity import ToggleEntity -import homeassistant.helpers.config_validation as cv -from homeassistant.const import ( - STATE_ON, - SERVICE_TURN_ON, - SERVICE_TURN_OFF, - SERVICE_TOGGLE, -) from homeassistant.components import group +from homeassistant.const import ( + SERVICE_TOGGLE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_ON, +) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa: F401 - ENTITY_SERVICE_SCHEMA, PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, + make_entity_service_schema, ) - +from homeassistant.helpers.entity import ToggleEntity +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.loader import bind_hass # mypy: allow-untyped-defs, no-check-untyped-defs @@ -56,29 +55,10 @@ DEFAULT_HOLD_SECS = 0 SUPPORT_LEARN_COMMAND = 1 -REMOTE_SERVICE_ACTIVITY_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( +REMOTE_SERVICE_ACTIVITY_SCHEMA = make_entity_service_schema( {vol.Optional(ATTR_ACTIVITY): cv.string} ) -REMOTE_SERVICE_SEND_COMMAND_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - { - vol.Required(ATTR_COMMAND): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(ATTR_DEVICE): cv.string, - vol.Optional(ATTR_NUM_REPEATS, default=DEFAULT_NUM_REPEATS): cv.positive_int, - vol.Optional(ATTR_DELAY_SECS): vol.Coerce(float), - vol.Optional(ATTR_HOLD_SECS, default=DEFAULT_HOLD_SECS): vol.Coerce(float), - } -) - -REMOTE_SERVICE_LEARN_COMMAND_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - { - vol.Optional(ATTR_DEVICE): cv.string, - vol.Optional(ATTR_COMMAND): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(ATTR_ALTERNATIVE): cv.boolean, - vol.Optional(ATTR_TIMEOUT): cv.positive_int, - } -) - @bind_hass def is_on(hass, entity_id=None): @@ -107,12 +87,27 @@ async def async_setup(hass, config): ) component.async_register_entity_service( - SERVICE_SEND_COMMAND, REMOTE_SERVICE_SEND_COMMAND_SCHEMA, "async_send_command" + SERVICE_SEND_COMMAND, + { + vol.Required(ATTR_COMMAND): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(ATTR_DEVICE): cv.string, + vol.Optional( + ATTR_NUM_REPEATS, default=DEFAULT_NUM_REPEATS + ): cv.positive_int, + vol.Optional(ATTR_DELAY_SECS): vol.Coerce(float), + vol.Optional(ATTR_HOLD_SECS, default=DEFAULT_HOLD_SECS): vol.Coerce(float), + }, + "async_send_command", ) component.async_register_entity_service( SERVICE_LEARN_COMMAND, - REMOTE_SERVICE_LEARN_COMMAND_SCHEMA, + { + vol.Optional(ATTR_DEVICE): cv.string, + vol.Optional(ATTR_COMMAND): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(ATTR_ALTERNATIVE): cv.boolean, + vol.Optional(ATTR_TIMEOUT): cv.positive_int, + }, "async_learn_command", ) diff --git a/homeassistant/components/remote/services.yaml b/homeassistant/components/remote/services.yaml index a551ba18ed4..1d712a8f285 100644 --- a/homeassistant/components/remote/services.yaml +++ b/homeassistant/components/remote/services.yaml @@ -64,34 +64,3 @@ learn_command: timeout: description: Timeout, in seconds, for the command to be learned. example: '30' - - -harmony_sync: - description: Syncs the remote's configuration. - fields: - entity_id: - description: Name(s) of entities to sync. - example: 'remote.family_room' - -harmony_change_channel: - description: Sends change channel command to the Harmony HUB - fields: - entity_id: - description: Name(s) of Harmony remote entities to send change channel command to - example: 'remote.family_room' - channel: - description: Channel number to change to - example: '200' - -xiaomi_miio_learn_command: - description: 'Learn an IR command, press "Call Service", point the remote at the IR device, and the learned command will be shown as a notification in Overview.' - fields: - entity_id: - description: 'Name of the entity to learn command from.' - example: 'remote.xiaomi_miio' - slot: - description: 'Define the slot used to save the IR command (Value from 1 to 1000000)' - example: '1' - timeout: - description: 'Define the timeout in seconds, before which the command must be learned.' - example: '30' diff --git a/homeassistant/components/remote_rpi_gpio/__init__.py b/homeassistant/components/remote_rpi_gpio/__init__.py index 33356d0e3b8..e1b66128e3f 100644 --- a/homeassistant/components/remote_rpi_gpio/__init__.py +++ b/homeassistant/components/remote_rpi_gpio/__init__.py @@ -1,6 +1,9 @@ """Support for controlling GPIO pins of a Raspberry Pi.""" import logging +from gpiozero import LED, Button +from gpiozero.pins.pigpio import PiGPIOFactory + _LOGGER = logging.getLogger(__name__) CONF_BOUNCETIME = "bouncetime" @@ -21,8 +24,6 @@ def setup(hass, config): def setup_output(address, port, invert_logic): """Set up a GPIO as output.""" - from gpiozero import LED - from gpiozero.pins.pigpio import PiGPIOFactory try: return LED(port, active_high=invert_logic, pin_factory=PiGPIOFactory(address)) @@ -32,8 +33,6 @@ def setup_output(address, port, invert_logic): def setup_input(address, port, pull_mode, bouncetime): """Set up a GPIO as input.""" - from gpiozero import Button - from gpiozero.pins.pigpio import PiGPIOFactory if pull_mode == "UP": pull_gpio_up = True diff --git a/homeassistant/components/remote_rpi_gpio/binary_sensor.py b/homeassistant/components/remote_rpi_gpio/binary_sensor.py index e12d83324fd..862bd30ae43 100644 --- a/homeassistant/components/remote_rpi_gpio/binary_sensor.py +++ b/homeassistant/components/remote_rpi_gpio/binary_sensor.py @@ -1,19 +1,17 @@ """Support for binary sensor using RPi GPIO.""" import logging +import requests import voluptuous as vol -import requests - +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice from homeassistant.const import CONF_HOST -from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA - import homeassistant.helpers.config_validation as cv from . import ( CONF_BOUNCETIME, - CONF_PULL_MODE, CONF_INVERT_LOGIC, + CONF_PULL_MODE, DEFAULT_BOUNCETIME, DEFAULT_INVERT_LOGIC, DEFAULT_PULL_MODE, diff --git a/homeassistant/components/remote_rpi_gpio/switch.py b/homeassistant/components/remote_rpi_gpio/switch.py index 8240de7951d..a5b255179cd 100644 --- a/homeassistant/components/remote_rpi_gpio/switch.py +++ b/homeassistant/components/remote_rpi_gpio/switch.py @@ -3,9 +3,8 @@ import logging import voluptuous as vol -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA -from homeassistant.const import DEVICE_DEFAULT_NAME, CONF_HOST - +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice +from homeassistant.const import CONF_HOST, DEVICE_DEFAULT_NAME import homeassistant.helpers.config_validation as cv from . import CONF_INVERT_LOGIC, DEFAULT_INVERT_LOGIC diff --git a/homeassistant/components/rest/notify.py b/homeassistant/components/rest/notify.py index f2bfcf3aba7..4f3de14b731 100644 --- a/homeassistant/components/rest/notify.py +++ b/homeassistant/components/rest/notify.py @@ -4,6 +4,14 @@ import logging import requests import voluptuous as vol +from homeassistant.components.notify import ( + ATTR_MESSAGE, + ATTR_TARGET, + ATTR_TITLE, + ATTR_TITLE_DEFAULT, + PLATFORM_SCHEMA, + BaseNotificationService, +) from homeassistant.const import ( CONF_AUTHENTICATION, CONF_HEADERS, @@ -18,15 +26,6 @@ from homeassistant.const import ( ) import homeassistant.helpers.config_validation as cv -from homeassistant.components.notify import ( - ATTR_TARGET, - ATTR_TITLE, - ATTR_TITLE_DEFAULT, - ATTR_MESSAGE, - PLATFORM_SCHEMA, - BaseNotificationService, -) - CONF_DATA = "data" CONF_DATA_TEMPLATE = "data_template" CONF_MESSAGE_PARAMETER_NAME = "message_param_name" diff --git a/homeassistant/components/rest/sensor.py b/homeassistant/components/rest/sensor.py index 41adb855903..51120cb350c 100644 --- a/homeassistant/components/rest/sensor.py +++ b/homeassistant/components/rest/sensor.py @@ -1,34 +1,34 @@ """Support for RESTful API sensors.""" -import logging import json +import logging -import voluptuous as vol import requests from requests.auth import HTTPBasicAuth, HTTPDigestAuth +import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA +from homeassistant.components.sensor import DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA from homeassistant.const import ( CONF_AUTHENTICATION, + CONF_DEVICE_CLASS, CONF_FORCE_UPDATE, CONF_HEADERS, - CONF_NAME, CONF_METHOD, + CONF_NAME, CONF_PASSWORD, CONF_PAYLOAD, CONF_RESOURCE, CONF_RESOURCE_TEMPLATE, + CONF_TIMEOUT, CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME, - CONF_TIMEOUT, CONF_VALUE_TEMPLATE, CONF_VERIFY_SSL, - CONF_DEVICE_CLASS, HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION, ) from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -197,13 +197,18 @@ class RestSensor(Entity): if value: try: json_dict = json.loads(value) + if isinstance(json_dict, list): + json_dict = json_dict[0] if isinstance(json_dict, dict): attrs = { k: json_dict[k] for k in self._json_attrs if k in json_dict } self._attributes = attrs else: - _LOGGER.warning("JSON result was not a dictionary") + _LOGGER.warning( + "JSON result was not a dictionary" + " or list with 0th element a dictionary" + ) except ValueError: _LOGGER.warning("REST result could not be parsed as JSON") _LOGGER.debug("Erroneous JSON: %s", value) @@ -227,32 +232,33 @@ class RestData: self, method, resource, auth, headers, data, verify_ssl, timeout=DEFAULT_TIMEOUT ): """Initialize the data object.""" - self._request = requests.Request( - method, resource, headers=headers, auth=auth, data=data - ).prepare() + self._method = method + self._resource = resource + self._auth = auth + self._headers = headers + self._request_data = data self._verify_ssl = verify_ssl self._timeout = timeout self.data = None def set_url(self, url): """Set url.""" - self._request.prepare_url(url, None) + self._resource = url def update(self): """Get the latest data from REST service with provided method.""" - _LOGGER.debug("Updating from %s", self._request.url) + _LOGGER.debug("Updating from %s", self._resource) try: - with requests.Session() as sess: - response = sess.send( - self._request, timeout=self._timeout, verify=self._verify_ssl - ) - + response = requests.request( + self._method, + self._resource, + headers=self._headers, + auth=self._auth, + data=self._request_data, + timeout=self._timeout, + verify=self._verify_ssl, + ) self.data = response.text except requests.exceptions.RequestException as ex: - _LOGGER.error( - "Error fetching data: %s from %s failed with %s", - self._request, - self._request.url, - ex, - ) + _LOGGER.error("Error fetching data: %s failed with %s", self._resource, ex) self.data = None diff --git a/homeassistant/components/rest/switch.py b/homeassistant/components/rest/switch.py index a02a8507194..fe409a46be7 100644 --- a/homeassistant/components/rest/switch.py +++ b/homeassistant/components/rest/switch.py @@ -6,15 +6,15 @@ import aiohttp import async_timeout import voluptuous as vol -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import ( CONF_HEADERS, + CONF_METHOD, CONF_NAME, + CONF_PASSWORD, CONF_RESOURCE, CONF_TIMEOUT, - CONF_METHOD, CONF_USERNAME, - CONF_PASSWORD, CONF_VERIFY_SSL, ) from homeassistant.helpers.aiohttp_client import async_get_clientsession diff --git a/homeassistant/components/rest_command/__init__.py b/homeassistant/components/rest_command/__init__.py index 223dc7da7cc..7dfbb964167 100644 --- a/homeassistant/components/rest_command/__init__.py +++ b/homeassistant/components/rest_command/__init__.py @@ -4,17 +4,16 @@ import logging import aiohttp from aiohttp import hdrs -import async_timeout import voluptuous as vol from homeassistant.const import ( - CONF_TIMEOUT, - CONF_USERNAME, - CONF_PASSWORD, - CONF_URL, - CONF_PAYLOAD, - CONF_METHOD, CONF_HEADERS, + CONF_METHOD, + CONF_PASSWORD, + CONF_PAYLOAD, + CONF_TIMEOUT, + CONF_URL, + CONF_USERNAME, CONF_VERIFY_SSL, ) from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -38,7 +37,7 @@ COMMAND_SCHEMA = vol.Schema( vol.Optional(CONF_METHOD, default=DEFAULT_METHOD): vol.All( vol.Lower, vol.In(SUPPORT_REST_METHODS) ), - vol.Optional(CONF_HEADERS): vol.Schema({cv.string: cv.string}), + vol.Optional(CONF_HEADERS): vol.Schema({cv.string: cv.template}), vol.Inclusive(CONF_USERNAME, "authentication"): cv.string, vol.Inclusive(CONF_PASSWORD, "authentication"): cv.string, vol.Optional(CONF_PAYLOAD): cv.template, @@ -76,15 +75,15 @@ async def async_setup(hass, config): template_payload = command_config[CONF_PAYLOAD] template_payload.hass = hass - headers = None + template_headers = None if CONF_HEADERS in command_config: - headers = command_config[CONF_HEADERS] + template_headers = command_config[CONF_HEADERS] + for template_header in template_headers.values(): + template_header.hass = hass + content_type = None if CONF_CONTENT_TYPE in command_config: content_type = command_config[CONF_CONTENT_TYPE] - if headers is None: - headers = {} - headers[hdrs.CONTENT_TYPE] = content_type async def async_service_handler(service): """Execute a shell command service.""" @@ -95,22 +94,47 @@ async def async_setup(hass, config): ) request_url = template_url.async_render(variables=service.data) - try: - with async_timeout.timeout(timeout): - request = await getattr(websession, method)( - request_url, data=payload, auth=auth, headers=headers + + headers = None + if template_headers: + headers = {} + for header_name, template_header in template_headers.items(): + headers[header_name] = template_header.async_render( + variables=service.data ) - if request.status < 400: - _LOGGER.info("Success call %s.", request.url) - else: - _LOGGER.warning("Error %d on call %s.", request.status, request.url) + if content_type: + if headers is None: + headers = {} + headers[hdrs.CONTENT_TYPE] = content_type + + try: + async with getattr(websession, method)( + request_url, + data=payload, + auth=auth, + headers=headers, + timeout=timeout, + ) as response: + + if response.status < 400: + _LOGGER.debug( + "Success. Url: %s. Status code: %d.", + response.url, + response.status, + ) + else: + _LOGGER.warning( + "Error. Url: %s. Status code %d.", + response.url, + response.status, + ) except asyncio.TimeoutError: - _LOGGER.warning("Timeout call %s.", request.url) + _LOGGER.warning("Timeout call %s.", response.url, exc_info=1) except aiohttp.ClientError: - _LOGGER.error("Client error %s.", request_url) + _LOGGER.error("Client error %s.", request_url, exc_info=1) # register services hass.services.async_register(DOMAIN, name, async_service_handler) diff --git a/homeassistant/components/rflink/__init__.py b/homeassistant/components/rflink/__init__.py index b3e1d2b16b7..2e5875b9d08 100644 --- a/homeassistant/components/rflink/__init__.py +++ b/homeassistant/components/rflink/__init__.py @@ -19,7 +19,6 @@ from homeassistant.const import ( from homeassistant.core import CoreState, callback from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.deprecation import get_deprecated from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -33,12 +32,9 @@ ATTR_EVENT = "event" ATTR_STATE = "state" CONF_ALIASES = "aliases" -CONF_ALIASSES = "aliasses" CONF_GROUP_ALIASES = "group_aliases" -CONF_GROUP_ALIASSES = "group_aliasses" CONF_GROUP = "group" CONF_NOGROUP_ALIASES = "nogroup_aliases" -CONF_NOGROUP_ALIASSES = "nogroup_aliasses" CONF_DEVICE_DEFAULTS = "device_defaults" CONF_DEVICE_ID = "device_id" CONF_DEVICES = "devices" @@ -563,18 +559,3 @@ class SwitchableRflinkDevice(RflinkCommand, RestoreEntity): def async_turn_off(self, **kwargs): """Turn the device off.""" return self._async_handle_command("turn_off") - - -DEPRECATED_CONFIG_OPTIONS = [CONF_ALIASSES, CONF_GROUP_ALIASSES, CONF_NOGROUP_ALIASSES] -REPLACEMENT_CONFIG_OPTIONS = [CONF_ALIASES, CONF_GROUP_ALIASES, CONF_NOGROUP_ALIASES] - - -def remove_deprecated(config): - """Remove deprecated config options from device config.""" - for index, deprecated_option in enumerate(DEPRECATED_CONFIG_OPTIONS): - if deprecated_option in config: - replacement_option = REPLACEMENT_CONFIG_OPTIONS[index] - # generate deprecation warning - get_deprecated(config, replacement_option, deprecated_option) - # remove old config value replacing new one - config[replacement_option] = config.pop(deprecated_option) diff --git a/homeassistant/components/rflink/light.py b/homeassistant/components/rflink/light.py index 682d45f8f42..db616b92fc4 100644 --- a/homeassistant/components/rflink/light.py +++ b/homeassistant/components/rflink/light.py @@ -14,23 +14,19 @@ import homeassistant.helpers.config_validation as cv from . import ( CONF_ALIASES, - CONF_ALIASSES, CONF_AUTOMATIC_ADD, CONF_DEVICE_DEFAULTS, CONF_DEVICES, CONF_FIRE_EVENT, CONF_GROUP, CONF_GROUP_ALIASES, - CONF_GROUP_ALIASSES, CONF_NOGROUP_ALIASES, - CONF_NOGROUP_ALIASSES, CONF_SIGNAL_REPETITIONS, DATA_DEVICE_REGISTER, DEVICE_DEFAULTS_SCHEMA, EVENT_KEY_COMMAND, EVENT_KEY_ID, SwitchableRflinkDevice, - remove_deprecated, ) _LOGGER = logging.getLogger(__name__) @@ -65,14 +61,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_FIRE_EVENT): cv.boolean, vol.Optional(CONF_SIGNAL_REPETITIONS): vol.Coerce(int), vol.Optional(CONF_GROUP, default=True): cv.boolean, - # deprecated config options - vol.Optional(CONF_ALIASSES): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_GROUP_ALIASSES): vol.All( - cv.ensure_list, [cv.string] - ), - vol.Optional(CONF_NOGROUP_ALIASSES): vol.All( - cv.ensure_list, [cv.string] - ), } ) }, @@ -131,7 +119,6 @@ def devices_from_config(domain_config): entity_class = entity_class_for_type(entity_type) device_config = dict(domain_config[CONF_DEVICE_DEFAULTS], **config) - remove_deprecated(device_config) is_hybrid = entity_class is HybridRflinkLight diff --git a/homeassistant/components/rflink/manifest.json b/homeassistant/components/rflink/manifest.json index bda260bdff2..8c322e5bdf5 100644 --- a/homeassistant/components/rflink/manifest.json +++ b/homeassistant/components/rflink/manifest.json @@ -3,7 +3,7 @@ "name": "Rflink", "documentation": "https://www.home-assistant.io/integrations/rflink", "requirements": [ - "rflink==0.0.46" + "rflink==0.0.50" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/rflink/sensor.py b/homeassistant/components/rflink/sensor.py index aa0ef4f9c62..bc736a1ede6 100644 --- a/homeassistant/components/rflink/sensor.py +++ b/homeassistant/components/rflink/sensor.py @@ -15,7 +15,6 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import ( CONF_ALIASES, - CONF_ALIASSES, CONF_AUTOMATIC_ADD, CONF_DEVICES, DATA_DEVICE_REGISTER, @@ -27,7 +26,6 @@ from . import ( SIGNAL_HANDLE_EVENT, TMP_ENTITY, RflinkDevice, - remove_deprecated, ) _LOGGER = logging.getLogger(__name__) @@ -52,8 +50,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_ALIASES, default=[]): vol.All( cv.ensure_list, [cv.string] ), - # deprecated config options - vol.Optional(CONF_ALIASSES): vol.All(cv.ensure_list, [cv.string]), } ) }, @@ -80,7 +76,6 @@ def devices_from_config(domain_config): config[ATTR_UNIT_OF_MEASUREMENT] = lookup_unit_for_sensor_type( config[CONF_SENSOR_TYPE] ) - remove_deprecated(config) device = RflinkSensor(device_id, **config) devices.append(device) diff --git a/homeassistant/components/rflink/switch.py b/homeassistant/components/rflink/switch.py index c9173acc1a5..8e0ce9a0c8e 100644 --- a/homeassistant/components/rflink/switch.py +++ b/homeassistant/components/rflink/switch.py @@ -9,19 +9,15 @@ import homeassistant.helpers.config_validation as cv from . import ( CONF_ALIASES, - CONF_ALIASSES, CONF_DEVICE_DEFAULTS, CONF_DEVICES, CONF_FIRE_EVENT, CONF_GROUP, CONF_GROUP_ALIASES, - CONF_GROUP_ALIASSES, CONF_NOGROUP_ALIASES, - CONF_NOGROUP_ALIASSES, CONF_SIGNAL_REPETITIONS, DEVICE_DEFAULTS_SCHEMA, SwitchableRflinkDevice, - remove_deprecated, ) _LOGGER = logging.getLogger(__name__) @@ -47,14 +43,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_FIRE_EVENT): cv.boolean, vol.Optional(CONF_SIGNAL_REPETITIONS): vol.Coerce(int), vol.Optional(CONF_GROUP, default=True): cv.boolean, - # deprecated config options - vol.Optional(CONF_ALIASSES): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_GROUP_ALIASSES): vol.All( - cv.ensure_list, [cv.string] - ), - vol.Optional(CONF_NOGROUP_ALIASSES): vol.All( - cv.ensure_list, [cv.string] - ), } ) }, @@ -68,7 +56,6 @@ def devices_from_config(domain_config): devices = [] for device_id, config in domain_config[CONF_DEVICES].items(): device_config = dict(domain_config[CONF_DEVICE_DEFAULTS], **config) - remove_deprecated(device_config) device = RflinkSwitch(device_id, **device_config) devices.append(device) diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index 1515ce33c6e..ceba82cf544 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -12,6 +12,8 @@ from homeassistant.const import ( ATTR_STATE, CONF_DEVICE, CONF_DEVICES, + CONF_HOST, + CONF_PORT, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, POWER_WATT, @@ -80,17 +82,21 @@ RFX_DEVICES = {} _LOGGER = logging.getLogger(__name__) DATA_RFXOBJECT = "rfxobject" -CONFIG_SCHEMA = vol.Schema( +BASE_SCHEMA = vol.Schema( { - DOMAIN: vol.Schema( - { - vol.Required(CONF_DEVICE): cv.string, - vol.Optional(CONF_DEBUG, default=False): cv.boolean, - vol.Optional(CONF_DUMMY, default=False): cv.boolean, - } - ) - }, - extra=vol.ALLOW_EXTRA, + vol.Optional(CONF_DEBUG, default=False): cv.boolean, + vol.Optional(CONF_DUMMY, default=False): cv.boolean, + } +) + +DEVICE_SCHEMA = BASE_SCHEMA.extend({vol.Required(CONF_DEVICE): cv.string}) + +PORT_SCHEMA = BASE_SCHEMA.extend( + {vol.Required(CONF_PORT): cv.port, vol.Optional(CONF_HOST): cv.string} +) + +CONFIG_SCHEMA = vol.Schema( + {DOMAIN: vol.Any(DEVICE_SCHEMA, PORT_SCHEMA)}, extra=vol.ALLOW_EXTRA ) @@ -115,7 +121,9 @@ def setup(hass, config): for subscriber in RECEIVED_EVT_SUBSCRIBERS: subscriber(event) - device = config[DOMAIN][ATTR_DEVICE] + device = config[DOMAIN].get(ATTR_DEVICE) + host = config[DOMAIN].get(CONF_HOST) + port = config[DOMAIN].get(CONF_PORT) debug = config[DOMAIN][ATTR_DEBUG] dummy_connection = config[DOMAIN][ATTR_DUMMY] @@ -123,6 +131,14 @@ def setup(hass, config): rfx_object = rfxtrxmod.Connect( device, None, debug=debug, transport_protocol=rfxtrxmod.DummyTransport2 ) + elif port is not None: + # If port is set then we create a TCP connection + rfx_object = rfxtrxmod.Connect( + (host, port), + None, + debug=debug, + transport_protocol=rfxtrxmod.PyNetworkTransport, + ) else: rfx_object = rfxtrxmod.Connect(device, None, debug=debug) diff --git a/homeassistant/components/rfxtrx/binary_sensor.py b/homeassistant/components/rfxtrx/binary_sensor.py index 6465dc36326..a88594dccea 100644 --- a/homeassistant/components/rfxtrx/binary_sensor.py +++ b/homeassistant/components/rfxtrx/binary_sensor.py @@ -4,7 +4,6 @@ import logging import RFXtrx as rfxtrxmod import voluptuous as vol -from homeassistant.components import rfxtrx from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA, @@ -26,6 +25,14 @@ from . import ( CONF_DEVICES, CONF_FIRE_EVENT, CONF_OFF_DELAY, + RECEIVED_EVT_SUBSCRIBERS, + RFX_DEVICES, + apply_received_command, + find_possible_pt2262_device, + get_pt2262_cmd, + get_pt2262_device, + get_pt2262_deviceid, + get_rfx_object, ) _LOGGER = logging.getLogger(__name__) @@ -58,16 +65,16 @@ def setup_platform(hass, config, add_entities, discovery_info=None): sensors = [] for packet_id, entity in config[CONF_DEVICES].items(): - event = rfxtrx.get_rfx_object(packet_id) + event = get_rfx_object(packet_id) device_id = slugify(event.device.id_string.lower()) - if device_id in rfxtrx.RFX_DEVICES: + if device_id in RFX_DEVICES: continue if entity.get(CONF_DATA_BITS) is not None: _LOGGER.debug( "Masked device id: %s", - rfxtrx.get_pt2262_deviceid(device_id, entity.get(CONF_DATA_BITS)), + get_pt2262_deviceid(device_id, entity.get(CONF_DATA_BITS)), ) _LOGGER.debug( @@ -88,7 +95,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) device.hass = hass sensors.append(device) - rfxtrx.RFX_DEVICES[device_id] = device + RFX_DEVICES[device_id] = device add_entities(sensors) @@ -99,10 +106,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): device_id = slugify(event.device.id_string.lower()) - if device_id in rfxtrx.RFX_DEVICES: - sensor = rfxtrx.RFX_DEVICES[device_id] - else: - sensor = rfxtrx.get_pt2262_device(device_id) + sensor = RFX_DEVICES.get(device_id, get_pt2262_device(device_id)) if sensor is None: # Add the entity if not exists and automatic_add is True @@ -110,7 +114,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return if event.device.packettype == 0x13: - poss_dev = rfxtrx.find_possible_pt2262_device(device_id) + poss_dev = find_possible_pt2262_device(device_id) if poss_dev is not None: poss_id = slugify(poss_dev.event.device.id_string.lower()) _LOGGER.debug("Found possible matching device ID: %s", poss_id) @@ -118,7 +122,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): pkt_id = "".join(f"{x:02x}" for x in event.data) sensor = RfxtrxBinarySensor(event, pkt_id) sensor.hass = hass - rfxtrx.RFX_DEVICES[device_id] = sensor + RFX_DEVICES[device_id] = sensor add_entities([sensor]) _LOGGER.info( "Added binary sensor %s (Device ID: %s Class: %s Sub: %s)", @@ -140,12 +144,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if sensor.is_lighting4: if sensor.data_bits is not None: - cmd = rfxtrx.get_pt2262_cmd(device_id, sensor.data_bits) + cmd = get_pt2262_cmd(device_id, sensor.data_bits) sensor.apply_cmd(int(cmd, 16)) else: sensor.update_state(True) else: - rfxtrx.apply_received_command(event) + apply_received_command(event) if ( sensor.is_on @@ -163,8 +167,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) # Subscribe to main RFXtrx events - if binary_sensor_update not in rfxtrx.RECEIVED_EVT_SUBSCRIBERS: - rfxtrx.RECEIVED_EVT_SUBSCRIBERS.append(binary_sensor_update) + if binary_sensor_update not in RECEIVED_EVT_SUBSCRIBERS: + RECEIVED_EVT_SUBSCRIBERS.append(binary_sensor_update) class RfxtrxBinarySensor(BinarySensorDevice): @@ -195,7 +199,7 @@ class RfxtrxBinarySensor(BinarySensorDevice): self._cmd_off = cmd_off if data_bits is not None: - self._masked_id = rfxtrx.get_pt2262_deviceid( + self._masked_id = get_pt2262_deviceid( event.device.id_string.lower(), data_bits ) else: diff --git a/homeassistant/components/rfxtrx/cover.py b/homeassistant/components/rfxtrx/cover.py index 7aff22bd124..e1eb6ae77f5 100644 --- a/homeassistant/components/rfxtrx/cover.py +++ b/homeassistant/components/rfxtrx/cover.py @@ -2,10 +2,10 @@ import RFXtrx as rfxtrxmod import voluptuous as vol -from homeassistant.components import rfxtrx from homeassistant.components.cover import PLATFORM_SCHEMA, CoverDevice -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_NAME, STATE_OPEN from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.restore_state import RestoreEntity from . import ( CONF_AUTOMATIC_ADD, @@ -13,6 +13,11 @@ from . import ( CONF_FIRE_EVENT, CONF_SIGNAL_REPETITIONS, DEFAULT_SIGNAL_REPETITIONS, + RECEIVED_EVT_SUBSCRIBERS, + RfxtrxDevice, + apply_received_command, + get_devices_from_config, + get_new_device, ) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -35,7 +40,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the RFXtrx cover.""" - covers = rfxtrx.get_devices_from_config(config, RfxtrxCover) + covers = get_devices_from_config(config, RfxtrxCover) add_entities(covers) def cover_update(event): @@ -47,20 +52,28 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ): return - new_device = rfxtrx.get_new_device(event, config, RfxtrxCover) + new_device = get_new_device(event, config, RfxtrxCover) if new_device: add_entities([new_device]) - rfxtrx.apply_received_command(event) + apply_received_command(event) # Subscribe to main RFXtrx events - if cover_update not in rfxtrx.RECEIVED_EVT_SUBSCRIBERS: - rfxtrx.RECEIVED_EVT_SUBSCRIBERS.append(cover_update) + if cover_update not in RECEIVED_EVT_SUBSCRIBERS: + RECEIVED_EVT_SUBSCRIBERS.append(cover_update) -class RfxtrxCover(rfxtrx.RfxtrxDevice, CoverDevice): +class RfxtrxCover(RfxtrxDevice, CoverDevice, RestoreEntity): """Representation of a RFXtrx cover.""" + async def async_added_to_hass(self): + """Restore RFXtrx cover device state (OPEN/CLOSE).""" + await super().async_added_to_hass() + + old_state = await self.async_get_last_state() + if old_state is not None: + self._state = old_state.state == STATE_OPEN + @property def should_poll(self): """Return the polling state. No polling available in RFXtrx cover.""" diff --git a/homeassistant/components/rfxtrx/light.py b/homeassistant/components/rfxtrx/light.py index a745a11388a..437cce89c49 100644 --- a/homeassistant/components/rfxtrx/light.py +++ b/homeassistant/components/rfxtrx/light.py @@ -4,15 +4,15 @@ import logging import RFXtrx as rfxtrxmod import voluptuous as vol -from homeassistant.components import rfxtrx from homeassistant.components.light import ( ATTR_BRIGHTNESS, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, Light, ) -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_NAME, STATE_ON from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.restore_state import RestoreEntity from . import ( CONF_AUTOMATIC_ADD, @@ -20,6 +20,11 @@ from . import ( CONF_FIRE_EVENT, CONF_SIGNAL_REPETITIONS, DEFAULT_SIGNAL_REPETITIONS, + RECEIVED_EVT_SUBSCRIBERS, + RfxtrxDevice, + apply_received_command, + get_devices_from_config, + get_new_device, ) _LOGGER = logging.getLogger(__name__) @@ -46,7 +51,7 @@ SUPPORT_RFXTRX = SUPPORT_BRIGHTNESS def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the RFXtrx platform.""" - lights = rfxtrx.get_devices_from_config(config, RfxtrxLight) + lights = get_devices_from_config(config, RfxtrxLight) add_entities(lights) def light_update(event): @@ -57,20 +62,35 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ): return - new_device = rfxtrx.get_new_device(event, config, RfxtrxLight) + new_device = get_new_device(event, config, RfxtrxLight) if new_device: add_entities([new_device]) - rfxtrx.apply_received_command(event) + apply_received_command(event) # Subscribe to main RFXtrx events - if light_update not in rfxtrx.RECEIVED_EVT_SUBSCRIBERS: - rfxtrx.RECEIVED_EVT_SUBSCRIBERS.append(light_update) + if light_update not in RECEIVED_EVT_SUBSCRIBERS: + RECEIVED_EVT_SUBSCRIBERS.append(light_update) -class RfxtrxLight(rfxtrx.RfxtrxDevice, Light): +class RfxtrxLight(RfxtrxDevice, Light, RestoreEntity): """Representation of a RFXtrx light.""" + async def async_added_to_hass(self): + """Restore RFXtrx device state (ON/OFF).""" + await super().async_added_to_hass() + + old_state = await self.async_get_last_state() + if old_state is not None: + self._state = old_state.state == STATE_ON + + # Restore the brightness of dimmable devices + if ( + old_state is not None + and old_state.attributes.get(ATTR_BRIGHTNESS) is not None + ): + self._brightness = int(old_state.attributes[ATTR_BRIGHTNESS]) + @property def brightness(self): """Return the brightness of this light between 0..255.""" diff --git a/homeassistant/components/rfxtrx/manifest.json b/homeassistant/components/rfxtrx/manifest.json index a75a8ba9eb1..e26ceb7ef57 100644 --- a/homeassistant/components/rfxtrx/manifest.json +++ b/homeassistant/components/rfxtrx/manifest.json @@ -3,7 +3,7 @@ "name": "Rfxtrx", "documentation": "https://www.home-assistant.io/integrations/rfxtrx", "requirements": [ - "pyRFXtrx==0.23" + "pyRFXtrx==0.24" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/rfxtrx/sensor.py b/homeassistant/components/rfxtrx/sensor.py index 5429943a7a6..3f74ff18695 100644 --- a/homeassistant/components/rfxtrx/sensor.py +++ b/homeassistant/components/rfxtrx/sensor.py @@ -4,7 +4,6 @@ import logging from RFXtrx import SensorEvent import voluptuous as vol -from homeassistant.components import rfxtrx from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME, CONF_NAME import homeassistant.helpers.config_validation as cv @@ -19,6 +18,9 @@ from . import ( CONF_DEVICES, CONF_FIRE_EVENT, DATA_TYPES, + RECEIVED_EVT_SUBSCRIBERS, + RFX_DEVICES, + get_rfx_object, ) _LOGGER = logging.getLogger(__name__) @@ -46,9 +48,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the RFXtrx platform.""" sensors = [] for packet_id, entity_info in config[CONF_DEVICES].items(): - event = rfxtrx.get_rfx_object(packet_id) + event = get_rfx_object(packet_id) device_id = "sensor_{}".format(slugify(event.device.id_string.lower())) - if device_id in rfxtrx.RFX_DEVICES: + if device_id in RFX_DEVICES: continue _LOGGER.info("Add %s rfxtrx.sensor", entity_info[ATTR_NAME]) @@ -66,7 +68,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) sensors.append(new_sensor) sub_sensors[_data_type] = new_sensor - rfxtrx.RFX_DEVICES[device_id] = sub_sensors + RFX_DEVICES[device_id] = sub_sensors add_entities(sensors) def sensor_update(event): @@ -76,8 +78,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): device_id = "sensor_" + slugify(event.device.id_string.lower()) - if device_id in rfxtrx.RFX_DEVICES: - sensors = rfxtrx.RFX_DEVICES[device_id] + if device_id in RFX_DEVICES: + sensors = RFX_DEVICES[device_id] for data_type in sensors: # Some multi-sensor devices send individual messages for each # of their sensors. Update only if event contains the @@ -108,11 +110,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): new_sensor = RfxtrxSensor(event, pkt_id, data_type) sub_sensors = {} sub_sensors[new_sensor.data_type] = new_sensor - rfxtrx.RFX_DEVICES[device_id] = sub_sensors + RFX_DEVICES[device_id] = sub_sensors add_entities([new_sensor]) - if sensor_update not in rfxtrx.RECEIVED_EVT_SUBSCRIBERS: - rfxtrx.RECEIVED_EVT_SUBSCRIBERS.append(sensor_update) + if sensor_update not in RECEIVED_EVT_SUBSCRIBERS: + RECEIVED_EVT_SUBSCRIBERS.append(sensor_update) class RfxtrxSensor(Entity): diff --git a/homeassistant/components/rfxtrx/switch.py b/homeassistant/components/rfxtrx/switch.py index 6d91b261a4f..05e4a37ab44 100644 --- a/homeassistant/components/rfxtrx/switch.py +++ b/homeassistant/components/rfxtrx/switch.py @@ -4,10 +4,10 @@ import logging import RFXtrx as rfxtrxmod import voluptuous as vol -from homeassistant.components import rfxtrx from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_NAME, STATE_ON from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.restore_state import RestoreEntity from . import ( CONF_AUTOMATIC_ADD, @@ -15,6 +15,11 @@ from . import ( CONF_FIRE_EVENT, CONF_SIGNAL_REPETITIONS, DEFAULT_SIGNAL_REPETITIONS, + RECEIVED_EVT_SUBSCRIBERS, + RfxtrxDevice, + apply_received_command, + get_devices_from_config, + get_new_device, ) _LOGGER = logging.getLogger(__name__) @@ -40,7 +45,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities_callback, discovery_info=None): """Set up the RFXtrx platform.""" # Add switch from config file - switches = rfxtrx.get_devices_from_config(config, RfxtrxSwitch) + switches = get_devices_from_config(config, RfxtrxSwitch) add_entities_callback(switches) def switch_update(event): @@ -52,20 +57,28 @@ def setup_platform(hass, config, add_entities_callback, discovery_info=None): ): return - new_device = rfxtrx.get_new_device(event, config, RfxtrxSwitch) + new_device = get_new_device(event, config, RfxtrxSwitch) if new_device: add_entities_callback([new_device]) - rfxtrx.apply_received_command(event) + apply_received_command(event) # Subscribe to main RFXtrx events - if switch_update not in rfxtrx.RECEIVED_EVT_SUBSCRIBERS: - rfxtrx.RECEIVED_EVT_SUBSCRIBERS.append(switch_update) + if switch_update not in RECEIVED_EVT_SUBSCRIBERS: + RECEIVED_EVT_SUBSCRIBERS.append(switch_update) -class RfxtrxSwitch(rfxtrx.RfxtrxDevice, SwitchDevice): +class RfxtrxSwitch(RfxtrxDevice, SwitchDevice, RestoreEntity): """Representation of a RFXtrx switch.""" + async def async_added_to_hass(self): + """Restore RFXtrx switch device state (ON/OFF).""" + await super().async_added_to_hass() + + old_state = await self.async_get_last_state() + if old_state is not None: + self._state = old_state.state == STATE_ON + def turn_on(self, **kwargs): """Turn the device on.""" self._send_command("turn_on") diff --git a/homeassistant/components/ring/__init__.py b/homeassistant/components/ring/__init__.py index 006b3ae3e9a..a68749b2c67 100644 --- a/homeassistant/components/ring/__init__.py +++ b/homeassistant/components/ring/__init__.py @@ -1,14 +1,15 @@ """Support for Ring Doorbell/Chimes.""" +from datetime import timedelta import logging -from datetime import timedelta from requests.exceptions import ConnectTimeout, HTTPError +from ring_doorbell import Ring import voluptuous as vol -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, CONF_SCAN_INTERVAL -from homeassistant.helpers.event import track_time_interval -from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.event import track_time_interval _LOGGER = logging.getLogger(__name__) @@ -50,8 +51,6 @@ def setup(hass, config): scan_interval = conf[CONF_SCAN_INTERVAL] try: - from ring_doorbell import Ring - cache = hass.config.path(DEFAULT_CACHEDB) ring = Ring(username=username, password=password, cache_file=cache) if not ring.is_connected: diff --git a/homeassistant/components/ring/camera.py b/homeassistant/components/ring/camera.py index 461b3a199d7..1d2fe6ff67b 100644 --- a/homeassistant/components/ring/camera.py +++ b/homeassistant/components/ring/camera.py @@ -3,16 +3,18 @@ import asyncio from datetime import timedelta import logging +from haffmpeg.camera import CameraMjpeg +from haffmpeg.tools import IMAGE_JPEG, ImageFrame import voluptuous as vol from homeassistant.components.camera import PLATFORM_SCHEMA, Camera from homeassistant.components.ffmpeg import DATA_FFMPEG from homeassistant.const import ATTR_ATTRIBUTION +from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream -from homeassistant.util import dt as dt_util from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.core import callback +from homeassistant.util import dt as dt_util from . import ( ATTRIBUTION, @@ -122,7 +124,6 @@ class RingCam(Camera): async def async_camera_image(self): """Return a still image response from the camera.""" - from haffmpeg.tools import ImageFrame, IMAGE_JPEG ffmpeg = ImageFrame(self._ffmpeg.binary, loop=self.hass.loop) @@ -140,7 +141,6 @@ class RingCam(Camera): async def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from the camera.""" - from haffmpeg.camera import CameraMjpeg if self._video_url is None: return diff --git a/homeassistant/components/ring/light.py b/homeassistant/components/ring/light.py index 697be4d1579..bc86e5b5fd1 100644 --- a/homeassistant/components/ring/light.py +++ b/homeassistant/components/ring/light.py @@ -1,9 +1,10 @@ """This component provides HA switch support for Ring Door Bell/Chimes.""" -import logging from datetime import timedelta +import logging + from homeassistant.components.light import Light -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.util.dt as dt_util from . import DATA_RING_STICKUP_CAMS, SIGNAL_UPDATE_RING diff --git a/homeassistant/components/ring/manifest.json b/homeassistant/components/ring/manifest.json index 47fc2f3a6a8..6fc57244deb 100644 --- a/homeassistant/components/ring/manifest.json +++ b/homeassistant/components/ring/manifest.json @@ -2,11 +2,7 @@ "domain": "ring", "name": "Ring", "documentation": "https://www.home-assistant.io/integrations/ring", - "requirements": [ - "ring_doorbell==0.2.3" - ], - "dependencies": [ - "ffmpeg" - ], + "requirements": ["ring_doorbell==0.2.8"], + "dependencies": ["ffmpeg"], "codeowners": [] } diff --git a/homeassistant/components/ring/sensor.py b/homeassistant/components/ring/sensor.py index 6a64226a053..b54c750664e 100644 --- a/homeassistant/components/ring/sensor.py +++ b/homeassistant/components/ring/sensor.py @@ -9,11 +9,11 @@ from homeassistant.const import ( CONF_ENTITY_NAMESPACE, CONF_MONITORED_CONDITIONS, ) +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 homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.core import callback from . import ( ATTRIBUTION, diff --git a/homeassistant/components/ring/switch.py b/homeassistant/components/ring/switch.py index 413d2a70aae..16fc4a6717f 100644 --- a/homeassistant/components/ring/switch.py +++ b/homeassistant/components/ring/switch.py @@ -1,9 +1,10 @@ """This component provides HA switch support for Ring Door Bell/Chimes.""" -import logging from datetime import timedelta +import logging + from homeassistant.components.switch import SwitchDevice -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.util.dt as dt_util from . import DATA_RING_STICKUP_CAMS, SIGNAL_UPDATE_RING diff --git a/homeassistant/components/ripple/sensor.py b/homeassistant/components/ripple/sensor.py index ebbcec708c3..ab0da77b173 100644 --- a/homeassistant/components/ripple/sensor.py +++ b/homeassistant/components/ripple/sensor.py @@ -1,6 +1,7 @@ """Support for Ripple sensors.""" from datetime import timedelta +from pyripple import get_balance import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -62,7 +63,6 @@ class RippleSensor(Entity): def update(self): """Get the latest state of the sensor.""" - from pyripple import get_balance balance = get_balance(self.address) if balance is not None: diff --git a/homeassistant/components/rmvtransport/sensor.py b/homeassistant/components/rmvtransport/sensor.py index 190274518cd..8df1191a420 100644 --- a/homeassistant/components/rmvtransport/sensor.py +++ b/homeassistant/components/rmvtransport/sensor.py @@ -1,14 +1,14 @@ """Support for departure information for Rhein-Main public transport.""" import asyncio -import logging from datetime import timedelta +import logging from RMVtransport import RMVtransport from RMVtransport.rmvtransport import RMVtransportApiConnectionError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION +from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/rocketchat/notify.py b/homeassistant/components/rocketchat/notify.py index 8657d0f9450..2b5b8dcd235 100644 --- a/homeassistant/components/rocketchat/notify.py +++ b/homeassistant/components/rocketchat/notify.py @@ -1,16 +1,20 @@ """Rocket.Chat notification service.""" import logging +from rocketchat_API.APIExceptions.RocketExceptions import ( + RocketAuthenticationException, + RocketConnectionException, +) +from rocketchat_API.rocketchat import RocketChat import voluptuous as vol -from homeassistant.const import CONF_PASSWORD, CONF_ROOM, CONF_URL, CONF_USERNAME -import homeassistant.helpers.config_validation as cv - from homeassistant.components.notify import ( ATTR_DATA, PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.const import CONF_PASSWORD, CONF_ROOM, CONF_URL, CONF_USERNAME +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -27,10 +31,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def get_service(hass, config, discovery_info=None): """Return the notify service.""" - from rocketchat_API.APIExceptions.RocketExceptions import ( - RocketConnectionException, - RocketAuthenticationException, - ) username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) @@ -54,7 +54,6 @@ class RocketChatNotificationService(BaseNotificationService): def __init__(self, url, username, password, room): """Initialize the service.""" - from rocketchat_API.rocketchat import RocketChat self._room = room self._server = RocketChat(username, password, server_url=url) diff --git a/homeassistant/components/roku/__init__.py b/homeassistant/components/roku/__init__.py index aa13814ee6b..b84b6dd1e63 100644 --- a/homeassistant/components/roku/__init__.py +++ b/homeassistant/components/roku/__init__.py @@ -1,6 +1,7 @@ """Support for Roku.""" import logging +from roku import Roku, RokuException import voluptuous as vol from homeassistant.components.discovery import SERVICE_ROKU @@ -64,7 +65,6 @@ def setup(hass, config): def scan_for_rokus(hass): """Scan for devices and present a notification of the ones found.""" - from roku import Roku, RokuException rokus = Roku.discover() @@ -94,7 +94,6 @@ def scan_for_rokus(hass): def _setup_roku(hass, hass_config, roku_config): """Set up a Roku.""" - from roku import Roku host = roku_config[CONF_HOST] diff --git a/homeassistant/components/roku/manifest.json b/homeassistant/components/roku/manifest.json index f2639b31d15..1b5f07eb87a 100644 --- a/homeassistant/components/roku/manifest.json +++ b/homeassistant/components/roku/manifest.json @@ -3,7 +3,7 @@ "name": "Roku", "documentation": "https://www.home-assistant.io/integrations/roku", "requirements": [ - "roku==3.1" + "roku==4.0.0" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index 12aca141510..a785d7b18ff 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -1,6 +1,8 @@ """Support for the Roku media player.""" import logging + import requests.exceptions +from roku import Roku from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( @@ -10,10 +12,10 @@ from homeassistant.components.media_player.const import ( SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - SUPPORT_TURN_ON, - SUPPORT_TURN_OFF, ) from homeassistant.const import ( CONF_HOST, @@ -54,7 +56,6 @@ class RokuDevice(MediaPlayerDevice): def __init__(self, host): """Initialize the Roku device.""" - from roku import Roku self.roku = Roku(host) self.ip_address = host @@ -174,7 +175,7 @@ class RokuDevice(MediaPlayerDevice): def turn_on(self): """Turn on the Roku.""" - self.roku.power() + self.roku.poweron() def turn_off(self): """Turn off the Roku.""" diff --git a/homeassistant/components/roku/remote.py b/homeassistant/components/roku/remote.py index f443b7e8e74..c953d9ba734 100644 --- a/homeassistant/components/roku/remote.py +++ b/homeassistant/components/roku/remote.py @@ -1,5 +1,6 @@ """Support for the Roku remote.""" import requests.exceptions +from roku import Roku from homeassistant.components import remote from homeassistant.const import CONF_HOST @@ -19,7 +20,6 @@ class RokuRemote(remote.RemoteDevice): def __init__(self, host): """Initialize the Roku device.""" - from roku import Roku self.roku = Roku(host) self._device_info = {} diff --git a/homeassistant/components/roku/services.yaml b/homeassistant/components/roku/services.yaml index e69de29bb2d..956ecb0dd2d 100644 --- a/homeassistant/components/roku/services.yaml +++ b/homeassistant/components/roku/services.yaml @@ -0,0 +1,2 @@ +roku_scan: + description: Scans the local network for Rokus. All found devices are presented as a persistent notification. diff --git a/homeassistant/components/roomba/manifest.json b/homeassistant/components/roomba/manifest.json index 5064357a7df..92ed16406db 100644 --- a/homeassistant/components/roomba/manifest.json +++ b/homeassistant/components/roomba/manifest.json @@ -3,7 +3,7 @@ "name": "Roomba", "documentation": "https://www.home-assistant.io/integrations/roomba", "requirements": [ - "roombapy==1.3.1" + "roombapy==1.4.2" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/roomba/vacuum.py b/homeassistant/components/roomba/vacuum.py index 291658e19f4..172a494b602 100644 --- a/homeassistant/components/roomba/vacuum.py +++ b/homeassistant/components/roomba/vacuum.py @@ -3,12 +3,14 @@ import asyncio import logging import async_timeout +from roomba import Roomba import voluptuous as vol from homeassistant.components.vacuum import ( PLATFORM_SCHEMA, SUPPORT_BATTERY, SUPPORT_FAN_SPEED, + SUPPORT_LOCATE, SUPPORT_PAUSE, SUPPORT_RETURN_HOME, SUPPORT_SEND_COMMAND, @@ -16,7 +18,6 @@ from homeassistant.components.vacuum import ( SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, - SUPPORT_LOCATE, VacuumDevice, ) from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME @@ -33,15 +34,16 @@ ATTR_ERROR = "error" ATTR_POSITION = "position" ATTR_SOFTWARE_VERSION = "software_version" -CAP_BIN_FULL = "bin_full" CAP_POSITION = "position" CAP_CARPET_BOOST = "carpet_boost" CONF_CERT = "certificate" CONF_CONTINUOUS = "continuous" +CONF_DELAY = "delay" DEFAULT_CERT = "/etc/ssl/certs/ca-certificates.crt" DEFAULT_CONTINUOUS = True +DEFAULT_DELAY = 1 DEFAULT_NAME = "Roomba" PLATFORM = "roomba" @@ -59,6 +61,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Required(CONF_PASSWORD): cv.string, vol.Optional(CONF_CERT, default=DEFAULT_CERT): cv.string, vol.Optional(CONF_CONTINUOUS, default=DEFAULT_CONTINUOUS): cv.boolean, + vol.Optional(CONF_DELAY, default=DEFAULT_DELAY): cv.positive_int, }, extra=vol.ALLOW_EXTRA, ) @@ -82,7 +85,6 @@ SUPPORT_ROOMBA_CARPET_BOOST = SUPPORT_ROOMBA | SUPPORT_FAN_SPEED async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the iRobot Roomba vacuum cleaner platform.""" - from roomba import Roomba if PLATFORM not in hass.data: hass.data[PLATFORM] = {} @@ -93,6 +95,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= password = config.get(CONF_PASSWORD) certificate = config.get(CONF_CERT) continuous = config.get(CONF_CONTINUOUS) + delay = config.get(CONF_DELAY) roomba = Roomba( address=host, @@ -100,6 +103,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= password=password, cert_name=certificate, continuous=continuous, + delay=delay, ) _LOGGER.debug("Initializing communication with host %s", host) @@ -271,18 +275,14 @@ class RoombaVacuum(VacuumDevice): # Get the capabilities of our unit capabilities = state.get("cap", {}) - cap_bin_full = capabilities.get("binFullDetect") cap_carpet_boost = capabilities.get("carpetBoost") cap_pos = capabilities.get("pose") # Store capabilities self._capabilities = { - CAP_BIN_FULL: cap_bin_full == 1, CAP_CARPET_BOOST: cap_carpet_boost == 1, CAP_POSITION: cap_pos == 1, } - bin_state = state.get("bin", {}) - # Roomba software version software_version = state.get("softwareVer") @@ -296,10 +296,11 @@ class RoombaVacuum(VacuumDevice): self._is_on = self._status in ["Running"] # Set properties that are to appear in the GUI - self._state_attrs = { - ATTR_BIN_PRESENT: bin_state.get("present"), - ATTR_SOFTWARE_VERSION: software_version, - } + self._state_attrs = {ATTR_SOFTWARE_VERSION: software_version} + + # Get bin state + bin_state = self._get_bin_state(state) + self._state_attrs.update(bin_state) # Only add cleaning time and cleaned area attrs when the vacuum is # currently on @@ -330,10 +331,6 @@ class RoombaVacuum(VacuumDevice): position = f"({pos_x}, {pos_y}, {theta})" self._state_attrs[ATTR_POSITION] = position - # Not all Roombas have a bin full sensor - if self._capabilities[CAP_BIN_FULL]: - self._state_attrs[ATTR_BIN_FULL] = bin_state.get("full") - # Fan speed mode (Performance, Automatic or Eco) # Not all Roombas expose carpet boost if self._capabilities[CAP_CARPET_BOOST]: @@ -350,3 +347,16 @@ class RoombaVacuum(VacuumDevice): fan_speed = FAN_SPEED_ECO self._fan_speed = fan_speed + + @staticmethod + def _get_bin_state(state): + bin_raw_state = state.get("bin", {}) + bin_state = {} + + if bin_raw_state.get("present") is not None: + bin_state[ATTR_BIN_PRESENT] = bin_raw_state.get("present") + + if bin_raw_state.get("full") is not None: + bin_state[ATTR_BIN_FULL] = bin_raw_state.get("full") + + return bin_state diff --git a/homeassistant/components/route53/__init__.py b/homeassistant/components/route53/__init__.py index 3dffc3ffd9e..a84475ab8a1 100644 --- a/homeassistant/components/route53/__init__.py +++ b/homeassistant/components/route53/__init__.py @@ -3,6 +3,8 @@ from datetime import timedelta import logging from typing import List +import boto3 +from ipify import exceptions, get_ip import voluptuous as vol from homeassistant.const import CONF_DOMAIN, CONF_TTL, CONF_ZONE @@ -72,10 +74,6 @@ def _update_route53( records: List[str], ttl: int, ): - import boto3 - from ipify import get_ip - from ipify import exceptions - _LOGGER.debug("Starting update for zone %s", zone) client = boto3.client( diff --git a/homeassistant/components/rova/sensor.py b/homeassistant/components/rova/sensor.py index fe0b5dead84..86a04829c75 100644 --- a/homeassistant/components/rova/sensor.py +++ b/homeassistant/components/rova/sensor.py @@ -3,6 +3,8 @@ from datetime import datetime, timedelta import logging +from requests.exceptions import ConnectTimeout, HTTPError +from rova.rova import Rova import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -49,8 +51,6 @@ _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_entities, discovery_info=None): """Create the Rova data service and sensors.""" - from rova.rova import Rova - from requests.exceptions import HTTPError, ConnectTimeout zip_code = config[CONF_ZIP_CODE] house_number = config[CONF_HOUSE_NUMBER] @@ -132,7 +132,6 @@ class RovaData: @Throttle(UPDATE_DELAY) def update(self): """Update the data from the Rova API.""" - from requests.exceptions import HTTPError, ConnectTimeout try: items = self.api.get_calendar_items() diff --git a/homeassistant/components/rpi_camera/camera.py b/homeassistant/components/rpi_camera/camera.py index d486f406e41..bf04e0ef492 100644 --- a/homeassistant/components/rpi_camera/camera.py +++ b/homeassistant/components/rpi_camera/camera.py @@ -1,14 +1,14 @@ """Camera platform that has a Raspberry Pi camera.""" -import os -import subprocess import logging +import os import shutil +import subprocess from tempfile import NamedTemporaryFile import voluptuous as vol -from homeassistant.components.camera import Camera, PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, CONF_FILE_PATH, EVENT_HOMEASSISTANT_STOP +from homeassistant.components.camera import PLATFORM_SCHEMA, Camera +from homeassistant.const import CONF_FILE_PATH, CONF_NAME, EVENT_HOMEASSISTANT_STOP from homeassistant.helpers import config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/rpi_gpio/switch.py b/homeassistant/components/rpi_gpio/switch.py index e9a46978eaf..03cb9f083ce 100644 --- a/homeassistant/components/rpi_gpio/switch.py +++ b/homeassistant/components/rpi_gpio/switch.py @@ -3,11 +3,11 @@ import logging import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA from homeassistant.components import rpi_gpio +from homeassistant.components.switch import PLATFORM_SCHEMA from homeassistant.const import DEVICE_DEFAULT_NAME -from homeassistant.helpers.entity import ToggleEntity import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import ToggleEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/rpi_gpio_pwm/light.py b/homeassistant/components/rpi_gpio_pwm/light.py index 27dd4da80ac..aededbc676c 100644 --- a/homeassistant/components/rpi_gpio_pwm/light.py +++ b/homeassistant/components/rpi_gpio_pwm/light.py @@ -1,22 +1,28 @@ """Support for LED lights that can be controlled using PWM.""" import logging +from pwmled import Color +from pwmled.driver.gpio import GpioDriver +from pwmled.driver.pca9685 import Pca9685Driver +from pwmled.led import SimpleLed +from pwmled.led.rgb import RgbLed +from pwmled.led.rgbw import RgbwLed import voluptuous as vol -from homeassistant.const import CONF_NAME, CONF_TYPE, STATE_ON, CONF_ADDRESS from homeassistant.components.light import ( - Light, ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_TRANSITION, + PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_TRANSITION, - PLATFORM_SCHEMA, + Light, ) +from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_TYPE, STATE_ON import homeassistant.helpers.config_validation as cv -import homeassistant.util.color as color_util from homeassistant.helpers.restore_state import RestoreEntity +import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) @@ -61,11 +67,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the PWM LED lights.""" - from pwmled.led import SimpleLed - from pwmled.led.rgb import RgbLed - from pwmled.led.rgbw import RgbwLed - from pwmled.driver.gpio import GpioDriver - from pwmled.driver.pca9685 import Pca9685Driver leds = [] for led_conf in config[CONF_LEDS]: @@ -240,7 +241,6 @@ def _from_hass_brightness(brightness): def _from_hass_color(color): """Convert Home Assistant RGB list to Color tuple.""" - from pwmled import Color rgb = color_util.color_hs_to_RGB(*color) return Color(*tuple(rgb)) diff --git a/homeassistant/components/rpi_rf/switch.py b/homeassistant/components/rpi_rf/switch.py index 18e4a28d5c8..5c09111c1cb 100644 --- a/homeassistant/components/rpi_rf/switch.py +++ b/homeassistant/components/rpi_rf/switch.py @@ -1,10 +1,11 @@ """Support for a switch using a 433MHz module via GPIO on a Raspberry Pi.""" import importlib import logging +from threading import RLock import voluptuous as vol -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import CONF_NAME, CONF_SWITCHES, EVENT_HOMEASSISTANT_STOP import homeassistant.helpers.config_validation as cv @@ -44,7 +45,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( 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") - from threading import RLock gpio = config.get(CONF_GPIO) rfdevice = rpi_rf.RFDevice(gpio) diff --git a/homeassistant/components/rss_feed_template/__init__.py b/homeassistant/components/rss_feed_template/__init__.py index 440482ac31a..69bc9474267 100644 --- a/homeassistant/components/rss_feed_template/__init__.py +++ b/homeassistant/components/rss_feed_template/__init__.py @@ -1,7 +1,7 @@ """Support to export sensor values via RSS feed.""" from html import escape -from aiohttp import web +from aiohttp import web import voluptuous as vol from homeassistant.components.http import HomeAssistantView diff --git a/homeassistant/components/rtorrent/sensor.py b/homeassistant/components/rtorrent/sensor.py index ed16331e912..4ae272ca9bd 100644 --- a/homeassistant/components/rtorrent/sensor.py +++ b/homeassistant/components/rtorrent/sensor.py @@ -6,14 +6,14 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_URL, - CONF_NAME, CONF_MONITORED_VARIABLES, + CONF_NAME, + CONF_URL, STATE_IDLE, ) -from homeassistant.helpers.entity import Entity -import homeassistant.helpers.config_validation as cv from homeassistant.exceptions import PlatformNotReady +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/russound_rio/media_player.py b/homeassistant/components/russound_rio/media_player.py index 69967c21fd5..c954553160e 100644 --- a/homeassistant/components/russound_rio/media_player.py +++ b/homeassistant/components/russound_rio/media_player.py @@ -1,9 +1,10 @@ """Support for Russound multizone controllers using RIO Protocol.""" import logging +from russound_rio import Russound import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, SUPPORT_SELECT_SOURCE, @@ -44,7 +45,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Russound RIO platform.""" - from russound_rio import Russound host = config.get(CONF_HOST) port = config.get(CONF_PORT) diff --git a/homeassistant/components/russound_rnet/media_player.py b/homeassistant/components/russound_rnet/media_player.py index e62e0a6b3af..70ed1212363 100644 --- a/homeassistant/components/russound_rnet/media_player.py +++ b/homeassistant/components/russound_rnet/media_player.py @@ -1,9 +1,10 @@ """Support for interfacing with Russound via RNET Protocol.""" import logging +from russound import russound import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, @@ -51,8 +52,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): _LOGGER.error("Invalid config. Expected %s and %s", CONF_HOST, CONF_PORT) return False - from russound import russound - russ = russound.Russound(host, port) russ.connect() diff --git a/homeassistant/components/sabnzbd/__init__.py b/homeassistant/components/sabnzbd/__init__.py index bf5e90e21f1..f436bcb8a72 100644 --- a/homeassistant/components/sabnzbd/__init__.py +++ b/homeassistant/components/sabnzbd/__init__.py @@ -1,14 +1,14 @@ """Support for monitoring an SABnzbd NZB client.""" -import logging from datetime import timedelta +import logging +from pysabnzbd import SabnzbdApi, SabnzbdApiException import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.discovery import SERVICE_SABNZBD from homeassistant.const import ( - CONF_HOST, CONF_API_KEY, + CONF_HOST, CONF_NAME, CONF_PATH, CONF_PORT, @@ -18,6 +18,7 @@ from homeassistant.const import ( from homeassistant.core import callback from homeassistant.helpers import discovery 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 from homeassistant.util.json import load_json, save_json @@ -86,7 +87,6 @@ CONFIG_SCHEMA = vol.Schema( async def async_check_sabnzbd(sab_api): """Check if we can reach SABnzbd.""" - from pysabnzbd import SabnzbdApiException try: await sab_api.check_available() @@ -100,7 +100,6 @@ async def async_configure_sabnzbd( hass, config, use_ssl, name=DEFAULT_NAME, api_key=None ): """Try to configure Sabnzbd and request api key if configuration fails.""" - from pysabnzbd import SabnzbdApi host = config[CONF_HOST] port = config[CONF_PORT] @@ -174,7 +173,6 @@ def async_setup_sabnzbd(hass, sab_api, config, name): async def async_update_sabnzbd(now): """Refresh SABnzbd queue data.""" - from pysabnzbd import SabnzbdApiException try: await sab_api.refresh_data() @@ -188,7 +186,6 @@ def async_setup_sabnzbd(hass, sab_api, config, name): @callback def async_request_configuration(hass, config, host, web_root): """Request configuration steps from the user.""" - from pysabnzbd import SabnzbdApi configurator = hass.components.configurator # We got an error if this method is called while we are configuring @@ -239,7 +236,6 @@ class SabnzbdApiData: async def async_pause_queue(self): """Pause Sabnzbd queue.""" - from pysabnzbd import SabnzbdApiException try: return await self.sab_api.pause_queue() @@ -249,7 +245,6 @@ class SabnzbdApiData: async def async_resume_queue(self): """Resume Sabnzbd queue.""" - from pysabnzbd import SabnzbdApiException try: return await self.sab_api.resume_queue() @@ -259,7 +254,6 @@ class SabnzbdApiData: async def async_set_queue_speed(self, limit): """Set speed limit for the Sabnzbd queue.""" - from pysabnzbd import SabnzbdApiException try: return await self.sab_api.set_speed_limit(limit) diff --git a/homeassistant/components/saj/sensor.py b/homeassistant/components/saj/sensor.py index 2a17d110c6e..704e9996d2d 100644 --- a/homeassistant/components/saj/sensor.py +++ b/homeassistant/components/saj/sensor.py @@ -9,8 +9,8 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_HOST, - CONF_PASSWORD, CONF_NAME, + CONF_PASSWORD, CONF_TYPE, CONF_USERNAME, DEVICE_CLASS_POWER, @@ -24,6 +24,7 @@ from homeassistant.const import ( TEMP_FAHRENHEIT, ) 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 @@ -38,12 +39,12 @@ UNIT_OF_MEASUREMENT_HOURS = "h" INVERTER_TYPES = ["ethernet", "wifi"] SAJ_UNIT_MAPPINGS = { - "W": POWER_WATT, - "kWh": ENERGY_KILO_WATT_HOUR, + "": None, "h": UNIT_OF_MEASUREMENT_HOURS, "kg": MASS_KILOGRAMS, + "kWh": ENERGY_KILO_WATT_HOUR, + "W": POWER_WATT, "°C": TEMP_CELSIUS, - "": None, } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -58,7 +59,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up SAJ sensors.""" + """Set up the SAJ sensors.""" remove_interval_update = None wifi = config[CONF_TYPE] == INVERTER_TYPES[1] @@ -80,7 +81,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= saj = pysaj.SAJ(config[CONF_HOST], **kwargs) done = await saj.read(sensor_def) except pysaj.UnauthorizedException: - _LOGGER.error("Username and/or password is wrong.") + _LOGGER.error("Username and/or password is wrong") return except pysaj.UnexpectedResponseException as err: _LOGGER.error( @@ -88,13 +89,15 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) return - if done: - for sensor in sensor_def: - hass_sensors.append( - SAJsensor(saj.serialnumber, sensor, inverter_name=config.get(CONF_NAME)) - ) + if not done: + raise PlatformNotReady - async_add_entities(hass_sensors) + for sensor in sensor_def: + hass_sensors.append( + SAJsensor(saj.serialnumber, sensor, inverter_name=config.get(CONF_NAME)) + ) + + async_add_entities(hass_sensors) async def async_saj(): """Update all the SAJ sensors.""" @@ -167,7 +170,7 @@ class SAJsensor(Entity): """Representation of a SAJ sensor.""" def __init__(self, serialnumber, pysaj_sensor, inverter_name=None): - """Initialize the sensor.""" + """Initialize the SAJ sensor.""" self._sensor = pysaj_sensor self._inverter_name = inverter_name self._serialnumber = serialnumber diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index e84b4e4d5bf..4b463fa7c23 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -2,11 +2,12 @@ import asyncio from datetime import timedelta -from samsungctl import exceptions as samsung_exceptions, Remote as SamsungRemote +from samsungctl import Remote as SamsungRemote, exceptions as samsung_exceptions import voluptuous as vol import wakeonlan +from websocket import WebSocketException -from homeassistant.components.media_player import MediaPlayerDevice, DEVICE_CLASS_TV +from homeassistant.components.media_player import DEVICE_CLASS_TV, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, SUPPORT_NEXT_TRACK, @@ -183,8 +184,13 @@ class SamsungTVDevice(MediaPlayerDevice): try: self.get_remote().control(key) break - except (samsung_exceptions.ConnectionClosed, BrokenPipeError): + except ( + samsung_exceptions.ConnectionClosed, + BrokenPipeError, + WebSocketException, + ): # BrokenPipe can occur when the commands is sent to fast + # WebSocketException can occur when timed out self._remote = None self._state = STATE_ON except AttributeError: @@ -197,6 +203,7 @@ class SamsungTVDevice(MediaPlayerDevice): LOGGER.debug("Failed sending command %s", key, exc_info=True) return except OSError: + # Different reasons, e.g. hostname not resolveable self._state = STATE_OFF self._remote = None if self._power_off_in_progress(): diff --git a/homeassistant/components/satel_integra/__init__.py b/homeassistant/components/satel_integra/__init__.py index a657f6239d1..84bb3b570d8 100644 --- a/homeassistant/components/satel_integra/__init__.py +++ b/homeassistant/components/satel_integra/__init__.py @@ -2,9 +2,10 @@ import collections import logging +from satel_integra.satel_integra import AsyncSatel import voluptuous as vol -from homeassistant.const import EVENT_HOMEASSISTANT_STOP, CONF_HOST, CONF_PORT +from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.discovery import async_load_platform @@ -61,9 +62,7 @@ PARTITION_SCHEMA = vol.Schema( def is_alarm_code_necessary(value): """Check if alarm code must be configured.""" if value.get(CONF_SWITCHABLE_OUTPUTS) and CONF_DEVICE_CODE not in value: - raise vol.Invalid( - "You need to specify alarm " " code to use switchable_outputs" - ) + raise vol.Invalid("You need to specify alarm code to use switchable_outputs") return value @@ -102,8 +101,6 @@ async def async_setup(hass, config): port = conf.get(CONF_PORT) partitions = conf.get(CONF_DEVICE_PARTITIONS) - from satel_integra.satel_integra import AsyncSatel - monitored_outputs = collections.OrderedDict( list(outputs.items()) + list(switchable_outputs.items()) ) diff --git a/homeassistant/components/satel_integra/alarm_control_panel.py b/homeassistant/components/satel_integra/alarm_control_panel.py index 2f0e165f21f..d4294788fdd 100644 --- a/homeassistant/components/satel_integra/alarm_control_panel.py +++ b/homeassistant/components/satel_integra/alarm_control_panel.py @@ -1,9 +1,15 @@ """Support for Satel Integra alarm, using ETHM module.""" import asyncio -import logging from collections import OrderedDict +import logging + +from satel_integra.satel_integra import AlarmState import homeassistant.components.alarm_control_panel as alarm +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, @@ -17,8 +23,8 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import ( CONF_ARM_HOME_MODE, CONF_DEVICE_PARTITIONS, - DATA_SATEL, CONF_ZONE_NAME, + DATA_SATEL, SIGNAL_PANEL_MESSAGE, ) @@ -78,7 +84,6 @@ class SatelIntegraAlarmPanel(alarm.AlarmControlPanel): def _read_alarm_state(self): """Read current status of the alarm and translate it into HA status.""" - from satel_integra.satel_integra import AlarmState # Default - disarmed: hass_alarm_status = STATE_ALARM_DISARMED @@ -131,6 +136,11 @@ class SatelIntegraAlarmPanel(alarm.AlarmControlPanel): """Return the state of the device.""" return self._state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY + async def async_alarm_disarm(self, code=None): """Send disarm command.""" if not code: diff --git a/homeassistant/components/satel_integra/binary_sensor.py b/homeassistant/components/satel_integra/binary_sensor.py index 1e4877229b9..cbe760c06bf 100644 --- a/homeassistant/components/satel_integra/binary_sensor.py +++ b/homeassistant/components/satel_integra/binary_sensor.py @@ -10,9 +10,9 @@ from . import ( CONF_ZONE_NAME, CONF_ZONE_TYPE, CONF_ZONES, + DATA_SATEL, SIGNAL_OUTPUTS_UPDATED, SIGNAL_ZONES_UPDATED, - DATA_SATEL, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/satel_integra/switch.py b/homeassistant/components/satel_integra/switch.py index 5b5e4f3095b..9233b3d152d 100644 --- a/homeassistant/components/satel_integra/switch.py +++ b/homeassistant/components/satel_integra/switch.py @@ -9,8 +9,8 @@ from . import ( CONF_DEVICE_CODE, CONF_SWITCHABLE_OUTPUTS, CONF_ZONE_NAME, - SIGNAL_OUTPUTS_UPDATED, DATA_SATEL, + SIGNAL_OUTPUTS_UPDATED, ) _LOGGER = logging.getLogger(__name__) @@ -69,14 +69,14 @@ class SatelIntegraSwitch(SwitchDevice): async def async_turn_on(self, **kwargs): """Turn the device on.""" - _LOGGER.debug("Switch: %s status: %s," " turning on", self._name, self._state) + _LOGGER.debug("Switch: %s status: %s, turning on", self._name, self._state) await self._satel.set_output(self._code, self._device_number, True) self.async_schedule_update_ha_state() async def async_turn_off(self, **kwargs): """Turn the device off.""" _LOGGER.debug( - "Switch name: %s status: %s," " turning off", self._name, self._state + "Switch name: %s status: %s, turning off", self._name, self._state ) await self._satel.set_output(self._code, self._device_number, False) self.async_schedule_update_ha_state() diff --git a/homeassistant/components/scene/__init__.py b/homeassistant/components/scene/__init__.py index 63a64f34fe9..75ec2bfd875 100644 --- a/homeassistant/components/scene/__init__.py +++ b/homeassistant/components/scene/__init__.py @@ -4,12 +4,11 @@ import logging import voluptuous as vol -from homeassistant.core import DOMAIN as HA_DOMAIN from homeassistant.const import CONF_PLATFORM, SERVICE_TURN_ON +from homeassistant.core import DOMAIN as HA_DOMAIN from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent - # mypy: allow-untyped-defs, no-check-untyped-defs DOMAIN = "scene" diff --git a/homeassistant/components/scene/services.yaml b/homeassistant/components/scene/services.yaml index 9cf1b9010a8..0c261ed60b5 100644 --- a/homeassistant/components/scene/services.yaml +++ b/homeassistant/components/scene/services.yaml @@ -34,3 +34,8 @@ create: light.ceiling: state: "on" brightness: 200 + snapshot_entities: + description: The entities of which a snapshot is to be taken + example: + - light.ceiling + - light.kitchen diff --git a/homeassistant/components/scrape/manifest.json b/homeassistant/components/scrape/manifest.json index 5fdcca372b9..6c5bf608999 100644 --- a/homeassistant/components/scrape/manifest.json +++ b/homeassistant/components/scrape/manifest.json @@ -3,7 +3,7 @@ "name": "Scrape", "documentation": "https://www.home-assistant.io/integrations/scrape", "requirements": [ - "beautifulsoup4==4.8.1" + "beautifulsoup4==4.8.2" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py index 0bfb7351c88..13d99a0cb8f 100644 --- a/homeassistant/components/scrape/sensor.py +++ b/homeassistant/components/scrape/sensor.py @@ -2,27 +2,27 @@ import logging from bs4 import BeautifulSoup -import voluptuous as vol from requests.auth import HTTPBasicAuth, HTTPDigestAuth +import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.components.rest.sensor import RestData +from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( + CONF_AUTHENTICATION, + CONF_HEADERS, CONF_NAME, + CONF_PASSWORD, CONF_RESOURCE, CONF_UNIT_OF_MEASUREMENT, + CONF_USERNAME, CONF_VALUE_TEMPLATE, CONF_VERIFY_SSL, - CONF_USERNAME, - CONF_HEADERS, - CONF_PASSWORD, - CONF_AUTHENTICATION, HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION, ) -from homeassistant.helpers.entity import Entity from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index 5a3223a8508..a8d78beae95 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -6,23 +6,22 @@ import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, - SERVICE_TURN_OFF, - SERVICE_TURN_ON, - SERVICE_TOGGLE, - SERVICE_RELOAD, - STATE_ON, + ATTR_NAME, CONF_ALIAS, EVENT_SCRIPT_STARTED, - ATTR_NAME, + SERVICE_RELOAD, + SERVICE_TOGGLE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_ON, ) -from homeassistant.loader import bind_hass +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.config_validation import make_entity_service_schema from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA -from homeassistant.helpers.service import async_set_service_schema - from homeassistant.helpers.script import Script +from homeassistant.helpers.service import async_set_service_schema +from homeassistant.loader import bind_hass _LOGGER = logging.getLogger(__name__) @@ -60,7 +59,7 @@ CONFIG_SCHEMA = vol.Schema( ) SCRIPT_SERVICE_SCHEMA = vol.Schema(dict) -SCRIPT_TURN_ONOFF_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( +SCRIPT_TURN_ONOFF_SCHEMA = make_entity_service_schema( {vol.Optional(ATTR_VARIABLES): dict} ) RELOAD_SERVICE_SCHEMA = vol.Schema({}) @@ -207,7 +206,7 @@ class ScriptEntity(ToggleEntity): ) try: await self.script.async_run(kwargs.get(ATTR_VARIABLES), context) - except Exception as err: # pylint: disable=broad-except + except Exception as err: self.script.async_log_exception( _LOGGER, f"Error executing script {self.entity_id}", err ) diff --git a/homeassistant/components/scsgate/__init__.py b/homeassistant/components/scsgate/__init__.py index 739a2949d17..21e3608a51b 100644 --- a/homeassistant/components/scsgate/__init__.py +++ b/homeassistant/components/scsgate/__init__.py @@ -2,6 +2,10 @@ import logging from threading import Lock +from scsgate.connection import Connection +from scsgate.messages import ScenarioTriggeredMessage, StateMessage +from scsgate.reactor import Reactor +from scsgate.tasks import GetStatusTask import voluptuous as vol from homeassistant.const import CONF_DEVICE, CONF_NAME @@ -61,12 +65,8 @@ class SCSGate: self._device_being_registered = None self._device_being_registered_lock = Lock() - from scsgate.connection import Connection - connection = Connection(device=device, logger=self._logger) - from scsgate.reactor import Reactor - self._reactor = Reactor( connection=connection, logger=self._logger, @@ -75,7 +75,6 @@ class SCSGate: def handle_message(self, message): """Handle a messages seen on the bus.""" - from scsgate.messages import StateMessage, ScenarioTriggeredMessage self._logger.debug(f"Received message {message}") if not isinstance(message, StateMessage) and not isinstance( @@ -132,7 +131,6 @@ class SCSGate: def _activate_next_device(self): """Start the activation of the first device.""" - from scsgate.tasks import GetStatusTask with self._devices_to_register_lock: while self._devices_to_register: diff --git a/homeassistant/components/scsgate/cover.py b/homeassistant/components/scsgate/cover.py index 9aa19e3f668..9d034c146ee 100644 --- a/homeassistant/components/scsgate/cover.py +++ b/homeassistant/components/scsgate/cover.py @@ -1,10 +1,15 @@ """Support for SCSGate covers.""" import logging +from scsgate.tasks import ( + HaltRollerShutterTask, + LowerRollerShutterTask, + RaiseRollerShutterTask, +) import voluptuous as vol from homeassistant.components import scsgate -from homeassistant.components.cover import CoverDevice, PLATFORM_SCHEMA +from homeassistant.components.cover import PLATFORM_SCHEMA, CoverDevice from homeassistant.const import CONF_DEVICES, CONF_NAME import homeassistant.helpers.config_validation as cv @@ -69,20 +74,14 @@ class SCSGateCover(CoverDevice): def open_cover(self, **kwargs): """Move the cover.""" - from scsgate.tasks import RaiseRollerShutterTask - scsgate.SCSGATE.append_task(RaiseRollerShutterTask(target=self._scs_id)) def close_cover(self, **kwargs): """Move the cover down.""" - from scsgate.tasks import LowerRollerShutterTask - scsgate.SCSGATE.append_task(LowerRollerShutterTask(target=self._scs_id)) def stop_cover(self, **kwargs): """Stop the cover.""" - from scsgate.tasks import HaltRollerShutterTask - scsgate.SCSGATE.append_task(HaltRollerShutterTask(target=self._scs_id)) def process_event(self, message): diff --git a/homeassistant/components/scsgate/light.py b/homeassistant/components/scsgate/light.py index c183fc6a3f8..a04dfdc7e7a 100644 --- a/homeassistant/components/scsgate/light.py +++ b/homeassistant/components/scsgate/light.py @@ -1,10 +1,11 @@ """Support for SCSGate lights.""" import logging +from scsgate.tasks import ToggleStatusTask import voluptuous as vol from homeassistant.components import scsgate -from homeassistant.components.light import Light, PLATFORM_SCHEMA +from homeassistant.components.light import PLATFORM_SCHEMA, Light from homeassistant.const import ATTR_ENTITY_ID, ATTR_STATE, CONF_DEVICES, CONF_NAME import homeassistant.helpers.config_validation as cv @@ -70,7 +71,6 @@ class SCSGateLight(Light): def turn_on(self, **kwargs): """Turn the device on.""" - from scsgate.tasks import ToggleStatusTask scsgate.SCSGATE.append_task(ToggleStatusTask(target=self._scs_id, toggled=True)) @@ -79,7 +79,6 @@ class SCSGateLight(Light): def turn_off(self, **kwargs): """Turn the device off.""" - from scsgate.tasks import ToggleStatusTask scsgate.SCSGATE.append_task( ToggleStatusTask(target=self._scs_id, toggled=False) diff --git a/homeassistant/components/scsgate/switch.py b/homeassistant/components/scsgate/switch.py index 75e55e259a6..b2043d3a4c3 100644 --- a/homeassistant/components/scsgate/switch.py +++ b/homeassistant/components/scsgate/switch.py @@ -1,11 +1,13 @@ """Support for SCSGate switches.""" import logging +from scsgate.messages import ScenarioTriggeredMessage, StateMessage +from scsgate.tasks import ToggleStatusTask import voluptuous as vol from homeassistant.components import scsgate -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA -from homeassistant.const import ATTR_ENTITY_ID, ATTR_STATE, CONF_NAME, CONF_DEVICES +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice +from homeassistant.const import ATTR_ENTITY_ID, ATTR_STATE, CONF_DEVICES, CONF_NAME import homeassistant.helpers.config_validation as cv ATTR_SCENARIO_ID = "scenario_id" @@ -105,7 +107,6 @@ class SCSGateSwitch(SwitchDevice): def turn_on(self, **kwargs): """Turn the device on.""" - from scsgate.tasks import ToggleStatusTask scsgate.SCSGATE.append_task(ToggleStatusTask(target=self._scs_id, toggled=True)) @@ -114,7 +115,6 @@ class SCSGateSwitch(SwitchDevice): def turn_off(self, **kwargs): """Turn the device off.""" - from scsgate.tasks import ToggleStatusTask scsgate.SCSGATE.append_task( ToggleStatusTask(target=self._scs_id, toggled=False) @@ -172,7 +172,6 @@ class SCSGateScenarioSwitch: def process_event(self, message): """Handle a SCSGate message related with this switch.""" - from scsgate.messages import StateMessage, ScenarioTriggeredMessage if isinstance(message, StateMessage): scenario_id = message.bytes[4] diff --git a/homeassistant/components/season/sensor.py b/homeassistant/components/season/sensor.py index e5f756f0c75..8c237c1da19 100644 --- a/homeassistant/components/season/sensor.py +++ b/homeassistant/components/season/sensor.py @@ -7,12 +7,15 @@ import voluptuous as vol from homeassistant import util from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_TYPE +from homeassistant.const import CONF_NAME, CONF_TYPE +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) +DEFAULT_NAME = "Season" + EQUATOR = "equator" NORTHERN = "northern" @@ -44,7 +47,10 @@ SEASON_ICONS = { PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Optional(CONF_TYPE, default=TYPE_ASTRONOMICAL): vol.In(VALID_TYPES)} + { + vol.Optional(CONF_TYPE, default=TYPE_ASTRONOMICAL): vol.In(VALID_TYPES), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + } ) @@ -56,6 +62,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): latitude = util.convert(hass.config.latitude, float) _type = config.get(CONF_TYPE) + name = config.get(CONF_NAME) if latitude < 0: hemisphere = SOUTHERN @@ -65,7 +72,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): hemisphere = EQUATOR _LOGGER.debug(_type) - add_entities([Season(hass, hemisphere, _type)]) + add_entities([Season(hass, hemisphere, _type, name)]) return True @@ -105,9 +112,10 @@ def get_season(date, hemisphere, season_tracking_type): class Season(Entity): """Representation of the current season.""" - def __init__(self, hass, hemisphere, season_tracking_type): + def __init__(self, hass, hemisphere, season_tracking_type, name): """Initialize the season.""" self.hass = hass + self._name = name self.hemisphere = hemisphere self.datetime = dt_util.utcnow().replace(tzinfo=None) self.type = season_tracking_type @@ -116,7 +124,7 @@ class Season(Entity): @property def name(self): """Return the name.""" - return "Season" + return self._name @property def state(self): diff --git a/homeassistant/components/sendgrid/notify.py b/homeassistant/components/sendgrid/notify.py index f16758a5355..6dbf4d5c2b7 100644 --- a/homeassistant/components/sendgrid/notify.py +++ b/homeassistant/components/sendgrid/notify.py @@ -1,17 +1,8 @@ """SendGrid notification service.""" import logging -import voluptuous as vol - from sendgrid import SendGridAPIClient - -from homeassistant.const import ( - CONF_API_KEY, - CONF_RECIPIENT, - CONF_SENDER, - CONTENT_TYPE_TEXT_PLAIN, -) -import homeassistant.helpers.config_validation as cv +import voluptuous as vol from homeassistant.components.notify import ( ATTR_TITLE, @@ -19,6 +10,13 @@ from homeassistant.components.notify import ( PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.const import ( + CONF_API_KEY, + CONF_RECIPIENT, + CONF_SENDER, + CONTENT_TYPE_TEXT_PLAIN, +) +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sense/__init__.py b/homeassistant/components/sense/__init__.py index f905c369d72..ce0d3bce5dc 100644 --- a/homeassistant/components/sense/__init__.py +++ b/homeassistant/components/sense/__init__.py @@ -1,7 +1,12 @@ """Support for monitoring a Sense energy sensor.""" -import logging from datetime import timedelta +import logging +from sense_energy import ( + ASyncSenseable, + SenseAPITimeoutException, + SenseAuthenticationException, +) import voluptuous as vol from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_TIMEOUT @@ -36,11 +41,6 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass, config): """Set up the Sense sensor.""" - from sense_energy import ( - ASyncSenseable, - SenseAuthenticationException, - SenseAPITimeoutException, - ) username = config[DOMAIN][CONF_EMAIL] password = config[DOMAIN][CONF_PASSWORD] diff --git a/homeassistant/components/sense/binary_sensor.py b/homeassistant/components/sense/binary_sensor.py index ffc3ecb3cab..81f1b64c864 100644 --- a/homeassistant/components/sense/binary_sensor.py +++ b/homeassistant/components/sense/binary_sensor.py @@ -2,8 +2,8 @@ import logging from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import SENSE_DATA, SENSE_DEVICE_UPDATE diff --git a/homeassistant/components/sense/sensor.py b/homeassistant/components/sense/sensor.py index 36474620b03..d177a480ddf 100644 --- a/homeassistant/components/sense/sensor.py +++ b/homeassistant/components/sense/sensor.py @@ -2,6 +2,8 @@ from datetime import timedelta import logging +from sense_energy import SenseAPITimeoutException + from homeassistant.const import ENERGY_KILO_WATT_HOUR, POWER_WATT from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -114,7 +116,6 @@ class Sense(Entity): async def async_update(self): """Get the latest data, update state.""" - from sense_energy import SenseAPITimeoutException try: await self.update_sensor() diff --git a/homeassistant/components/sensehat/light.py b/homeassistant/components/sensehat/light.py index dcfc9b925e5..462c4245cd4 100644 --- a/homeassistant/components/sensehat/light.py +++ b/homeassistant/components/sensehat/light.py @@ -1,18 +1,19 @@ """Support for Sense Hat LEDs.""" import logging +from sense_hat import SenseHat import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.light import ( ATTR_BRIGHTNESS, - SUPPORT_BRIGHTNESS, ATTR_HS_COLOR, + PLATFORM_SCHEMA, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR, Light, - PLATFORM_SCHEMA, ) from homeassistant.const import CONF_NAME +import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) @@ -28,7 +29,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Sense Hat Light platform.""" - from sense_hat import SenseHat sensehat = SenseHat() diff --git a/homeassistant/components/sensehat/sensor.py b/homeassistant/components/sensehat/sensor.py index 7a7af09b4eb..980c23f8555 100644 --- a/homeassistant/components/sensehat/sensor.py +++ b/homeassistant/components/sensehat/sensor.py @@ -1,12 +1,13 @@ """Support for Sense HAT sensors.""" -import os -import logging from datetime import timedelta +import logging +import os +from sense_hat import SenseHat import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import TEMP_CELSIUS, CONF_DISPLAY_OPTIONS, CONF_NAME +from homeassistant.const import CONF_DISPLAY_OPTIONS, CONF_NAME, TEMP_CELSIUS import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -117,7 +118,6 @@ class SenseHatData: @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data from Sense HAT.""" - from sense_hat import SenseHat sense = SenseHat() temp_from_h = sense.get_temperature_from_humidity() diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py index a14bdb49133..2431b223f09 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -5,16 +5,16 @@ import logging import aiohttp import async_timeout -import voluptuous as vol import pysensibo +import voluptuous as vol from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice from homeassistant.components.climate.const import ( - HVAC_MODE_HEAT_COOL, HVAC_MODE_COOL, HVAC_MODE_DRY, HVAC_MODE_FAN_ONLY, HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, SUPPORT_FAN_MODE, SUPPORT_SWING_MODE, diff --git a/homeassistant/components/sensor/.translations/da.json b/homeassistant/components/sensor/.translations/da.json index df9b9935dc1..3febed8ac09 100644 --- a/homeassistant/components/sensor/.translations/da.json +++ b/homeassistant/components/sensor/.translations/da.json @@ -1,26 +1,26 @@ { "device_automation": { "condition_type": { - "is_battery_level": "{entity_name} batteriniveau", - "is_humidity": "{entity_name} fugtighed", - "is_illuminance": "{entity_name} belysningsstyrke", - "is_power": "{entity_name} str\u00f8m", - "is_pressure": "{entity_name} tryk", - "is_signal_strength": "{entity_name} signalstyrke", - "is_temperature": "{entity_name} temperatur", - "is_timestamp": "{entity_name} tidsstempel", - "is_value": "{entity_name} v\u00e6rdi" + "is_battery_level": "Aktuelt {entity_name}-batteriniveau", + "is_humidity": "Aktuel {entity_name}-luftfugtighed", + "is_illuminance": "Aktuel {entity_name}-lysstyrke", + "is_power": "Aktuel {entity_name}-str\u00f8m", + "is_pressure": "Aktuelt {entity_name}-lufttryk", + "is_signal_strength": "Aktuel {entity_name}-signalstyrke", + "is_temperature": "Aktuel {entity_name}-temperatur", + "is_timestamp": "Aktuel {entity_name}-tidsstempel", + "is_value": "Aktuel {entity_name}-v\u00e6rdi" }, "trigger_type": { - "battery_level": "{entity_name} batteriniveau", - "humidity": "{entity_name} fugtighed", - "illuminance": "{entity_name} belysningsstyrke", - "power": "{entity_name} str\u00f8m", - "pressure": "{entity_name} tryk", - "signal_strength": "{entity_name} signalstyrke", - "temperature": "{entity_name} temperatur", - "timestamp": "{entity_name} tidsstempel", - "value": "{entity_name} v\u00e6rdi" + "battery_level": "{entity_name} batteriniveau \u00e6ndres", + "humidity": "{entity_name} luftfugtighed \u00e6ndres", + "illuminance": "{entity_name} lysstyrke \u00e6ndres", + "power": "{entity_name} str\u00f8m \u00e6ndres", + "pressure": "{entity_name} lufttryk \u00e6ndres", + "signal_strength": "{entity_name} signalstyrke \u00e6ndres", + "temperature": "{entity_name} temperatur \u00e6ndres", + "timestamp": "{entity_name} tidsstempel \u00e6ndres", + "value": "{entity_name} v\u00e6rdi \u00e6ndres" } } } \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/es.json b/homeassistant/components/sensor/.translations/es.json index c5641d38cc0..7b8ef36efe1 100644 --- a/homeassistant/components/sensor/.translations/es.json +++ b/homeassistant/components/sensor/.translations/es.json @@ -1,22 +1,22 @@ { "device_automation": { "condition_type": { - "is_battery_level": "{entity_name} nivel de bater\u00eda", - "is_humidity": "{entity_name} humedad", - "is_illuminance": "{entity_name} iluminancia", - "is_power": "{entity_name} alimentaci\u00f3n", - "is_pressure": "{entity_name} presi\u00f3n", - "is_signal_strength": "{entity_name} intensidad de la se\u00f1al", - "is_temperature": "{entity_name} temperatura", - "is_timestamp": "{entity_name} marca de tiempo", - "is_value": "{entity_name} valor" + "is_battery_level": "Nivel de bater\u00eda actual de {entity_name}", + "is_humidity": "Humedad actual de {entity_name}", + "is_illuminance": "Luminosidad actual de {entity_name}", + "is_power": "Potencia actual de {entity_name}", + "is_pressure": "Presi\u00f3n actual de {entity_name}", + "is_signal_strength": "Intensidad de la se\u00f1al actual de {entity_name}", + "is_temperature": "Temperatura actual de {entity_name}", + "is_timestamp": "Marca de tiempo actual de {entity_name}", + "is_value": "Valor actual de {entity_name}" }, "trigger_type": { - "battery_level": "{entity_name} nivel de bater\u00eda", - "humidity": "{entity_name} humedad", - "illuminance": "{entity_name} iluminancia", - "power": "{entity_name} alimentaci\u00f3n", - "pressure": "{entity_name} presi\u00f3n", + "battery_level": "Cambios de nivel de bater\u00eda de {entity_name}", + "humidity": "Cambios de humedad de {entity_name}", + "illuminance": "Cambios de luminosidad de {entity_name}", + "power": "Cambios de potencia de {entity_name}", + "pressure": "Cambios de presi\u00f3n de {entity_name}", "signal_strength": "cambios de la intensidad de se\u00f1al de {entity_name} ", "temperature": "{entity_name} cambios de temperatura", "timestamp": "{entity_name} cambios de fecha y hora", diff --git a/homeassistant/components/sensor/.translations/hu.json b/homeassistant/components/sensor/.translations/hu.json index 78ea3e5e89b..a83db67b3e1 100644 --- a/homeassistant/components/sensor/.translations/hu.json +++ b/homeassistant/components/sensor/.translations/hu.json @@ -1,26 +1,26 @@ { "device_automation": { "condition_type": { - "is_battery_level": "{entity_name} akku szint", - "is_humidity": "{entity_name} p\u00e1ratartalom", - "is_illuminance": "{entity_name} megvil\u00e1g\u00edt\u00e1s", - "is_power": "{entity_name} teljes\u00edtm\u00e9ny", - "is_pressure": "{entity_name} nyom\u00e1s", - "is_signal_strength": "{entity_name} jeler\u0151ss\u00e9g", - "is_temperature": "{entity_name} h\u0151m\u00e9rs\u00e9klet", - "is_timestamp": "{entity_name} id\u0151b\u00e9lyeg", - "is_value": "{entity_name} \u00e9rt\u00e9k" + "is_battery_level": "{entity_name} aktu\u00e1lis akku szintje", + "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", + "is_pressure": "{entity_name} aktu\u00e1lis nyom\u00e1sa", + "is_signal_strength": "{entity_name} aktu\u00e1lis jeler\u0151ss\u00e9ge", + "is_temperature": "{entity_name} aktu\u00e1lis h\u0151m\u00e9rs\u00e9klete", + "is_timestamp": "{entity_name} aktu\u00e1lis id\u0151b\u00e9lyege", + "is_value": "{entity_name} aktu\u00e1lis \u00e9rt\u00e9ke" }, "trigger_type": { - "battery_level": "{entity_name} akku szint", - "humidity": "{entity_name} p\u00e1ratartalom", - "illuminance": "{entity_name} megvil\u00e1g\u00edt\u00e1s", - "power": "{entity_name} teljes\u00edtm\u00e9ny", - "pressure": "{entity_name} nyom\u00e1s", - "signal_strength": "{entity_name} jeler\u0151ss\u00e9g", - "temperature": "{entity_name} h\u0151m\u00e9rs\u00e9klet", - "timestamp": "{entity_name} id\u0151b\u00e9lyeg", - "value": "{entity_name} \u00e9rt\u00e9k" + "battery_level": "{entity_name} akku szintje v\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", + "pressure": "{entity_name} nyom\u00e1sa v\u00e1ltozik", + "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" } } } \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/ko.json b/homeassistant/components/sensor/.translations/ko.json index 0e74f3f4f89..7716cc016c3 100644 --- a/homeassistant/components/sensor/.translations/ko.json +++ b/homeassistant/components/sensor/.translations/ko.json @@ -1,26 +1,26 @@ { "device_automation": { "condition_type": { - "is_battery_level": "\ud604\uc7ac {entity_name} \ubc30\ud130\ub9ac \uc794\ub7c9", - "is_humidity": "\ud604\uc7ac {entity_name} \uc2b5\ub3c4", - "is_illuminance": "\ud604\uc7ac {entity_name} \uc870\ub3c4", - "is_power": "\ud604\uc7ac {entity_name} \uc18c\ube44 \uc804\ub825", - "is_pressure": "\ud604\uc7ac {entity_name} \uc555\ub825", - "is_signal_strength": "\ud604\uc7ac {entity_name} \uc2e0\ud638 \uac15\ub3c4", - "is_temperature": "\ud604\uc7ac {entity_name} \uc628\ub3c4", - "is_timestamp": "\ud604\uc7ac {entity_name} \uc2dc\uac01", - "is_value": "\ud604\uc7ac {entity_name} \uac12" + "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" }, "trigger_type": { - "battery_level": "{entity_name} \ubc30\ud130\ub9ac \uc794\ub7c9 \ubcc0\ud654", - "humidity": "{entity_name} \uc2b5\ub3c4 \ubcc0\ud654", - "illuminance": "{entity_name} \uc870\ub3c4 \ubcc0\ud654", - "power": "{entity_name} \uc18c\ube44 \uc804\ub825 \ubcc0\ud654", - "pressure": "{entity_name} \uc555\ub825 \ubcc0\ud654", - "signal_strength": "{entity_name} \uc2e0\ud638 \uac15\ub3c4 \ubcc0\ud654", - "temperature": "{entity_name} \uc628\ub3c4 \ubcc0\ud654", - "timestamp": "{entity_name} \uc2dc\uac01 \ubcc0\ud654", - "value": "{entity_name} \uac12 \ubcc0\ud654" + "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" } } } \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/no.json b/homeassistant/components/sensor/.translations/no.json index 6709e4eb28c..1c0fc108510 100644 --- a/homeassistant/components/sensor/.translations/no.json +++ b/homeassistant/components/sensor/.translations/no.json @@ -1,26 +1,26 @@ { "device_automation": { "condition_type": { - "is_battery_level": "{entity_name} batteriniv\u00e5", - "is_humidity": "{entity_name} fuktighet", - "is_illuminance": "{entity_name} belysningsstyrke", - "is_power": "{entity_name} effekt", - "is_pressure": "{entity_name} trykk", - "is_signal_strength": "{entity_name} signalstyrke", - "is_temperature": "{entity_name} temperatur", - "is_timestamp": "{entity_name} tidsstempel", - "is_value": "{entity_name} verdi" + "is_battery_level": "Gjeldende {entity_name} batteriniv\u00e5", + "is_humidity": "Gjeldende {entity_name} fuktighet", + "is_illuminance": "Gjeldende {entity_name} belysningsstyrke", + "is_power": "Gjeldende {entity_name} str\u00f8m", + "is_pressure": "Gjeldende {entity_name} trykk", + "is_signal_strength": "Gjeldende {entity_name} signalstyrke", + "is_temperature": "Gjeldende {entity_name} temperatur", + "is_timestamp": "Gjeldende {entity_name} tidsstempel", + "is_value": "Gjeldende {entity_name} verdi" }, "trigger_type": { - "battery_level": "{entity_name} batteriniv\u00e5", - "humidity": "{entity_name} fuktighet", - "illuminance": "{entity_name} belysningsstyrke", - "power": "{entity_name} str\u00f8m", - "pressure": "{entity_name} trykk", - "signal_strength": "{entity_name} signalstyrke", - "temperature": "{entity_name} temperatur", - "timestamp": "{entity_name} tidsstempel", - "value": "{entity_name} verdi" + "battery_level": "{entity_name} batteriniv\u00e5 endres", + "humidity": "{entity_name} fuktighets endringer", + "illuminance": "{entity_name} belysningsstyrke endringer", + "power": "{entity_name} str\u00f8m endringer", + "pressure": "{entity_name} trykk endringer", + "signal_strength": "{entity_name} signalstyrkeendringer", + "temperature": "{entity_name} temperaturendringer", + "timestamp": "{entity_name} tidsstempel endringer", + "value": "{entity_name} verdi endringer" } } } \ No newline at end of file diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 53e4b0ffcf7..83711a07759 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -21,7 +21,6 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 ) from homeassistant.helpers.entity_component import EntityComponent - # mypy: allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/device_condition.py b/homeassistant/components/sensor/device_condition.py index 259fb5dbab9..7417765f9f4 100644 --- a/homeassistant/components/sensor/device_condition.py +++ b/homeassistant/components/sensor/device_condition.py @@ -1,11 +1,11 @@ """Provides device conditions for sensors.""" from typing import Dict, List + import voluptuous as vol from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, ) -from homeassistant.core import HomeAssistant from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, @@ -22,16 +22,16 @@ from homeassistant.const import ( DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TIMESTAMP, ) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import condition, config_validation as cv from homeassistant.helpers.entity_registry import ( async_entries_for_device, async_get_registry, ) -from homeassistant.helpers import condition, config_validation as cv from homeassistant.helpers.typing import ConfigType from . import DOMAIN - # mypy: allow-untyped-defs, no-check-untyped-defs DEVICE_CLASS_NONE = "none" diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index 73e55340da9..1af8a5e4ab0 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -23,12 +23,11 @@ from homeassistant.const import ( DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TIMESTAMP, ) -from homeassistant.helpers.entity_registry import async_entries_for_device from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.entity_registry import async_entries_for_device from . import DOMAIN - # mypy: allow-untyped-defs, no-check-untyped-defs DEVICE_CLASS_NONE = "none" diff --git a/homeassistant/components/serial_pm/sensor.py b/homeassistant/components/serial_pm/sensor.py index 1d46b05d46e..75587e4eab7 100644 --- a/homeassistant/components/serial_pm/sensor.py +++ b/homeassistant/components/serial_pm/sensor.py @@ -1,6 +1,7 @@ """Support for particulate matter sensors connected to a serial port.""" import logging +from pmsensor import serial_pm as pm import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -24,8 +25,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the available PM sensors.""" - from pmsensor import serial_pm as pm - try: coll = pm.PMDataCollector( config.get(CONF_SERIAL_DEVICE), pm.SUPPORTED_SENSORS[config.get(CONF_BRAND)] diff --git a/homeassistant/components/seven_segments/manifest.json b/homeassistant/components/seven_segments/manifest.json index 0bf49d4b906..6e375da0322 100644 --- a/homeassistant/components/seven_segments/manifest.json +++ b/homeassistant/components/seven_segments/manifest.json @@ -2,7 +2,7 @@ "domain": "seven_segments", "name": "Seven segments", "documentation": "https://www.home-assistant.io/integrations/seven_segments", - "requirements": [], + "requirements": ["pillow==6.2.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/seventeentrack/sensor.py b/homeassistant/components/seventeentrack/sensor.py index 33abe2f1f86..167f4347c0c 100644 --- a/homeassistant/components/seventeentrack/sensor.py +++ b/homeassistant/components/seventeentrack/sensor.py @@ -1,7 +1,9 @@ """Support for package tracking sensors from 17track.net.""" -import logging from datetime import timedelta +import logging +from py17track import Client as SeventeenTrackClient +from py17track.errors import SeventeenTrackError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -61,12 +63,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Configure the platform and add the sensors.""" - from py17track import Client - from py17track.errors import SeventeenTrackError websession = aiohttp_client.async_get_clientsession(hass) - client = Client(websession) + client = SeventeenTrackClient(websession) try: login_result = await client.profile.login( @@ -290,7 +290,6 @@ class SeventeenTrackData: async def _async_update(self): """Get updated data from 17track.net.""" - from py17track.errors import SeventeenTrackError try: packages = await self._client.profile.packages( diff --git a/homeassistant/components/shell_command/__init__.py b/homeassistant/components/shell_command/__init__.py index 42057407814..89a1a20e8e4 100644 --- a/homeassistant/components/shell_command/__init__.py +++ b/homeassistant/components/shell_command/__init__.py @@ -5,8 +5,8 @@ import shlex import voluptuous as vol -from homeassistant.exceptions import TemplateError from homeassistant.core import ServiceCall +from homeassistant.exceptions import TemplateError from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.typing import ConfigType, HomeAssistantType diff --git a/homeassistant/components/shodan/manifest.json b/homeassistant/components/shodan/manifest.json index 36348a1975c..007f6ef1d99 100644 --- a/homeassistant/components/shodan/manifest.json +++ b/homeassistant/components/shodan/manifest.json @@ -2,11 +2,7 @@ "domain": "shodan", "name": "Shodan", "documentation": "https://www.home-assistant.io/integrations/shodan", - "requirements": [ - "shodan==1.19.1" - ], + "requirements": ["shodan==1.21.1"], "dependencies": [], - "codeowners": [ - "@fabaff" - ] -} \ No newline at end of file + "codeowners": ["@fabaff"] +} diff --git a/homeassistant/components/shopping_list/__init__.py b/homeassistant/components/shopping_list/__init__.py index a5e901b8c6e..856ea0784ba 100644 --- a/homeassistant/components/shopping_list/__init__.py +++ b/homeassistant/components/shopping_list/__init__.py @@ -5,14 +5,12 @@ import uuid import voluptuous as vol -from homeassistant.const import HTTP_NOT_FOUND, HTTP_BAD_REQUEST -from homeassistant.core import callback -from homeassistant.components import http +from homeassistant.components import http, websocket_api from homeassistant.components.http.data_validator import RequestDataValidator -from homeassistant.helpers import intent +from homeassistant.const import 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 homeassistant.components import websocket_api ATTR_NAME = "name" @@ -20,8 +18,6 @@ DOMAIN = "shopping_list" _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema({DOMAIN: {}}, extra=vol.ALLOW_EXTRA) EVENT = "shopping_list_updated" -INTENT_ADD_ITEM = "HassShoppingListAddItem" -INTENT_LAST_ITEMS = "HassShoppingListLastItems" ITEM_UPDATE_SCHEMA = vol.Schema({"complete": bool, ATTR_NAME: str}) PERSISTENCE = ".shopping_list.json" @@ -86,9 +82,6 @@ def async_setup(hass, config): data = hass.data[DOMAIN] = ShoppingData(hass) yield from data.async_load() - intent.async_register(hass, AddItemIntent()) - intent.async_register(hass, ListTopItemsIntent()) - hass.services.async_register( DOMAIN, SERVICE_ADD_ITEM, add_item_service, schema=SERVICE_ITEM_SCHEMA ) @@ -175,49 +168,6 @@ class ShoppingData: save_json(self.hass.config.path(PERSISTENCE), self.items) -class AddItemIntent(intent.IntentHandler): - """Handle AddItem intents.""" - - intent_type = INTENT_ADD_ITEM - slot_schema = {"item": cv.string} - - @asyncio.coroutine - def async_handle(self, intent_obj): - """Handle the intent.""" - slots = self.async_validate_slots(intent_obj.slots) - item = slots["item"]["value"] - intent_obj.hass.data[DOMAIN].async_add(item) - - response = intent_obj.create_response() - response.async_set_speech(f"I've added {item} to your shopping list") - intent_obj.hass.bus.async_fire(EVENT) - return response - - -class ListTopItemsIntent(intent.IntentHandler): - """Handle AddItem intents.""" - - intent_type = INTENT_LAST_ITEMS - slot_schema = {"item": cv.string} - - @asyncio.coroutine - def async_handle(self, intent_obj): - """Handle the intent.""" - items = intent_obj.hass.data[DOMAIN].items[-5:] - response = intent_obj.create_response() - - if not items: - response.async_set_speech("There are no items on your shopping list") - else: - response.async_set_speech( - "These are the top {} items on your shopping list: {}".format( - min(len(items), 5), - ", ".join(itm["name"] for itm in reversed(items)), - ) - ) - return response - - class ShoppingListView(http.HomeAssistantView): """View to retrieve shopping list content.""" diff --git a/homeassistant/components/shopping_list/intent.py b/homeassistant/components/shopping_list/intent.py new file mode 100644 index 00000000000..21ae7181e89 --- /dev/null +++ b/homeassistant/components/shopping_list/intent.py @@ -0,0 +1,55 @@ +"""Intents for the Shopping List integration.""" +from homeassistant.helpers import intent +import homeassistant.helpers.config_validation as cv + +from . import DOMAIN, EVENT + +INTENT_ADD_ITEM = "HassShoppingListAddItem" +INTENT_LAST_ITEMS = "HassShoppingListLastItems" + + +async def async_setup_intents(hass): + """Set up the Shopping List intents.""" + intent.async_register(hass, AddItemIntent()) + intent.async_register(hass, ListTopItemsIntent()) + + +class AddItemIntent(intent.IntentHandler): + """Handle AddItem intents.""" + + intent_type = INTENT_ADD_ITEM + slot_schema = {"item": cv.string} + + async def async_handle(self, intent_obj): + """Handle the intent.""" + slots = self.async_validate_slots(intent_obj.slots) + item = slots["item"]["value"] + intent_obj.hass.data[DOMAIN].async_add(item) + + response = intent_obj.create_response() + response.async_set_speech(f"I've added {item} to your shopping list") + intent_obj.hass.bus.async_fire(EVENT) + return response + + +class ListTopItemsIntent(intent.IntentHandler): + """Handle AddItem intents.""" + + intent_type = INTENT_LAST_ITEMS + slot_schema = {"item": cv.string} + + async def async_handle(self, intent_obj): + """Handle the intent.""" + items = intent_obj.hass.data[DOMAIN].items[-5:] + response = intent_obj.create_response() + + if not items: + response.async_set_speech("There are no items on your shopping list") + else: + response.async_set_speech( + "These are the top {} items on your shopping list: {}".format( + min(len(items), 5), + ", ".join(itm["name"] for itm in reversed(items)), + ) + ) + return response diff --git a/homeassistant/components/sht31/sensor.py b/homeassistant/components/sht31/sensor.py index 7f8b6ecdc52..8a520377896 100644 --- a/homeassistant/components/sht31/sensor.py +++ b/homeassistant/components/sht31/sensor.py @@ -4,17 +4,21 @@ from datetime import timedelta import logging import math +from Adafruit_SHT31 import SHT31 import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import TEMP_CELSIUS, CONF_NAME, CONF_MONITORED_CONDITIONS +from homeassistant.const import ( + CONF_MONITORED_CONDITIONS, + CONF_NAME, + PRECISION_TENTHS, + TEMP_CELSIUS, +) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.temperature import display_temp -from homeassistant.const import PRECISION_TENTHS from homeassistant.util import Throttle - _LOGGER = logging.getLogger(__name__) CONF_I2C_ADDRESS = "i2c_address" @@ -43,7 +47,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the sensor platform.""" - from Adafruit_SHT31 import SHT31 i2c_address = config.get(CONF_I2C_ADDRESS) sensor = SHT31(address=i2c_address) diff --git a/homeassistant/components/sigfox/sensor.py b/homeassistant/components/sigfox/sensor.py index b890880389c..27e2fe9b563 100644 --- a/homeassistant/components/sigfox/sensor.py +++ b/homeassistant/components/sigfox/sensor.py @@ -1,15 +1,15 @@ """Sensor for SigFox devices.""" -import logging import datetime import json +import logging from urllib.parse import urljoin import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv 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 _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/signal_messenger/__init__.py b/homeassistant/components/signal_messenger/__init__.py new file mode 100644 index 00000000000..045eb95f1b3 --- /dev/null +++ b/homeassistant/components/signal_messenger/__init__.py @@ -0,0 +1 @@ +"""The signalmessenger component.""" diff --git a/homeassistant/components/signal_messenger/manifest.json b/homeassistant/components/signal_messenger/manifest.json new file mode 100644 index 00000000000..b3494ce8bab --- /dev/null +++ b/homeassistant/components/signal_messenger/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "signal_messenger", + "name": "signal_messenger", + "documentation": "https://www.home-assistant.io/integrations/signal_messenger", + "dependencies": [], + "codeowners": ["@bbernhard"], + "requirements": [ + "pysignalclirestapi==0.1.4" + ] +} diff --git a/homeassistant/components/signal_messenger/notify.py b/homeassistant/components/signal_messenger/notify.py new file mode 100644 index 00000000000..8fbf9c70873 --- /dev/null +++ b/homeassistant/components/signal_messenger/notify.py @@ -0,0 +1,71 @@ +"""Signal Messenger for notify component.""" +import logging + +from pysignalclirestapi import SignalCliRestApi, SignalCliRestApiError +import voluptuous as vol + +from homeassistant.components.notify import ( + ATTR_DATA, + PLATFORM_SCHEMA, + BaseNotificationService, +) +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +CONF_SENDER_NR = "number" +CONF_RECP_NR = "recipients" +CONF_SIGNAL_CLI_REST_API = "url" +ATTR_FILENAME = "attachment" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_SENDER_NR): cv.string, + vol.Required(CONF_SIGNAL_CLI_REST_API): cv.string, + vol.Required(CONF_RECP_NR): vol.All(cv.ensure_list, [cv.string]), + } +) + + +def get_service(hass, config, discovery_info=None): + """Get the SignalMessenger notification service.""" + + sender_nr = config[CONF_SENDER_NR] + recp_nrs = config[CONF_RECP_NR] + signal_cli_rest_api_url = config[CONF_SIGNAL_CLI_REST_API] + + signal_cli_rest_api = SignalCliRestApi( + signal_cli_rest_api_url, sender_nr, api_version=1 + ) + + return SignalNotificationService(recp_nrs, signal_cli_rest_api) + + +class SignalNotificationService(BaseNotificationService): + """Implement the notification service for SignalMessenger.""" + + def __init__(self, recp_nrs, signal_cli_rest_api): + """Initialize the service.""" + + self._recp_nrs = recp_nrs + self._signal_cli_rest_api = signal_cli_rest_api + + def send_message(self, message="", **kwargs): + """Send a message to a one or more recipients. + + Additionally a file can be attached. + """ + + _LOGGER.debug("Sending signal message") + + data = kwargs.get(ATTR_DATA) + + filename = None + if data is not None and ATTR_FILENAME in data: + filename = data[ATTR_FILENAME] + + try: + self._signal_cli_rest_api.send_message(message, self._recp_nrs, filename) + except SignalCliRestApiError as ex: + _LOGGER.error("%s", ex) + raise ex diff --git a/homeassistant/components/simplepush/notify.py b/homeassistant/components/simplepush/notify.py index 0b3b09fe11b..63bcd31935e 100644 --- a/homeassistant/components/simplepush/notify.py +++ b/homeassistant/components/simplepush/notify.py @@ -1,17 +1,17 @@ """Simplepush notification service.""" import logging +from simplepush import send, send_encrypted import voluptuous as vol -from homeassistant.const import CONF_PASSWORD -import homeassistant.helpers.config_validation as cv - from homeassistant.components.notify import ( ATTR_TITLE, ATTR_TITLE_DEFAULT, PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.const import CONF_PASSWORD +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -48,7 +48,6 @@ class SimplePushNotificationService(BaseNotificationService): def send_message(self, message="", **kwargs): """Send a message to a Simplepush user.""" - from simplepush import send, send_encrypted title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) diff --git a/homeassistant/components/simplisafe/.translations/da.json b/homeassistant/components/simplisafe/.translations/da.json index 3ec3d7b456c..0d3970eeba5 100644 --- a/homeassistant/components/simplisafe/.translations/da.json +++ b/homeassistant/components/simplisafe/.translations/da.json @@ -9,7 +9,7 @@ "data": { "code": "Kode (til Home Assistant)", "password": "Adgangskode", - "username": "Email adresse" + "username": "Emailadresse" }, "title": "Udfyld dine oplysninger" } diff --git a/homeassistant/components/simplisafe/.translations/ru.json b/homeassistant/components/simplisafe/.translations/ru.json index 721ba69d67e..301eed6d1c1 100644 --- a/homeassistant/components/simplisafe/.translations/ru.json +++ b/homeassistant/components/simplisafe/.translations/ru.json @@ -2,7 +2,7 @@ "config": { "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_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435." + "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435." }, "step": { "user": { diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index d57e7b83fa4..63ac0ca973c 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -1,10 +1,11 @@ """Support for SimpliSafe alarm systems.""" import asyncio -import logging from datetime import timedelta +import logging from simplipy import API from simplipy.errors import InvalidCredentialsError, SimplipyError +from simplipy.system.v3 import LevelMap as V3Volume import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT @@ -14,6 +15,7 @@ from homeassistant.const import ( CONF_SCAN_INTERVAL, CONF_TOKEN, CONF_USERNAME, + STATE_HOME, ) from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryNotReady @@ -35,27 +37,57 @@ from .const import DATA_CLIENT, DEFAULT_SCAN_INTERVAL, DOMAIN, TOPIC_UPDATE _LOGGER = logging.getLogger(__name__) -ATTR_PIN_LABEL = "label" -ATTR_PIN_LABEL_OR_VALUE = "label_or_pin" -ATTR_PIN_VALUE = "pin" -ATTR_SYSTEM_ID = "system_id" - CONF_ACCOUNTS = "accounts" DATA_LISTENER = "listener" -SERVICE_REMOVE_PIN_SCHEMA = vol.Schema( +ATTR_ARMED_LIGHT_STATE = "armed_light_state" +ATTR_ARRIVAL_STATE = "arrival_state" +ATTR_PIN_LABEL = "label" +ATTR_PIN_LABEL_OR_VALUE = "label_or_pin" +ATTR_PIN_VALUE = "pin" +ATTR_SECONDS = "seconds" +ATTR_SYSTEM_ID = "system_id" +ATTR_TRANSITION = "transition" +ATTR_VOLUME = "volume" +ATTR_VOLUME_PROPERTY = "volume_property" + +STATE_AWAY = "away" +STATE_ENTRY = "entry" +STATE_EXIT = "exit" + +VOLUME_PROPERTY_ALARM = "alarm" +VOLUME_PROPERTY_CHIME = "chime" +VOLUME_PROPERTY_VOICE_PROMPT = "voice_prompt" + +SERVICE_BASE_SCHEMA = vol.Schema({vol.Required(ATTR_SYSTEM_ID): cv.positive_int}) + +SERVICE_REMOVE_PIN_SCHEMA = SERVICE_BASE_SCHEMA.extend( + {vol.Required(ATTR_PIN_LABEL_OR_VALUE): cv.string} +) + +SERVICE_SET_DELAY_SCHEMA = SERVICE_BASE_SCHEMA.extend( { - vol.Required(ATTR_SYSTEM_ID): cv.string, - vol.Required(ATTR_PIN_LABEL_OR_VALUE): cv.string, + vol.Required(ATTR_ARRIVAL_STATE): vol.In((STATE_AWAY, STATE_HOME)), + vol.Required(ATTR_TRANSITION): vol.In((STATE_ENTRY, STATE_EXIT)), + vol.Required(ATTR_SECONDS): cv.positive_int, } ) -SERVICE_SET_PIN_SCHEMA = vol.Schema( +SERVICE_SET_LIGHT_SCHEMA = SERVICE_BASE_SCHEMA.extend( + {vol.Required(ATTR_ARMED_LIGHT_STATE): cv.boolean} +) + +SERVICE_SET_PIN_SCHEMA = SERVICE_BASE_SCHEMA.extend( + {vol.Required(ATTR_PIN_LABEL): cv.string, vol.Required(ATTR_PIN_VALUE): cv.string} +) + +SERVICE_SET_VOLUME_SCHEMA = SERVICE_BASE_SCHEMA.extend( { - vol.Required(ATTR_SYSTEM_ID): cv.string, - vol.Required(ATTR_PIN_LABEL): cv.string, - vol.Required(ATTR_PIN_VALUE): cv.string, + vol.Required(ATTR_VOLUME_PROPERTY): vol.In( + (VOLUME_PROPERTY_ALARM, VOLUME_PROPERTY_CHIME, VOLUME_PROPERTY_VOICE_PROMPT) + ), + vol.Required(ATTR_VOLUME): cv.string, } ) @@ -150,7 +182,7 @@ async def async_setup_entry(hass, config_entry): _async_save_refresh_token(hass, config_entry, api.refresh_token) systems = await api.get_systems() - simplisafe = SimpliSafe(hass, config_entry, systems) + simplisafe = SimpliSafe(hass, api, systems, config_entry) await simplisafe.async_update() hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = simplisafe @@ -175,21 +207,122 @@ async def async_setup_entry(hass, config_entry): async_register_base_station(hass, system, config_entry.entry_id) ) + @callback + def verify_system_exists(coro): + """Log an error if a service call uses an invalid system ID.""" + + async def decorator(call): + """Decorate.""" + system_id = int(call.data[ATTR_SYSTEM_ID]) + if system_id not in systems: + _LOGGER.error("Unknown system ID in service call: %s", system_id) + return + await coro(call) + + return decorator + + @callback + def v3_only(coro): + """Log an error if the decorated coroutine is called with a v2 system.""" + + async def decorator(call): + """Decorate.""" + system = systems[int(call.data[ATTR_SYSTEM_ID])] + if system.version != 3: + _LOGGER.error("Service only available on V3 systems") + return + await coro(call) + + return decorator + + @verify_system_exists @_verify_domain_control async def remove_pin(call): """Remove a PIN.""" - system = systems[int(call.data[ATTR_SYSTEM_ID])] - await system.remove_pin(call.data[ATTR_PIN_LABEL_OR_VALUE]) + system = systems[call.data[ATTR_SYSTEM_ID]] + try: + await system.remove_pin(call.data[ATTR_PIN_LABEL_OR_VALUE]) + except SimplipyError as err: + _LOGGER.error("Error during service call: %s", err) + return + @verify_system_exists + @v3_only + @_verify_domain_control + async def set_alarm_duration(call): + """Set the duration of a running alarm.""" + system = systems[call.data[ATTR_SYSTEM_ID]] + try: + await system.set_alarm_duration(call.data[ATTR_SECONDS]) + except SimplipyError as err: + _LOGGER.error("Error during service call: %s", err) + return + + @verify_system_exists + @v3_only + @_verify_domain_control + async def set_delay(call): + """Set the delay duration for entry/exit, away/home (any combo).""" + system = systems[call.data[ATTR_SYSTEM_ID]] + coro = getattr( + system, + f"set_{call.data[ATTR_TRANSITION]}_delay_{call.data[ATTR_ARRIVAL_STATE]}", + ) + + try: + await coro(call.data[ATTR_SECONDS]) + except SimplipyError as err: + _LOGGER.error("Error during service call: %s", err) + return + + @verify_system_exists + @v3_only + @_verify_domain_control + async def set_armed_light(call): + """Turn the base station light on/off.""" + system = systems[call.data[ATTR_SYSTEM_ID]] + try: + await system.set_light(call.data[ATTR_ARMED_LIGHT_STATE]) + except SimplipyError as err: + _LOGGER.error("Error during service call: %s", err) + return + + @verify_system_exists @_verify_domain_control async def set_pin(call): """Set a PIN.""" - system = systems[int(call.data[ATTR_SYSTEM_ID])] - await system.set_pin(call.data[ATTR_PIN_LABEL], call.data[ATTR_PIN_VALUE]) + system = systems[call.data[ATTR_SYSTEM_ID]] + try: + await system.set_pin(call.data[ATTR_PIN_LABEL], call.data[ATTR_PIN_VALUE]) + except SimplipyError as err: + _LOGGER.error("Error during service call: %s", err) + return + + @verify_system_exists + @v3_only + @_verify_domain_control + async def set_volume_property(call): + """Set a volume parameter in an appropriate service call.""" + system = systems[call.data[ATTR_SYSTEM_ID]] + try: + volume = V3Volume[call.data[ATTR_VOLUME]] + except KeyError: + _LOGGER.error("Unknown volume string: %s", call.data[ATTR_VOLUME]) + return + except SimplipyError as err: + _LOGGER.error("Error during service call: %s", err) + return + else: + coro = getattr(system, f"set_{call.data[ATTR_VOLUME_PROPERTY]}_volume") + await coro(volume) for service, method, schema in [ ("remove_pin", remove_pin, SERVICE_REMOVE_PIN_SCHEMA), + ("set_alarm_duration", set_alarm_duration, SERVICE_SET_DELAY_SCHEMA), + ("set_delay", set_delay, SERVICE_SET_DELAY_SCHEMA), + ("set_armed_light", set_armed_light, SERVICE_SET_LIGHT_SCHEMA), ("set_pin", set_pin, SERVICE_SET_PIN_SCHEMA), + ("set_volume_property", set_volume_property, SERVICE_SET_VOLUME_SCHEMA), ]: hass.services.async_register(DOMAIN, service, method, schema=schema) @@ -215,8 +348,9 @@ async def async_unload_entry(hass, entry): class SimpliSafe: """Define a SimpliSafe API object.""" - def __init__(self, hass, config_entry, systems): + def __init__(self, hass, api, systems, config_entry): """Initialize.""" + self._api = api self._config_entry = config_entry self._hass = hass self.last_event_data = {} @@ -238,9 +372,9 @@ class SimpliSafe: self.last_event_data[system.system_id] = latest_event - if system.api.refresh_token_dirty: + if self._api.refresh_token_dirty: _async_save_refresh_token( - self._hass, self._config_entry, system.api.refresh_token + self._hass, self._config_entry, self._api.refresh_token ) async def async_update(self): diff --git a/homeassistant/components/simplisafe/alarm_control_panel.py b/homeassistant/components/simplisafe/alarm_control_panel.py index a63a077ed15..05dad43955c 100644 --- a/homeassistant/components/simplisafe/alarm_control_panel.py +++ b/homeassistant/components/simplisafe/alarm_control_panel.py @@ -10,6 +10,10 @@ from homeassistant.components.alarm_control_panel import ( FORMAT_TEXT, AlarmControlPanel, ) +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, +) from homeassistant.const import ( CONF_CODE, STATE_ALARM_ARMED_AWAY, @@ -24,14 +28,23 @@ from .const import DATA_CLIENT, DOMAIN _LOGGER = logging.getLogger(__name__) ATTR_ALARM_ACTIVE = "alarm_active" +ATTR_ALARM_DURATION = "alarm_duration" +ATTR_ALARM_VOLUME = "alarm_volume" ATTR_BATTERY_BACKUP_POWER_LEVEL = "battery_backup_power_level" +ATTR_CHIME_VOLUME = "chime_volume" +ATTR_ENTRY_DELAY_AWAY = "entry_delay_away" +ATTR_ENTRY_DELAY_HOME = "entry_delay_home" +ATTR_EXIT_DELAY_AWAY = "exit_delay_away" +ATTR_EXIT_DELAY_HOME = "exit_delay_home" ATTR_GSM_STRENGTH = "gsm_strength" ATTR_LAST_EVENT_INFO = "last_event_info" ATTR_LAST_EVENT_SENSOR_NAME = "last_event_sensor_name" ATTR_LAST_EVENT_SENSOR_TYPE = "last_event_sensor_type" ATTR_LAST_EVENT_TIMESTAMP = "last_event_timestamp" ATTR_LAST_EVENT_TYPE = "last_event_type" +ATTR_LIGHT = "light" ATTR_RF_JAMMING = "rf_jamming" +ATTR_VOICE_PROMPT_VOLUME = "voice_prompt_volume" ATTR_WALL_POWER_LEVEL = "wall_power_level" ATTR_WIFI_STRENGTH = "wifi_strength" @@ -64,16 +77,26 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanel): self._simplisafe = simplisafe self._state = None - # Some properties only exist for V2 or V3 systems: - for prop in ( - ATTR_BATTERY_BACKUP_POWER_LEVEL, - ATTR_GSM_STRENGTH, - ATTR_RF_JAMMING, - ATTR_WALL_POWER_LEVEL, - ATTR_WIFI_STRENGTH, - ): - if hasattr(system, prop): - self._attrs[prop] = getattr(system, prop) + self._attrs.update({ATTR_ALARM_ACTIVE: self._system.alarm_going_off}) + if self._system.version == 3: + self._attrs.update( + { + ATTR_ALARM_DURATION: self._system.alarm_duration, + ATTR_ALARM_VOLUME: self._system.alarm_volume.name, + ATTR_BATTERY_BACKUP_POWER_LEVEL: self._system.battery_backup_power_level, + ATTR_CHIME_VOLUME: self._system.chime_volume.name, + ATTR_ENTRY_DELAY_AWAY: self._system.entry_delay_away, + ATTR_ENTRY_DELAY_HOME: self._system.entry_delay_home, + ATTR_EXIT_DELAY_AWAY: self._system.exit_delay_away, + ATTR_EXIT_DELAY_HOME: self._system.exit_delay_home, + ATTR_GSM_STRENGTH: self._system.gsm_strength, + ATTR_LIGHT: self._system.light, + ATTR_RF_JAMMING: self._system.rf_jamming, + ATTR_VOICE_PROMPT_VOLUME: self._system.voice_prompt_volume.name, + ATTR_WALL_POWER_LEVEL: self._system.wall_power_level, + ATTR_WIFI_STRENGTH: self._system.wifi_strength, + } + ) @property def changed_by(self): @@ -94,6 +117,11 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanel): """Return the state of the entity.""" return self._state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY + def _validate_code(self, code, state): """Validate given code.""" check = self._code is None or code == self._code @@ -149,12 +177,23 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanel): self._state = None last_event = self._simplisafe.last_event_data[self._system.system_id] + + try: + last_event_sensor_type = EntityTypes(last_event["sensorType"]).name + except ValueError: + _LOGGER.warning( + 'Encountered unknown entity type: %s ("%s"). Please report it at' + "https://github.com/home-assistant/home-assistant/issues.", + last_event["sensorType"], + last_event["sensorName"], + ) + last_event_sensor_type = None + self._attrs.update( { - ATTR_ALARM_ACTIVE: self._system.alarm_going_off, ATTR_LAST_EVENT_INFO: last_event["info"], ATTR_LAST_EVENT_SENSOR_NAME: last_event["sensorName"], - ATTR_LAST_EVENT_SENSOR_TYPE: EntityTypes(last_event["sensorType"]).name, + ATTR_LAST_EVENT_SENSOR_TYPE: last_event_sensor_type, ATTR_LAST_EVENT_TIMESTAMP: utc_from_timestamp( last_event["eventTimestamp"] ), diff --git a/homeassistant/components/simplisafe/config_flow.py b/homeassistant/components/simplisafe/config_flow.py index 0e3af0ae03c..6e1082948d3 100644 --- a/homeassistant/components/simplisafe/config_flow.py +++ b/homeassistant/components/simplisafe/config_flow.py @@ -1,10 +1,11 @@ """Config flow to configure the SimpliSafe component.""" from collections import OrderedDict +from simplipy import API +from simplipy.errors import SimplipyError import voluptuous as vol from homeassistant import config_entries -from homeassistant.core import callback from homeassistant.const import ( CONF_CODE, CONF_PASSWORD, @@ -12,6 +13,7 @@ from homeassistant.const import ( CONF_TOKEN, CONF_USERNAME, ) +from homeassistant.core import callback from homeassistant.helpers import aiohttp_client from .const import DEFAULT_SCAN_INTERVAL, DOMAIN @@ -53,8 +55,6 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow): async def async_step_user(self, user_input=None): """Handle the start of the config flow.""" - from simplipy import API - from simplipy.errors import SimplipyError if not user_input: return await self._show_form() diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index 61a16f8aa44..2df49bb5209 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/simplisafe", "requirements": [ - "simplisafe-python==5.2.0" + "simplisafe-python==5.3.6" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/simplisafe/services.yaml b/homeassistant/components/simplisafe/services.yaml index 52e66a435c6..d8a4973b49e 100644 --- a/homeassistant/components/simplisafe/services.yaml +++ b/homeassistant/components/simplisafe/services.yaml @@ -10,11 +10,46 @@ remove_pin: label_or_pin: description: The label/value to remove. example: Test PIN +set_alarm_duration: + description: "Set the duration (in seconds) of an active alarm" + fields: + system_id: + description: The SimpliSafe system ID to affect + example: 123987 + seconds: + description: The number of seconds to sound the alarm + example: 120 +set_delay: + description: > + Set a duration for how long the base station should delay when transitioning + between states + fields: + system_id: + description: The SimpliSafe system ID to affect + example: 123987 + arrival_state: + description: The target "arrival" state (away, home) + example: away + transition: + description: The system state transition to affect (entry, exit) + example: exit + seconds: + description: "The number of seconds to delay" + example: 120 +set_light: + description: "Turn the base station light on/off" + fields: + system_id: + description: The SimpliSafe system ID to affect + example: 123987 + armed_light_state: + description: "True for on, False for off" + example: "True" set_pin: description: Set/update a PIN fields: system_id: - description: The SimpliSafe system ID to affect. + description: The SimpliSafe system ID to affect example: 123987 label: description: The label of the PIN @@ -22,3 +57,15 @@ set_pin: pin: description: The value of the PIN example: 1256 +set_volume_property: + description: Set a level for one of the base station's various volumes + fields: + system_id: + description: The SimpliSafe system ID to affect + example: 123987 + volume_property: + description: The volume property to set (alarm, chime, voice_prompt) + example: voice_prompt + volume: + description: "A volume (off, low, medium, high)" + example: low diff --git a/homeassistant/components/simulated/sensor.py b/homeassistant/components/simulated/sensor.py index f6ed54e5191..d05448a82c7 100644 --- a/homeassistant/components/simulated/sensor.py +++ b/homeassistant/components/simulated/sensor.py @@ -1,8 +1,8 @@ """Adds a simulated sensor.""" +from datetime import datetime import logging import math from random import Random -from datetime import datetime import voluptuous as vol diff --git a/homeassistant/components/sinch/notify.py b/homeassistant/components/sinch/notify.py index 173873c0a6c..c0092f013c4 100644 --- a/homeassistant/components/sinch/notify.py +++ b/homeassistant/components/sinch/notify.py @@ -1,25 +1,25 @@ """Support for Sinch notifications.""" import logging -import voluptuous as vol from clx.xms.api import MtBatchTextSmsResult from clx.xms.client import Client from clx.xms.exceptions import ( ErrorResponseException, - UnexpectedResponseException, - UnauthorizedException, NotFoundException, + UnauthorizedException, + UnexpectedResponseException, ) +import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.notify import ( - ATTR_MESSAGE, ATTR_DATA, + ATTR_MESSAGE, ATTR_TARGET, PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.const import CONF_API_KEY, CONF_SENDER +import homeassistant.helpers.config_validation as cv DOMAIN = "sinch" @@ -61,7 +61,7 @@ class SinchNotificationService(BaseNotificationService): def send_message(self, message="", **kwargs): """Send a message to a user.""" targets = kwargs.get(ATTR_TARGET, self.default_recipients) - data = kwargs.get(ATTR_DATA, {}) + data = kwargs.get(ATTR_DATA) or {} clx_args = {ATTR_MESSAGE: message, ATTR_SENDER: self.sender} diff --git a/homeassistant/components/sisyphus/__init__.py b/homeassistant/components/sisyphus/__init__.py index 771641c9b1d..5ad59da5dee 100644 --- a/homeassistant/components/sisyphus/__init__.py +++ b/homeassistant/components/sisyphus/__init__.py @@ -2,6 +2,7 @@ import asyncio import logging +from sisyphus_control import Table import voluptuous as vol from homeassistant.const import CONF_HOST, CONF_NAME, EVENT_HOMEASSISTANT_STOP @@ -29,7 +30,6 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass, config): """Set up the sisyphus component.""" - from sisyphus_control import Table class SocketIONoiseFilter(logging.Filter): """Filters out excessively verbose logs from SocketIO.""" @@ -105,7 +105,6 @@ class TableHolder: return await self._table_task async def _connect_table(self): - from sisyphus_control import Table self._table = await Table.connect(self._host, self._session) if self._name is None: diff --git a/homeassistant/components/sisyphus/media_player.py b/homeassistant/components/sisyphus/media_player.py index e06b84b4ac5..e708504ff7e 100644 --- a/homeassistant/components/sisyphus/media_player.py +++ b/homeassistant/components/sisyphus/media_player.py @@ -2,6 +2,7 @@ import logging import aiohttp +from sisyphus_control import Track from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( @@ -43,7 +44,6 @@ SUPPORTED_FEATURES = ( ) -# pylint: disable=unused-argument async def async_setup_platform(hass, config, add_entities, discovery_info=None): """Set up a media player entity for a Sisyphus table.""" host = discovery_info[CONF_HOST] @@ -141,7 +141,6 @@ class SisyphusPlayer(MediaPlayerDevice): @property def media_image_url(self): """Return the URL for a thumbnail image of the current track.""" - from sisyphus_control import Track if self._table.active_track: return self._table.active_track.get_thumbnail_url(Track.ThumbnailSize.LARGE) diff --git a/homeassistant/components/sky_hub/device_tracker.py b/homeassistant/components/sky_hub/device_tracker.py index 109c410c16d..f7760a59eed 100644 --- a/homeassistant/components/sky_hub/device_tracker.py +++ b/homeassistant/components/sky_hub/device_tracker.py @@ -5,13 +5,13 @@ import re import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv 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__) _MAC_REGEX = re.compile(r"(([0-9A-Fa-f]{1,2}\:){5}[0-9A-Fa-f]{1,2})") diff --git a/homeassistant/components/skybell/__init__.py b/homeassistant/components/skybell/__init__.py index fd01b6d22c9..a4e4263d360 100644 --- a/homeassistant/components/skybell/__init__.py +++ b/homeassistant/components/skybell/__init__.py @@ -1,10 +1,11 @@ """Support for the Skybell HD Doorbell.""" import logging -from requests.exceptions import HTTPError, ConnectTimeout +from requests.exceptions import ConnectTimeout, HTTPError +from skybellpy import Skybell import voluptuous as vol -from homeassistant.const import ATTR_ATTRIBUTION, CONF_USERNAME, CONF_PASSWORD +from homeassistant.const import ATTR_ATTRIBUTION, CONF_PASSWORD, CONF_USERNAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -39,8 +40,6 @@ def setup(hass, config): password = conf.get(CONF_PASSWORD) try: - from skybellpy import Skybell - cache = hass.config.path(DEFAULT_CACHEDB) skybell = Skybell( username=username, password=password, get_devices=True, cache_path=cache diff --git a/homeassistant/components/sleepiq/__init__.py b/homeassistant/components/sleepiq/__init__.py index 7035299709d..2b4d9d010a3 100644 --- a/homeassistant/components/sleepiq/__init__.py +++ b/homeassistant/components/sleepiq/__init__.py @@ -1,13 +1,14 @@ """Support for SleepIQ from SleepNumber.""" -import logging from datetime import timedelta +import logging +from sleepyq import Sleepyq import voluptuous as vol -import homeassistant.helpers.config_validation as cv +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD from homeassistant.util import Throttle DOMAIN = "sleepiq" @@ -47,8 +48,6 @@ def setup(hass, config): """ global DATA - from sleepyq import Sleepyq - username = config[DOMAIN][CONF_USERNAME] password = config[DOMAIN][CONF_PASSWORD] client = Sleepyq(username, password) diff --git a/homeassistant/components/slide/__init__.py b/homeassistant/components/slide/__init__.py index 54154ae863e..ccf4465577b 100644 --- a/homeassistant/components/slide/__init__.py +++ b/homeassistant/components/slide/__init__.py @@ -1,24 +1,25 @@ """Component for the Go Slide API.""" -import logging from datetime import timedelta +import logging -import voluptuous as vol from goslideapi import GoSlideCloud, goslideapi +import voluptuous as vol from homeassistant.const import ( - CONF_USERNAME, CONF_PASSWORD, CONF_SCAN_INTERVAL, - STATE_OPEN, + CONF_USERNAME, STATE_CLOSED, - STATE_OPENING, STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, ) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.discovery import async_load_platform -from homeassistant.helpers.event import async_track_time_interval, async_call_later -from .const import DOMAIN, SLIDES, API, COMPONENT, DEFAULT_RETRY +from homeassistant.helpers.event import async_call_later, async_track_time_interval + +from .const import API, COMPONENT, DEFAULT_RETRY, DOMAIN, SLIDES _LOGGER = logging.getLogger(__name__) @@ -59,8 +60,7 @@ async def async_setup(hass, config): for slide in result: if "device_id" not in slide: _LOGGER.error( - "Found invalid Slide entry, device_id is " "missing. Entry=%s", - slide, + "Found invalid Slide entry, device_id is missing. Entry=%s", slide, ) continue @@ -103,7 +103,7 @@ async def async_setup(hass, config): ) elif "code" in slide["device_info"]: _LOGGER.warning( - "Slide %s (%s) is offline with " "code=%s", + "Slide %s (%s) is offline with code=%s", slide["id"], slidenew["mac"], slide["device_info"]["code"], diff --git a/homeassistant/components/slide/cover.py b/homeassistant/components/slide/cover.py index 1c4e6da5aac..a567a9bf61b 100644 --- a/homeassistant/components/slide/cover.py +++ b/homeassistant/components/slide/cover.py @@ -2,15 +2,16 @@ import logging -from homeassistant.const import ATTR_ID from homeassistant.components.cover import ( ATTR_POSITION, - STATE_CLOSED, - STATE_OPENING, - STATE_CLOSING, DEVICE_CLASS_CURTAIN, + STATE_CLOSED, + STATE_CLOSING, + STATE_OPENING, CoverDevice, ) +from homeassistant.const import ATTR_ID + from .const import API, DOMAIN, SLIDES _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/smappee/sensor.py b/homeassistant/components/smappee/sensor.py index 28abf759d09..c61d28bbaac 100644 --- a/homeassistant/components/smappee/sensor.py +++ b/homeassistant/components/smappee/sensor.py @@ -97,19 +97,18 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) if smappee.is_local_active: - for location_id in smappee.locations.keys(): + if smappee.is_remote_active: + location_keys = smappee.locations.keys() + else: + location_keys = [None] + for location_id in location_keys: for sensor in SENSOR_TYPES: if "local" in SENSOR_TYPES[sensor]: - if smappee.is_remote_active: - dev.append( - SmappeeSensor( - smappee, location_id, sensor, SENSOR_TYPES[sensor] - ) - ) - else: - dev.append( - SmappeeSensor(smappee, None, sensor, SENSOR_TYPES[sensor]) + dev.append( + SmappeeSensor( + smappee, location_id, sensor, SENSOR_TYPES[sensor] ) + ) add_entities(dev, True) diff --git a/homeassistant/components/smartthings/.translations/da.json b/homeassistant/components/smartthings/.translations/da.json index 18412069394..04fe2171f39 100644 --- a/homeassistant/components/smartthings/.translations/da.json +++ b/homeassistant/components/smartthings/.translations/da.json @@ -1,9 +1,9 @@ { "config": { "error": { - "app_not_installed": "S\u00f8rg for at du har installeret og autoriseret Home Assistant SmartApp og pr\u00f8v igen.", + "app_not_installed": "S\u00f8rg for, at du har installeret og godkendt Home Assistant SmartApp, og pr\u00f8v igen.", "app_setup_error": "SmartApp kunne ikke konfigureres. Pr\u00f8v igen.", - "base_url_not_https": "`base_url` til` http` komponenten skal konfigureres og starte med `https://`.", + "base_url_not_https": "`base_url` til `http`-komponenten skal konfigureres og starte med `https://`.", "token_already_setup": "Token er allerede konfigureret.", "token_forbidden": "Adgangstoken er ikke indenfor OAuth", "token_invalid_format": "Adgangstoken skal v\u00e6re i UID/GUID format", diff --git a/homeassistant/components/smartthings/manifest.json b/homeassistant/components/smartthings/manifest.json index 8b5bf65afa1..9fa156e0b28 100644 --- a/homeassistant/components/smartthings/manifest.json +++ b/homeassistant/components/smartthings/manifest.json @@ -3,14 +3,8 @@ "name": "Smartthings", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/smartthings", - "requirements": [ - "pysmartapp==0.3.2", - "pysmartthings==0.6.9" - ], - "dependencies": [ - "webhook" - ], - "codeowners": [ - "@andrewsayre" - ] + "requirements": ["pysmartapp==0.3.2", "pysmartthings==0.7.0"], + "dependencies": ["webhook"], + "after_dependencies": ["cloud"], + "codeowners": ["@andrewsayre"] } diff --git a/homeassistant/components/smartthings/smartapp.py b/homeassistant/components/smartthings/smartapp.py index ecd4da5dcab..d0487290926 100644 --- a/homeassistant/components/smartthings/smartapp.py +++ b/homeassistant/components/smartthings/smartapp.py @@ -2,6 +2,7 @@ import asyncio import functools import logging +import secrets from urllib.parse import urlparse from uuid import uuid4 @@ -88,10 +89,7 @@ async def validate_installed_app(api, installed_app_id: str): def validate_webhook_requirements(hass: HomeAssistantType) -> bool: """Ensure HASS is setup properly to receive webhooks.""" - if ( - "cloud" in hass.config.components - and hass.components.cloud.async_active_subscription() - ): + if hass.components.cloud.async_active_subscription(): return True if hass.data[DOMAIN][CONF_CLOUDHOOK_URL] is not None: return True @@ -105,11 +103,7 @@ def get_webhook_url(hass: HomeAssistantType) -> str: Return the cloudhook if available, otherwise local webhook. """ cloudhook_url = hass.data[DOMAIN][CONF_CLOUDHOOK_URL] - if ( - "cloud" in hass.config.components - and hass.components.cloud.async_active_subscription() - and cloudhook_url is not None - ): + if hass.components.cloud.async_active_subscription() and cloudhook_url is not None: return cloudhook_url return webhook.async_generate_url(hass, hass.data[DOMAIN][CONF_WEBHOOK_ID]) @@ -215,7 +209,7 @@ async def setup_smartapp_endpoint(hass: HomeAssistantType): # Create config config = { CONF_INSTANCE_ID: str(uuid4()), - CONF_WEBHOOK_ID: webhook.generate_secret(), + CONF_WEBHOOK_ID: secrets.token_hex(), CONF_CLOUDHOOK_URL: None, } await store.async_save(config) @@ -229,12 +223,11 @@ async def setup_smartapp_endpoint(hass: HomeAssistantType): cloudhook_url = config.get(CONF_CLOUDHOOK_URL) if ( cloudhook_url is None - and "cloud" in hass.config.components and hass.components.cloud.async_active_subscription() and not hass.config_entries.async_entries(DOMAIN) ): cloudhook_url = await hass.components.cloud.async_create_cloudhook( - hass, config[CONF_WEBHOOK_ID] + config[CONF_WEBHOOK_ID] ) config[CONF_CLOUDHOOK_URL] = cloudhook_url await store.async_save(config) @@ -281,13 +274,9 @@ async def unload_smartapp_endpoint(hass: HomeAssistantType): return # Remove the cloudhook if it was created cloudhook_url = hass.data[DOMAIN][CONF_CLOUDHOOK_URL] - if ( - cloudhook_url - and "cloud" in hass.config.components - and hass.components.cloud.async_is_logged_in() - ): + if cloudhook_url and hass.components.cloud.async_is_logged_in(): await hass.components.cloud.async_delete_cloudhook( - hass, hass.data[DOMAIN][CONF_WEBHOOK_ID] + hass.data[DOMAIN][CONF_WEBHOOK_ID] ) # Remove cloudhook from storage store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) @@ -350,7 +339,7 @@ async def smartapp_sync_subscriptions( ) except Exception as error: # pylint:disable=broad-except _LOGGER.error( - "Failed to remove subscription for '%s' under app " "'%s': %s", + "Failed to remove subscription for '%s' under app '%s': %s", sub.capability, installed_app_id, error, diff --git a/homeassistant/components/smarty/__init__.py b/homeassistant/components/smarty/__init__.py index ad824055126..22987673005 100644 --- a/homeassistant/components/smarty/__init__.py +++ b/homeassistant/components/smarty/__init__.py @@ -1,12 +1,13 @@ """Support to control a Salda Smarty XP/XV ventilation unit.""" from datetime import timedelta - import ipaddress import logging + +from pysmarty import Smarty import voluptuous as vol -from homeassistant.const import CONF_NAME, CONF_HOST +from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import dispatcher_send @@ -36,7 +37,6 @@ SIGNAL_UPDATE_SMARTY = "smarty_update" def setup(hass, config): """Set up the smarty environment.""" - from pysmarty import Smarty conf = config[DOMAIN] diff --git a/homeassistant/components/smarty/binary_sensor.py b/homeassistant/components/smarty/binary_sensor.py index 8723f0248d3..a86b3548e95 100644 --- a/homeassistant/components/smarty/binary_sensor.py +++ b/homeassistant/components/smarty/binary_sensor.py @@ -2,9 +2,10 @@ import logging -from homeassistant.core import callback from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect + from . import DOMAIN, SIGNAL_UPDATE_SMARTY _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/smarty/fan.py b/homeassistant/components/smarty/fan.py index 81edac80cb0..bb6b7623779 100644 --- a/homeassistant/components/smarty/fan.py +++ b/homeassistant/components/smarty/fan.py @@ -2,7 +2,6 @@ import logging -from homeassistant.core import callback from homeassistant.components.fan import ( SPEED_HIGH, SPEED_LOW, @@ -11,6 +10,7 @@ from homeassistant.components.fan import ( SUPPORT_SET_SPEED, FanEntity, ) +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import DOMAIN, SIGNAL_UPDATE_SMARTY diff --git a/homeassistant/components/smarty/sensor.py b/homeassistant/components/smarty/sensor.py index bf647777b52..f5cd1fbb404 100644 --- a/homeassistant/components/smarty/sensor.py +++ b/homeassistant/components/smarty/sensor.py @@ -3,15 +3,16 @@ import datetime as dt import logging -from homeassistant.core import callback from homeassistant.const import ( - TEMP_CELSIUS, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TIMESTAMP, + TEMP_CELSIUS, ) -import homeassistant.util.dt as dt_util +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 _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/smhi/.translations/da.json b/homeassistant/components/smhi/.translations/da.json index b43fef7ec45..52c4f54ebd7 100644 --- a/homeassistant/components/smhi/.translations/da.json +++ b/homeassistant/components/smhi/.translations/da.json @@ -2,7 +2,7 @@ "config": { "error": { "name_exists": "Navnet findes allerede", - "wrong_location": "Placering kun i Sverige" + "wrong_location": "Lokalitet kun i Sverige" }, "step": { "user": { @@ -11,7 +11,7 @@ "longitude": "L\u00e6ngdegrad", "name": "Navn" }, - "title": "Placering i Sverige" + "title": "Lokalitet i Sverige" } }, "title": "Svensk vejr service (SMHI)" diff --git a/homeassistant/components/smhi/config_flow.py b/homeassistant/components/smhi/config_flow.py index 3b60cb66165..2c04896497a 100644 --- a/homeassistant/components/smhi/config_flow.py +++ b/homeassistant/components/smhi/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure SMHI component.""" +from smhi.smhi_lib import Smhi, SmhiForecastException import voluptuous as vol from homeassistant import config_entries @@ -96,7 +97,6 @@ class SmhiFlowHandler(config_entries.ConfigFlow): async def _check_location(self, longitude: str, latitude: str) -> bool: """Return true if location is ok.""" - from smhi.smhi_lib import Smhi, SmhiForecastException try: session = aiohttp_client.async_get_clientsession(self.hass) diff --git a/homeassistant/components/smhi/weather.py b/homeassistant/components/smhi/weather.py index 5f6722b72a6..574b8d85767 100644 --- a/homeassistant/components/smhi/weather.py +++ b/homeassistant/components/smhi/weather.py @@ -6,6 +6,8 @@ from typing import Dict, List import aiohttp import async_timeout +from smhi import Smhi +from smhi.smhi_lib import SmhiForecastException from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, @@ -90,7 +92,6 @@ class SmhiWeather(WeatherEntity): session: aiohttp.ClientSession = None, ) -> None: """Initialize the SMHI weather entity.""" - from smhi import Smhi self._name = name self._latitude = latitude @@ -107,7 +108,6 @@ class SmhiWeather(WeatherEntity): @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self) -> None: """Refresh the forecast data from SMHI weather API.""" - from smhi.smhi_lib import SmhiForecastException def fail(): """Postpone updates.""" diff --git a/homeassistant/components/smtp/notify.py b/homeassistant/components/smtp/notify.py index d592f25a61d..82b0f96f785 100644 --- a/homeassistant/components/smtp/notify.py +++ b/homeassistant/components/smtp/notify.py @@ -10,6 +10,13 @@ import smtplib import voluptuous as vol +from homeassistant.components.notify import ( + ATTR_DATA, + ATTR_TITLE, + ATTR_TITLE_DEFAULT, + PLATFORM_SCHEMA, + BaseNotificationService, +) from homeassistant.const import ( CONF_PASSWORD, CONF_PORT, @@ -21,14 +28,6 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util -from homeassistant.components.notify import ( - ATTR_DATA, - ATTR_TITLE, - ATTR_TITLE_DEFAULT, - PLATFORM_SCHEMA, - BaseNotificationService, -) - _LOGGER = logging.getLogger(__name__) ATTR_IMAGES = "images" # optional embedded image file attachments diff --git a/homeassistant/components/snips/__init__.py b/homeassistant/components/snips/__init__.py index 441104211cf..65015bd723c 100644 --- a/homeassistant/components/snips/__init__.py +++ b/homeassistant/components/snips/__init__.py @@ -1,13 +1,13 @@ """Support for Snips on-device ASR and NLU.""" +from datetime import timedelta import json import logging -from datetime import timedelta import voluptuous as vol -from homeassistant.core import callback -from homeassistant.helpers import intent, config_validation as cv from homeassistant.components import mqtt +from homeassistant.core import callback +from homeassistant.helpers import config_validation as cv, intent DOMAIN = "snips" CONF_INTENTS = "intents" @@ -135,7 +135,6 @@ async def async_setup(hass, config): intent_type = request["intent"]["intentName"].split("__")[-1] else: intent_type = request["intent"]["intentName"].split(":")[-1] - snips_response = None slots = {} for slot in request.get("slots", []): slots[slot["slotName"]] = {"value": resolve_slot_values(slot)} @@ -148,8 +147,15 @@ async def async_setup(hass, config): intent_response = await intent.async_handle( hass, DOMAIN, intent_type, slots, request["input"] ) + notification = {"sessionId": request.get("sessionId", "default")} + if "plain" in intent_response.speech: - snips_response = intent_response.speech["plain"]["speech"] + notification["text"] = intent_response.speech["plain"]["speech"] + + _LOGGER.debug("send_response %s", json.dumps(notification)) + mqtt.async_publish( + hass, "hermes/dialogueManager/endSession", json.dumps(notification) + ) except intent.UnknownIntent: _LOGGER.warning( "Received unknown intent %s", request["intent"]["intentName"] @@ -157,17 +163,6 @@ async def async_setup(hass, config): except intent.IntentError: _LOGGER.exception("Error while handling intent: %s.", intent_type) - if snips_response: - notification = { - "sessionId": request.get("sessionId", "default"), - "text": snips_response, - } - - _LOGGER.debug("send_response %s", json.dumps(notification)) - mqtt.async_publish( - hass, "hermes/dialogueManager/endSession", json.dumps(notification) - ) - await hass.components.mqtt.async_subscribe(INTENT_TOPIC, message_received) async def snips_say(call): diff --git a/homeassistant/components/snmp/switch.py b/homeassistant/components/snmp/switch.py index 8d5be1221c4..578b97c801e 100644 --- a/homeassistant/components/snmp/switch.py +++ b/homeassistant/components/snmp/switch.py @@ -2,7 +2,6 @@ import logging from pyasn1.type.univ import Integer - import pysnmp.hlapi.asyncio as hlapi from pysnmp.hlapi.asyncio import ( CommunityData, diff --git a/homeassistant/components/sochain/sensor.py b/homeassistant/components/sochain/sensor.py index cc5a477652b..608405dd1b4 100644 --- a/homeassistant/components/sochain/sensor.py +++ b/homeassistant/components/sochain/sensor.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +from pysochain import ChainSo import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -31,7 +32,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the sochain sensors.""" - from pysochain import ChainSo address = config.get(CONF_ADDRESS) network = config.get(CONF_NETWORK) diff --git a/homeassistant/components/solaredge/__init__.py b/homeassistant/components/solaredge/__init__.py index 8909b970aaf..bafc6b67f1c 100644 --- a/homeassistant/components/solaredge/__init__.py +++ b/homeassistant/components/solaredge/__init__.py @@ -6,7 +6,7 @@ from homeassistant.const import CONF_API_KEY, CONF_NAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import HomeAssistantType -from .const import DEFAULT_NAME, DOMAIN, CONF_SITE_ID +from .const import CONF_SITE_ID, DEFAULT_NAME, DOMAIN CONFIG_SCHEMA = vol.Schema( { diff --git a/homeassistant/components/solaredge/config_flow.py b/homeassistant/components/solaredge/config_flow.py index 67f05d83aa0..7c8c9380522 100644 --- a/homeassistant/components/solaredge/config_flow.py +++ b/homeassistant/components/solaredge/config_flow.py @@ -1,14 +1,14 @@ """Config flow for the SolarEdge platform.""" +from requests.exceptions import ConnectTimeout, HTTPError import solaredge import voluptuous as vol -from requests.exceptions import HTTPError, ConnectTimeout from homeassistant import config_entries from homeassistant.const import CONF_API_KEY, CONF_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.util import slugify -from .const import DOMAIN, DEFAULT_NAME, CONF_SITE_ID +from .const import CONF_SITE_ID, DEFAULT_NAME, DOMAIN @callback diff --git a/homeassistant/components/solaredge/const.py b/homeassistant/components/solaredge/const.py index 0d3d1a0cb5f..6fec88c42d5 100644 --- a/homeassistant/components/solaredge/const.py +++ b/homeassistant/components/solaredge/const.py @@ -1,7 +1,7 @@ """Constants for the SolarEdge Monitoring API.""" from datetime import timedelta -from homeassistant.const import POWER_WATT, ENERGY_WATT_HOUR +from homeassistant.const import ENERGY_WATT_HOUR, POWER_WATT DOMAIN = "solaredge" diff --git a/homeassistant/components/solaredge/sensor.py b/homeassistant/components/solaredge/sensor.py index 896596a2a34..60cabaf38f0 100644 --- a/homeassistant/components/solaredge/sensor.py +++ b/homeassistant/components/solaredge/sensor.py @@ -1,17 +1,19 @@ """Support for SolarEdge Monitoring API.""" import logging -import solaredge -from requests.exceptions import HTTPError, ConnectTimeout +from requests.exceptions import ConnectTimeout, HTTPError +import solaredge +from stringcase import snakecase + from homeassistant.const import CONF_API_KEY from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle from .const import ( CONF_SITE_ID, - OVERVIEW_UPDATE_DELAY, DETAILS_UPDATE_DELAY, INVENTORY_UPDATE_DELAY, + OVERVIEW_UPDATE_DELAY, POWER_FLOW_UPDATE_DELAY, SENSOR_TYPES, ) @@ -39,7 +41,7 @@ async def async_setup_entry(hass, entry, async_add_entities): return _LOGGER.debug("Credentials correct and site is active") except KeyError: - _LOGGER.error("Missing details data in solaredge response") + _LOGGER.error("Missing details data in SolarEdge response") return except (ConnectTimeout, HTTPError): _LOGGER.error("Could not retrieve details from SolarEdge API") @@ -262,7 +264,6 @@ class SolarEdgeDetailsDataService(SolarEdgeDataService): @Throttle(DETAILS_UPDATE_DELAY) def update(self): """Update the data from the SolarEdge Monitoring API.""" - from stringcase import snakecase try: data = self.api.get_details(self.site_id) @@ -349,7 +350,9 @@ class SolarEdgePowerFlowDataService(SolarEdgeDataService): power_to = [] if "connections" not in power_flow: - _LOGGER.error("Missing connections in power flow data") + _LOGGER.debug( + "Missing connections in power flow data. Assuming site does not have any" + ) return for connection in power_flow["connections"]: diff --git a/homeassistant/components/solaredge_local/sensor.py b/homeassistant/components/solaredge_local/sensor.py index 917fb86ddcb..ecf9dfde8b1 100644 --- a/homeassistant/components/solaredge_local/sensor.py +++ b/homeassistant/components/solaredge_local/sensor.py @@ -1,10 +1,10 @@ """Support for SolarEdge-local Monitoring API.""" -import logging -from datetime import timedelta -import statistics from copy import deepcopy +from datetime import timedelta +import logging +import statistics -from requests.exceptions import HTTPError, ConnectTimeout +from requests.exceptions import ConnectTimeout, HTTPError from solaredge_local import SolarEdge import voluptuous as vol @@ -12,8 +12,8 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_IP_ADDRESS, CONF_NAME, - POWER_WATT, ENERGY_WATT_HOUR, + POWER_WATT, TEMP_CELSIUS, TEMP_FAHRENHEIT, ) diff --git a/homeassistant/components/solarlog/.translations/nn.json b/homeassistant/components/solarlog/.translations/nn.json new file mode 100644 index 00000000000..3ce86b4e10a --- /dev/null +++ b/homeassistant/components/solarlog/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Solar-Log" + } +} \ No newline at end of file diff --git a/homeassistant/components/solarlog/const.py b/homeassistant/components/solarlog/const.py index 67eb8006cec..933f8014090 100644 --- a/homeassistant/components/solarlog/const.py +++ b/homeassistant/components/solarlog/const.py @@ -1,7 +1,7 @@ """Constants for the Solar-Log integration.""" from datetime import timedelta -from homeassistant.const import POWER_WATT, ENERGY_KILO_WATT_HOUR +from homeassistant.const import ENERGY_KILO_WATT_HOUR, POWER_WATT DOMAIN = "solarlog" diff --git a/homeassistant/components/solarlog/sensor.py b/homeassistant/components/solarlog/sensor.py index 583529ffe87..85ab9eb913e 100644 --- a/homeassistant/components/solarlog/sensor.py +++ b/homeassistant/components/solarlog/sensor.py @@ -6,14 +6,14 @@ from requests.exceptions import HTTPError, Timeout from sunwatcher.solarlog.solarlog import SolarLog import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_HOST, CONF_NAME +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -from .const import DOMAIN, DEFAULT_HOST, DEFAULT_NAME, SCAN_INTERVAL, SENSOR_TYPES +from .const import DEFAULT_HOST, DEFAULT_NAME, DOMAIN, SCAN_INTERVAL, SENSOR_TYPES _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/solax/sensor.py b/homeassistant/components/solax/sensor.py index a5b4547b344..8eb61560e63 100644 --- a/homeassistant/components/solax/sensor.py +++ b/homeassistant/components/solax/sensor.py @@ -1,6 +1,5 @@ """Support for Solax inverter via local API.""" import asyncio - from datetime import timedelta import logging @@ -8,11 +7,11 @@ from solax import real_time_api from solax.inverter import InverterError import voluptuous as vol -from homeassistant.const import TEMP_CELSIUS, CONF_IP_ADDRESS, CONF_PORT -from homeassistant.helpers.entity import Entity -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA +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 _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/soma/.translations/ca.json b/homeassistant/components/soma/.translations/ca.json index 18b33d1bc9b..a1a5b9489fa 100644 --- a/homeassistant/components/soma/.translations/ca.json +++ b/homeassistant/components/soma/.translations/ca.json @@ -3,7 +3,9 @@ "abort": { "already_setup": "Nom\u00e9s pots configurar un compte de Soma.", "authorize_url_timeout": "S'ha acabat el temps d'espera durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.", - "missing_configuration": "El component Soma no est\u00e0 configurat. Mira'n la documentaci\u00f3." + "connection_error": "No s'ha pogut connectar amb SOMA Connect.", + "missing_configuration": "El component Soma no est\u00e0 configurat. Mira'n la documentaci\u00f3.", + "result_error": "SOMA Connect ha respost amb un estat d'error." }, "create_entry": { "default": "Autenticaci\u00f3 exitosa amb Soma." diff --git a/homeassistant/components/soma/.translations/da.json b/homeassistant/components/soma/.translations/da.json index 557eeab55b1..49bf83148a2 100644 --- a/homeassistant/components/soma/.translations/da.json +++ b/homeassistant/components/soma/.translations/da.json @@ -3,7 +3,9 @@ "abort": { "already_setup": "Du kan kun konfigurere en Soma-konto.", "authorize_url_timeout": "Timeout ved generering af autoriseret url.", - "missing_configuration": "Soma-komponenten er ikke konfigureret. F\u00f8lg venligst dokumentationen." + "connection_error": "Kunne ikke oprette forbindelse til SOMA Connect.", + "missing_configuration": "Soma-komponenten er ikke konfigureret. F\u00f8lg venligst dokumentationen.", + "result_error": "SOMA Connect svarede med fejlstatus." }, "create_entry": { "default": "Godkendt med Soma." diff --git a/homeassistant/components/soma/.translations/en.json b/homeassistant/components/soma/.translations/en.json index 42e09a8762c..46bfd441fc4 100644 --- a/homeassistant/components/soma/.translations/en.json +++ b/homeassistant/components/soma/.translations/en.json @@ -3,7 +3,9 @@ "abort": { "already_setup": "You can only configure one Soma account.", "authorize_url_timeout": "Timeout generating authorize url.", - "missing_configuration": "The Soma component is not configured. Please follow the documentation." + "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." }, "create_entry": { "default": "Successfully authenticated with Soma." diff --git a/homeassistant/components/soma/.translations/es.json b/homeassistant/components/soma/.translations/es.json index 86922622704..6df113b82c9 100644 --- a/homeassistant/components/soma/.translations/es.json +++ b/homeassistant/components/soma/.translations/es.json @@ -3,7 +3,9 @@ "abort": { "already_setup": "S\u00f3lo puede configurar una cuenta de Soma.", "authorize_url_timeout": "Tiempo de espera agotado para la autorizaci\u00f3n de la url.", - "missing_configuration": "El componente Soma no est\u00e1 configurado. Por favor, leer la documentaci\u00f3n." + "connection_error": "No se ha podido conectar a SOMA Connect.", + "missing_configuration": "El componente Soma no est\u00e1 configurado. Por favor, leer la documentaci\u00f3n.", + "result_error": "SOMA Connect respondi\u00f3 con un error." }, "create_entry": { "default": "Autenticado con \u00e9xito con Soma." diff --git a/homeassistant/components/soma/.translations/fr.json b/homeassistant/components/soma/.translations/fr.json index a758ab0f615..0889cdea2ec 100644 --- a/homeassistant/components/soma/.translations/fr.json +++ b/homeassistant/components/soma/.translations/fr.json @@ -3,7 +3,9 @@ "abort": { "already_setup": "Vous ne pouvez configurer qu'un seul compte Soma.", "authorize_url_timeout": "D\u00e9lai d'attente g\u00e9n\u00e9rant l'autorisation de l'URL.", - "missing_configuration": "Le composant Soma n'est pas configur\u00e9. Veuillez suivre la documentation." + "connection_error": "Impossible de se connecter \u00e0 SOMA Connect.", + "missing_configuration": "Le composant Soma n'est pas configur\u00e9. Veuillez suivre la documentation.", + "result_error": "SOMA Connect a r\u00e9pondu avec l'\u00e9tat d'erreur." }, "create_entry": { "default": "Authentifi\u00e9 avec succ\u00e8s avec Soma." diff --git a/homeassistant/components/soma/.translations/it.json b/homeassistant/components/soma/.translations/it.json index 1398b2a66be..6c7d0129708 100644 --- a/homeassistant/components/soma/.translations/it.json +++ b/homeassistant/components/soma/.translations/it.json @@ -3,7 +3,9 @@ "abort": { "already_setup": "\u00c8 possibile configurare un solo account Soma.", "authorize_url_timeout": "Timeout durante la generazione dell'URL di autorizzazione.", - "missing_configuration": "Il componente Soma non \u00e8 configurato. Si prega di seguire la documentazione." + "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." }, "create_entry": { "default": "Autenticato con successo con Soma." diff --git a/homeassistant/components/soma/.translations/ko.json b/homeassistant/components/soma/.translations/ko.json index 90995ebc9f2..ae4d84671a3 100644 --- a/homeassistant/components/soma/.translations/ko.json +++ b/homeassistant/components/soma/.translations/ko.json @@ -3,7 +3,9 @@ "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.", - "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." + "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." diff --git a/homeassistant/components/soma/.translations/lb.json b/homeassistant/components/soma/.translations/lb.json index 93e9a1e66c4..fdf180a1a61 100644 --- a/homeassistant/components/soma/.translations/lb.json +++ b/homeassistant/components/soma/.translations/lb.json @@ -3,7 +3,9 @@ "abort": { "already_setup": "Dir k\u00ebnnt n\u00ebmmen een eenzegen Soma Kont konfigur\u00e9ieren.", "authorize_url_timeout": "Z\u00e4it Iwwerschreidung beim gener\u00e9ieren vun der Autorisatiouns URL.", - "missing_configuration": "D'Soma Komponent ass nach net konfigur\u00e9iert. Follegt w.e.g der Dokumentatioun." + "connection_error": "Feeler beim verbannen mat SOMA Connect.", + "missing_configuration": "D'Soma Komponent ass nach net konfigur\u00e9iert. Follegt w.e.g der Dokumentatioun.", + "result_error": "SOMA Connect \u00e4ntwert mat engem Feeler Code." }, "create_entry": { "default": "Erfollegr\u00e4ich mat Soma authentifiz\u00e9iert." diff --git a/homeassistant/components/soma/.translations/nl.json b/homeassistant/components/soma/.translations/nl.json index c1188b0ac63..058f7222666 100644 --- a/homeassistant/components/soma/.translations/nl.json +++ b/homeassistant/components/soma/.translations/nl.json @@ -3,7 +3,9 @@ "abort": { "already_setup": "U kunt slechts \u00e9\u00e9n Soma-account configureren.", "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", - "missing_configuration": "De Soma-component is niet geconfigureerd. Gelieve de documentatie te volgen." + "connection_error": "Kan geen verbinding maken met SOMA Connect.", + "missing_configuration": "De Soma-component is niet geconfigureerd. Gelieve de documentatie te volgen.", + "result_error": "SOMA Connect reageerde met een foutstatus." }, "create_entry": { "default": "Succesvol geverifieerd met Soma." diff --git a/homeassistant/components/soma/.translations/no.json b/homeassistant/components/soma/.translations/no.json index b2d80208b83..518cbc6a37e 100644 --- a/homeassistant/components/soma/.translations/no.json +++ b/homeassistant/components/soma/.translations/no.json @@ -3,7 +3,9 @@ "abort": { "already_setup": "Du kan bare konfigurere \u00e9n Soma-konto.", "authorize_url_timeout": "Tidsavbrudd ved generering av autoriseringsadresse.", - "missing_configuration": "Soma-komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen." + "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." }, "create_entry": { "default": "Vellykket autentisering med Somfy." diff --git a/homeassistant/components/soma/.translations/pl.json b/homeassistant/components/soma/.translations/pl.json index c71e160142e..102413bf446 100644 --- a/homeassistant/components/soma/.translations/pl.json +++ b/homeassistant/components/soma/.translations/pl.json @@ -3,7 +3,9 @@ "abort": { "already_setup": "Mo\u017cesz skonfigurowa\u0107 tylko jedno konto Soma.", "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji.", - "missing_configuration": "Komponent Soma nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105." + "connection_error": "Nie uda\u0142o si\u0119 po\u0142\u0105czy\u0107 z SOMA Connect.", + "missing_configuration": "Komponent Soma nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105.", + "result_error": "SOMA Connect odpowiedzia\u0142 statusem b\u0142\u0119du." }, "create_entry": { "default": "Pomy\u015blnie uwierzytelniono z Soma" diff --git a/homeassistant/components/soma/.translations/ru.json b/homeassistant/components/soma/.translations/ru.json index f28d672d3f2..fa581eb0821 100644 --- a/homeassistant/components/soma/.translations/ru.json +++ b/homeassistant/components/soma/.translations/ru.json @@ -3,7 +3,9 @@ "abort": { "already_setup": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "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": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 Soma \u043d\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d. \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." + "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a SOMA Connect.", + "missing_configuration": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 Soma \u043d\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d. \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.", + "result_error": "SOMA Connect \u043e\u0442\u0432\u0435\u0442\u0438\u043b \u0441\u043e \u0441\u0442\u0430\u0442\u0443\u0441\u043e\u043c \u043e\u0448\u0438\u0431\u043a\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." @@ -15,7 +17,7 @@ "port": "\u041f\u043e\u0440\u0442" }, "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 SOMA Connect.", - "title": "Soma" + "title": "SOMA Connect" } }, "title": "Soma" diff --git a/homeassistant/components/soma/.translations/sl.json b/homeassistant/components/soma/.translations/sl.json index b3075208d2c..01f7e50eb96 100644 --- a/homeassistant/components/soma/.translations/sl.json +++ b/homeassistant/components/soma/.translations/sl.json @@ -3,7 +3,9 @@ "abort": { "already_setup": "Nastavite lahko samo en ra\u010dun Soma.", "authorize_url_timeout": "\u010casovna omejitev za generiranje potrditvenega URL-ja je potekla.", - "missing_configuration": "Komponenta Soma ni konfigurirana. Upo\u0161tevajte dokumentacijo." + "connection_error": "Povezava s SOMA Connect ni uspela.", + "missing_configuration": "Komponenta Soma ni konfigurirana. Upo\u0161tevajte dokumentacijo.", + "result_error": "SOMA Connect se je odzvala s statusom napake." }, "create_entry": { "default": "Uspe\u0161no overjen s Soma." diff --git a/homeassistant/components/soma/.translations/zh-Hant.json b/homeassistant/components/soma/.translations/zh-Hant.json index 893abe82ee1..73b26cb91f1 100644 --- a/homeassistant/components/soma/.translations/zh-Hant.json +++ b/homeassistant/components/soma/.translations/zh-Hant.json @@ -3,7 +3,9 @@ "abort": { "already_setup": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44 Soma \u5e33\u865f\u3002", "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642", - "missing_configuration": "Soma \u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002" + "connection_error": "SOMA \u9023\u7dda\u5931\u6557\u3002", + "missing_configuration": "Soma \u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", + "result_error": "SOMA \u9023\u7dda\u56de\u61c9\u72c0\u614b\u932f\u8aa4\u3002" }, "create_entry": { "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Soma \u8a2d\u5099\u3002" diff --git a/homeassistant/components/soma/__init__.py b/homeassistant/components/soma/__init__.py index b4daa28b5b2..93ee4fc9b8f 100644 --- a/homeassistant/components/soma/__init__.py +++ b/homeassistant/components/soma/__init__.py @@ -1,20 +1,18 @@ """Support for Soma Smartshades.""" import logging -import voluptuous as vol from api.soma_api import SomaApi from requests import RequestException +import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PORT +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType -from homeassistant.const import CONF_HOST, CONF_PORT - -from .const import DOMAIN, HOST, PORT, API - +from .const import API, DOMAIN, HOST, PORT DEVICES = "devices" diff --git a/homeassistant/components/soma/config_flow.py b/homeassistant/components/soma/config_flow.py index e2f89273520..afb5d05b77e 100644 --- a/homeassistant/components/soma/config_flow.py +++ b/homeassistant/components/soma/config_flow.py @@ -1,12 +1,13 @@ """Config flow for Soma.""" import logging -import voluptuous as vol from api.soma_api import SomaApi from requests import RequestException +import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_HOST, CONF_PORT + from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -39,14 +40,22 @@ class SomaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Finish config flow.""" api = SomaApi(user_input["host"], user_input["port"]) try: - await self.hass.async_add_executor_job(api.list_devices) + result = await self.hass.async_add_executor_job(api.list_devices) _LOGGER.info("Successfully set up Soma Connect") - return self.async_create_entry( - title="Soma Connect", - data={"host": user_input["host"], "port": user_input["port"]}, + if result["result"] == "success": + return self.async_create_entry( + title="Soma Connect", + data={"host": user_input["host"], "port": user_input["port"]}, + ) + _LOGGER.error( + "Connection to SOMA Connect failed (result:%s)", result["result"] ) + return self.async_abort(reason="result_error") except RequestException: - _LOGGER.error("Connection to SOMA Connect failed") + _LOGGER.error("Connection to SOMA Connect failed with RequestException") + return self.async_abort(reason="connection_error") + except KeyError: + _LOGGER.error("Connection to SOMA Connect failed with KeyError") return self.async_abort(reason="connection_error") async def async_step_import(self, user_input=None): diff --git a/homeassistant/components/soma/cover.py b/homeassistant/components/soma/cover.py index 1577b7f2911..d23cc9ec5d0 100644 --- a/homeassistant/components/soma/cover.py +++ b/homeassistant/components/soma/cover.py @@ -2,9 +2,8 @@ import logging -from homeassistant.components.cover import CoverDevice, ATTR_POSITION -from homeassistant.components.soma import DOMAIN, SomaEntity, DEVICES, API - +from homeassistant.components.cover import ATTR_POSITION, CoverDevice +from homeassistant.components.soma import API, DEVICES, DOMAIN, SomaEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/soma/strings.json b/homeassistant/components/soma/strings.json index aa2f92f0be6..67f1f6b7d46 100644 --- a/homeassistant/components/soma/strings.json +++ b/homeassistant/components/soma/strings.json @@ -3,7 +3,9 @@ "abort": { "already_setup": "You can only configure one Soma account.", "authorize_url_timeout": "Timeout generating authorize url.", - "missing_configuration": "The Soma component is not configured. Please follow the documentation." + "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." }, "create_entry": { "default": "Successfully authenticated with Soma." diff --git a/homeassistant/components/somfy/.translations/da.json b/homeassistant/components/somfy/.translations/da.json index 9d05fd65a06..b50c030c636 100644 --- a/homeassistant/components/somfy/.translations/da.json +++ b/homeassistant/components/somfy/.translations/da.json @@ -8,6 +8,11 @@ "create_entry": { "default": "Godkendt med Somfy." }, + "step": { + "pick_implementation": { + "title": "V\u00e6lg godkendelsesmetode" + } + }, "title": "Somfy" } } \ No newline at end of file diff --git a/homeassistant/components/somfy/__init__.py b/homeassistant/components/somfy/__init__.py index b767ea83431..365c6839300 100644 --- a/homeassistant/components/somfy/__init__.py +++ b/homeassistant/components/somfy/__init__.py @@ -5,15 +5,15 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/integrations/somfy/ """ import asyncio -import logging from datetime import timedelta +import logging -import voluptuous as vol from requests import HTTPError +import voluptuous as vol -from homeassistant.helpers import config_validation as cv, config_entry_oauth2_flow from homeassistant.components.somfy import config_flow from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import Throttle @@ -26,7 +26,7 @@ DEVICES = "devices" _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(seconds=10) +SCAN_INTERVAL = timedelta(seconds=30) DOMAIN = "somfy" @@ -48,7 +48,7 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -SOMFY_COMPONENTS = ["cover"] +SOMFY_COMPONENTS = ["cover", "switch"] async def async_setup(hass, config): diff --git a/homeassistant/components/somfy/api.py b/homeassistant/components/somfy/api.py index 1cfea8ff7d8..b2516cb36c4 100644 --- a/homeassistant/components/somfy/api.py +++ b/homeassistant/components/somfy/api.py @@ -4,7 +4,7 @@ from typing import Dict, Union from pymfy.api import somfy_api -from homeassistant import core, config_entries +from homeassistant import config_entries, core from homeassistant.helpers import config_entry_oauth2_flow diff --git a/homeassistant/components/somfy/config_flow.py b/homeassistant/components/somfy/config_flow.py index cb180d4e247..2d143fbd196 100644 --- a/homeassistant/components/somfy/config_flow.py +++ b/homeassistant/components/somfy/config_flow.py @@ -3,6 +3,7 @@ import logging from homeassistant import config_entries from homeassistant.helpers import config_entry_oauth2_flow + from .const import DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/somfy/cover.py b/homeassistant/components/somfy/cover.py index d54e7c99001..5eabe7ee07a 100644 --- a/homeassistant/components/somfy/cover.py +++ b/homeassistant/components/somfy/cover.py @@ -1,18 +1,14 @@ -""" -Support for Somfy Covers. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/cover.somfy/ -""" -from pymfy.api.devices.category import Category +"""Support for Somfy Covers.""" from pymfy.api.devices.blind import Blind +from pymfy.api.devices.category import Category from homeassistant.components.cover import ( - CoverDevice, ATTR_POSITION, ATTR_TILT_POSITION, + CoverDevice, ) -from homeassistant.components.somfy import DOMAIN, SomfyEntity, DEVICES, API + +from . import API, DEVICES, DOMAIN, SomfyEntity async def async_setup_entry(hass, config_entry, async_add_entities): @@ -37,15 +33,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(await hass.async_add_executor_job(get_covers), True) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Old way of setting up platform. - - Can only be called when a user accidentally mentions the platform in their - config. But even in that case it would have been ignored. - """ - pass - - class SomfyCover(SomfyEntity, CoverDevice): """Representation of a Somfy cover device.""" diff --git a/homeassistant/components/somfy/manifest.json b/homeassistant/components/somfy/manifest.json index f5a17275bcb..82e62e7dd08 100644 --- a/homeassistant/components/somfy/manifest.json +++ b/homeassistant/components/somfy/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/somfy", "dependencies": ["http"], "codeowners": ["@tetienne"], - "requirements": ["pymfy==0.6.1"] + "requirements": ["pymfy==0.7.1"] } diff --git a/homeassistant/components/somfy/switch.py b/homeassistant/components/somfy/switch.py new file mode 100644 index 00000000000..bc31d68ec1d --- /dev/null +++ b/homeassistant/components/somfy/switch.py @@ -0,0 +1,50 @@ +"""Support for Somfy Camera Shutter.""" +from pymfy.api.devices.camera_protect import CameraProtect +from pymfy.api.devices.category import Category + +from homeassistant.components.switch import SwitchDevice + +from . import API, DEVICES, DOMAIN, SomfyEntity + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Somfy switch platform.""" + + def get_shutters(): + """Retrieve switches.""" + devices = hass.data[DOMAIN][DEVICES] + + return [ + SomfyCameraShutter(device, hass.data[DOMAIN][API]) + for device in devices + if Category.CAMERA.value in device.categories + ] + + async_add_entities(await hass.async_add_executor_job(get_shutters), True) + + +class SomfyCameraShutter(SomfyEntity, SwitchDevice): + """Representation of a Somfy Camera Shutter device.""" + + def __init__(self, device, api): + """Initialize the Somfy device.""" + super().__init__(device, api) + self.shutter = CameraProtect(self.device, self.api) + + async def async_update(self): + """Update the device with the latest data.""" + await super().async_update() + self.shutter = CameraProtect(self.device, self.api) + + def turn_on(self, **kwargs) -> None: + """Turn the entity on.""" + self.shutter.open_shutter() + + def turn_off(self, **kwargs): + """Turn the entity off.""" + self.shutter.close_shutter() + + @property + def is_on(self) -> bool: + """Return True if entity is on.""" + return self.shutter.get_shutter_position() == "opened" diff --git a/homeassistant/components/somfy_mylink/__init__.py b/homeassistant/components/somfy_mylink/__init__.py index 394de5980ea..6c6333af5a6 100644 --- a/homeassistant/components/somfy_mylink/__init__.py +++ b/homeassistant/components/somfy_mylink/__init__.py @@ -1,6 +1,7 @@ """Component for the Somfy MyLink device supporting the Synergy API.""" import logging +from somfy_mylink_synergy import SomfyMyLinkSynergy import voluptuous as vol from homeassistant.const import CONF_HOST, CONF_PORT @@ -48,7 +49,6 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass, config): """Set up the MyLink platform.""" - from somfy_mylink_synergy import SomfyMyLinkSynergy host = config[DOMAIN][CONF_HOST] port = config[DOMAIN][CONF_PORT] diff --git a/homeassistant/components/sonarr/sensor.py b/homeassistant/components/sonarr/sensor.py index 47738521bf0..82bcdad6ef4 100644 --- a/homeassistant/components/sonarr/sensor.py +++ b/homeassistant/components/sonarr/sensor.py @@ -1,20 +1,21 @@ """Support for Sonarr.""" +from datetime import datetime import logging import time -from datetime import datetime +from pytz import timezone import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_API_KEY, CONF_HOST, - CONF_PORT, CONF_MONITORED_CONDITIONS, + CONF_PORT, CONF_SSL, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -80,7 +81,6 @@ class SonarrSensor(Entity): def __init__(self, hass, conf, sensor_type): """Create Sonarr entity.""" - from pytz import timezone self.conf = conf self.host = conf.get(CONF_HOST) diff --git a/homeassistant/components/songpal/const.py b/homeassistant/components/songpal/const.py new file mode 100644 index 00000000000..6a19e316a9f --- /dev/null +++ b/homeassistant/components/songpal/const.py @@ -0,0 +1,3 @@ +"""Constants for the Songpal component.""" +DOMAIN = "songpal" +SET_SOUND_SETTING = "set_sound_setting" diff --git a/homeassistant/components/songpal/media_player.py b/homeassistant/components/songpal/media_player.py index 0567cd0ea6a..27a81b2a667 100644 --- a/homeassistant/components/songpal/media_player.py +++ b/homeassistant/components/songpal/media_player.py @@ -1,21 +1,20 @@ """Support for Songpal-enabled (Sony) media devices.""" import asyncio -import logging from collections import OrderedDict +import logging -import voluptuous as vol from songpal import ( + ConnectChange, + ContentChange, Device, + PowerChange, SongpalException, VolumeChange, - ContentChange, - PowerChange, - ConnectChange, ) +import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( - DOMAIN, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, @@ -26,13 +25,15 @@ from homeassistant.components.media_player.const import ( from homeassistant.const import ( ATTR_ENTITY_ID, CONF_NAME, + EVENT_HOMEASSISTANT_STOP, STATE_OFF, STATE_ON, - EVENT_HOMEASSISTANT_STOP, ) from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv +from .const import DOMAIN, SET_SOUND_SETTING + _LOGGER = logging.getLogger(__name__) CONF_ENDPOINT = "endpoint" @@ -42,8 +43,6 @@ PARAM_VALUE = "value" PLATFORM = "songpal" -SET_SOUND_SETTING = "songpal_set_sound_setting" - SUPPORT_SONGPAL = ( SUPPORT_VOLUME_SET | SUPPORT_VOLUME_STEP diff --git a/homeassistant/components/songpal/services.yaml b/homeassistant/components/songpal/services.yaml index e69de29bb2d..8cf1a664276 100644 --- a/homeassistant/components/songpal/services.yaml +++ b/homeassistant/components/songpal/services.yaml @@ -0,0 +1,13 @@ +set_sound_setting: + description: Change sound setting. + + fields: + entity_id: + description: Target device. + example: 'media_player.my_soundbar' + name: + description: Name of the setting. + example: 'nightMode' + value: + description: Value to set. + example: 'on' diff --git a/homeassistant/components/sonos/.translations/da.json b/homeassistant/components/sonos/.translations/da.json index c303bca0aa8..c4b1a555245 100644 --- a/homeassistant/components/sonos/.translations/da.json +++ b/homeassistant/components/sonos/.translations/da.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_devices_found": "Ingen Sonos-enheder kunne findes p\u00e5 netv\u00e6rket.", + "no_devices_found": "Der blev ikke fundet nogen Sonos-enheder p\u00e5 netv\u00e6rket.", "single_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning af Sonos" }, "step": { diff --git a/homeassistant/components/sonos/.translations/ko.json b/homeassistant/components/sonos/.translations/ko.json index 4ca3d621599..931a0beadfc 100644 --- a/homeassistant/components/sonos/.translations/ko.json +++ b/homeassistant/components/sonos/.translations/ko.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "Sonos \ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "Sonos \ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "Sonos" } }, diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 2baa02d0a5d..9ce72d87dfe 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -1056,7 +1056,6 @@ class SonosEntity(MediaPlayerDevice): def restore(self): """Restore a snapshotted state to a player.""" try: - # pylint: disable=protected-access self._soco_snapshot.restore() except (TypeError, AttributeError, SoCoException) as ex: # Can happen if restoring a coordinator onto a current slave diff --git a/homeassistant/components/soundtouch/const.py b/homeassistant/components/soundtouch/const.py new file mode 100644 index 00000000000..37bf1d8cc2b --- /dev/null +++ b/homeassistant/components/soundtouch/const.py @@ -0,0 +1,6 @@ +"""Constants for the Bose Soundtouch component.""" +DOMAIN = "soundtouch" +SERVICE_PLAY_EVERYWHERE = "play_everywhere" +SERVICE_CREATE_ZONE = "create_zone" +SERVICE_ADD_ZONE_SLAVE = "add_zone_slave" +SERVICE_REMOVE_ZONE_SLAVE = "remove_zone_slave" diff --git a/homeassistant/components/soundtouch/media_player.py b/homeassistant/components/soundtouch/media_player.py index f613ba22dfa..4a0f6b55b22 100644 --- a/homeassistant/components/soundtouch/media_player.py +++ b/homeassistant/components/soundtouch/media_player.py @@ -2,11 +2,11 @@ import logging import re +from libsoundtouch import soundtouch_device import voluptuous as vol from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( - DOMAIN, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, @@ -29,12 +29,15 @@ from homeassistant.const import ( ) import homeassistant.helpers.config_validation as cv -_LOGGER = logging.getLogger(__name__) +from .const import ( + DOMAIN, + SERVICE_ADD_ZONE_SLAVE, + SERVICE_CREATE_ZONE, + SERVICE_PLAY_EVERYWHERE, + SERVICE_REMOVE_ZONE_SLAVE, +) -SERVICE_PLAY_EVERYWHERE = "soundtouch_play_everywhere" -SERVICE_CREATE_ZONE = "soundtouch_create_zone" -SERVICE_ADD_ZONE_SLAVE = "soundtouch_add_zone_slave" -SERVICE_REMOVE_ZONE_SLAVE = "soundtouch_remove_zone_slave" +_LOGGER = logging.getLogger(__name__) MAP_STATUS = { "PLAY_STATE": STATE_PLAYING, @@ -98,9 +101,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return remote_config = {"id": "ha.component.soundtouch", "host": host, "port": port} - soundtouch_device = SoundTouchDevice(None, remote_config) - hass.data[DATA_SOUNDTOUCH].append(soundtouch_device) - add_entities([soundtouch_device]) + bose_soundtouch_entity = SoundTouchDevice(None, remote_config) + hass.data[DATA_SOUNDTOUCH].append(bose_soundtouch_entity) + add_entities([bose_soundtouch_entity]) else: name = config.get(CONF_NAME) remote_config = { @@ -108,9 +111,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): "port": config.get(CONF_PORT), "host": config.get(CONF_HOST), } - soundtouch_device = SoundTouchDevice(name, remote_config) - hass.data[DATA_SOUNDTOUCH].append(soundtouch_device) - add_entities([soundtouch_device]) + bose_soundtouch_entity = SoundTouchDevice(name, remote_config) + hass.data[DATA_SOUNDTOUCH].append(bose_soundtouch_entity) + add_entities([bose_soundtouch_entity]) def service_handle(service): """Handle the applying of a service.""" @@ -182,7 +185,6 @@ class SoundTouchDevice(MediaPlayerDevice): def __init__(self, name, config): """Create Soundtouch Entity.""" - from libsoundtouch import soundtouch_device self._device = soundtouch_device(config["host"], config["port"]) if name is None: diff --git a/homeassistant/components/soundtouch/services.yaml b/homeassistant/components/soundtouch/services.yaml index e69de29bb2d..fd848b76b2d 100644 --- a/homeassistant/components/soundtouch/services.yaml +++ b/homeassistant/components/soundtouch/services.yaml @@ -0,0 +1,36 @@ +play_everywhere: + description: Play on all Bose Soundtouch devices. + fields: + master: + description: Name of the master entity that will coordinate the grouping. Platform dependent. It is a shortcut for creating a multi-room zone with all devices + example: 'media_player.soundtouch_home' + +create_zone: + description: Create a Sountouch multi-room zone. + fields: + master: + description: Name of the master entity that will coordinate the multi-room zone. Platform dependent. + example: 'media_player.soundtouch_home' + slaves: + description: Name of slaves entities to add to the new zone. + example: 'media_player.soundtouch_bedroom' + +add_zone_slave: + description: Add a slave to a Sountouch multi-room zone. + fields: + master: + description: Name of the master entity that is coordinating the multi-room zone. Platform dependent. + example: 'media_player.soundtouch_home' + slaves: + description: Name of slaves entities to add to the existing zone. + example: 'media_player.soundtouch_bedroom' + +remove_zone_slave: + description: Remove a slave from the Sounttouch multi-room zone. + fields: + master: + description: Name of the master entity that is coordinating the multi-room zone. Platform dependent. + example: 'media_player.soundtouch_home' + slaves: + description: Name of slaves entities to remove from the existing zone. + example: 'media_player.soundtouch_bedroom' diff --git a/homeassistant/components/spc/__init__.py b/homeassistant/components/spc/__init__.py index b5db4b685ae..1601090463f 100644 --- a/homeassistant/components/spc/__init__.py +++ b/homeassistant/components/spc/__init__.py @@ -1,11 +1,14 @@ """Support for Vanderbilt (formerly Siemens) SPC alarm systems.""" import logging +from pyspcwebgw import SpcWebGateway +from pyspcwebgw.area import Area +from pyspcwebgw.zone import Zone import voluptuous as vol -from homeassistant.helpers import discovery, aiohttp_client -from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers import aiohttp_client, discovery import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_send _LOGGER = logging.getLogger(__name__) @@ -33,11 +36,8 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass, config): """Set up the SPC component.""" - from pyspcwebgw import SpcWebGateway async def async_upate_callback(spc_object): - from pyspcwebgw.area import Area - from pyspcwebgw.zone import Zone if isinstance(spc_object, Area): async_dispatcher_send(hass, SIGNAL_UPDATE_ALARM.format(spc_object.id)) diff --git a/homeassistant/components/spc/alarm_control_panel.py b/homeassistant/components/spc/alarm_control_panel.py index 8eeccc06515..ca5d77b2a82 100644 --- a/homeassistant/components/spc/alarm_control_panel.py +++ b/homeassistant/components/spc/alarm_control_panel.py @@ -1,7 +1,14 @@ """Support for Vanderbilt (formerly Siemens) SPC alarm systems.""" import logging +from pyspcwebgw.const import AreaMode + import homeassistant.components.alarm_control_panel as alarm +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, +) from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, @@ -19,7 +26,6 @@ _LOGGER = logging.getLogger(__name__) def _get_alarm_state(area): """Get the alarm state.""" - from pyspcwebgw.const import AreaMode if area.verified_alarm: return STATE_ALARM_TRIGGERED @@ -80,26 +86,27 @@ class SpcAlarm(alarm.AlarmControlPanel): """Return the state of the device.""" return _get_alarm_state(self._area) + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT + async def async_alarm_disarm(self, code=None): """Send disarm command.""" - from pyspcwebgw.const import AreaMode await self._api.change_mode(area=self._area, new_mode=AreaMode.UNSET) async def async_alarm_arm_home(self, code=None): """Send arm home command.""" - from pyspcwebgw.const import AreaMode await self._api.change_mode(area=self._area, new_mode=AreaMode.PART_SET_A) async def async_alarm_arm_night(self, code=None): """Send arm home command.""" - from pyspcwebgw.const import AreaMode await self._api.change_mode(area=self._area, new_mode=AreaMode.PART_SET_B) async def async_alarm_arm_away(self, code=None): """Send arm away command.""" - from pyspcwebgw.const import AreaMode await self._api.change_mode(area=self._area, new_mode=AreaMode.FULL_SET) diff --git a/homeassistant/components/spc/binary_sensor.py b/homeassistant/components/spc/binary_sensor.py index 1ce02af390f..2104f931c0a 100644 --- a/homeassistant/components/spc/binary_sensor.py +++ b/homeassistant/components/spc/binary_sensor.py @@ -1,6 +1,8 @@ """Support for Vanderbilt (formerly Siemens) SPC alarm systems.""" import logging +from pyspcwebgw.const import ZoneInput + from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -60,7 +62,6 @@ class SpcBinarySensor(BinarySensorDevice): @property def is_on(self): """Whether the device is switched on.""" - from pyspcwebgw.const import ZoneInput return self._zone.input == ZoneInput.OPEN diff --git a/homeassistant/components/speedtestdotnet/manifest.json b/homeassistant/components/speedtestdotnet/manifest.json index b32026d86ef..821d4158f57 100644 --- a/homeassistant/components/speedtestdotnet/manifest.json +++ b/homeassistant/components/speedtestdotnet/manifest.json @@ -6,5 +6,7 @@ "speedtest-cli==2.1.2" ], "dependencies": [], - "codeowners": [] + "codeowners": [ + "@rohankapoorcom" + ] } diff --git a/homeassistant/components/spider/__init__.py b/homeassistant/components/spider/__init__.py index 0d5e1606b53..125799b394a 100644 --- a/homeassistant/components/spider/__init__.py +++ b/homeassistant/components/spider/__init__.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +from spiderpy.spiderapi import SpiderApi, UnauthorizedException import voluptuous as vol from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME @@ -32,8 +33,6 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): """Set up Spider Component.""" - from spiderpy.spiderapi import SpiderApi - from spiderpy.spiderapi import UnauthorizedException username = config[DOMAIN][CONF_USERNAME] password = config[DOMAIN][CONF_PASSWORD] diff --git a/homeassistant/components/splunk/__init__.py b/homeassistant/components/splunk/__init__.py index c483d7fae87..1d5d39416a3 100644 --- a/homeassistant/components/splunk/__init__.py +++ b/homeassistant/components/splunk/__init__.py @@ -7,12 +7,12 @@ import requests import voluptuous as vol from homeassistant.const import ( - CONF_SSL, - CONF_VERIFY_SSL, CONF_HOST, CONF_NAME, CONF_PORT, + CONF_SSL, CONF_TOKEN, + CONF_VERIFY_SSL, EVENT_STATE_CHANGED, ) from homeassistant.helpers import state as state_helper diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index 236c8b8db89..ba0c725eb7f 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -37,7 +37,7 @@ CONF_CLIENT_ID = "client_id" CONF_CLIENT_SECRET = "client_secret" CONFIGURATOR_DESCRIPTION = ( - "To link your Spotify account, " "click the link, login, and authorize:" + "To link your Spotify account, click the link, login, and authorize:" ) CONFIGURATOR_LINK_NAME = "Link Spotify account" CONFIGURATOR_SUBMIT_CAPTION = "I authorized successfully" @@ -307,7 +307,7 @@ class SpotifyMediaPlayer(MediaPlayerDevice): def play_playlist(self, media_id, random_song): """Play random music in a playlist.""" - if not media_id.startswith("spotify:playlist:"): + if not media_id.startswith("spotify:"): _LOGGER.error("media id must be spotify playlist uri") return kwargs = {"context_uri": media_id} diff --git a/homeassistant/components/sql/manifest.json b/homeassistant/components/sql/manifest.json index 39435524c20..3434b154e29 100644 --- a/homeassistant/components/sql/manifest.json +++ b/homeassistant/components/sql/manifest.json @@ -2,11 +2,7 @@ "domain": "sql", "name": "Sql", "documentation": "https://www.home-assistant.io/integrations/sql", - "requirements": [ - "sqlalchemy==1.3.11" - ], + "requirements": ["sqlalchemy==1.3.12"], "dependencies": [], - "codeowners": [ - "@dgomes" - ] -} \ No newline at end of file + "codeowners": ["@dgomes"] +} diff --git a/homeassistant/components/squeezebox/const.py b/homeassistant/components/squeezebox/const.py new file mode 100644 index 00000000000..1e8fd6f3a2a --- /dev/null +++ b/homeassistant/components/squeezebox/const.py @@ -0,0 +1,3 @@ +"""Constants for the Squeezebox component.""" +DOMAIN = "squeezebox" +SERVICE_CALL_METHOD = "call_method" diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index 8e03763b709..94c497e4db6 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -12,7 +12,6 @@ import voluptuous as vol from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( ATTR_MEDIA_ENQUEUE, - DOMAIN, MEDIA_TYPE_MUSIC, SUPPORT_CLEAR_PLAYLIST, SUPPORT_NEXT_TRACK, @@ -39,11 +38,13 @@ from homeassistant.const import ( STATE_PAUSED, STATE_PLAYING, ) +from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.exceptions import PlatformNotReady from homeassistant.util.dt import utcnow +from .const import DOMAIN, SERVICE_CALL_METHOD + _LOGGER = logging.getLogger(__name__) DEFAULT_PORT = 9000 @@ -75,8 +76,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( } ) -SERVICE_CALL_METHOD = "squeezebox_call_method" - DATA_SQUEEZEBOX = "squeezebox" KNOWN_SERVERS = "squeezebox_known_servers" @@ -539,11 +538,11 @@ class SqueezeBoxDevice(MediaPlayerDevice): """ Call Squeezebox JSON/RPC method. - Escaped optional parameters are added to the command to form the list - of positional parameters (p0, p1..., pN) passed to JSON/RPC server. + Additional parameters are added to the command to form the list of + positional parameters (p0, p1..., pN) passed to JSON/RPC server. """ all_params = [command] if parameters: for parameter in parameters: - all_params.append(urllib.parse.quote(parameter, safe="+:=/?")) + all_params.append(parameter) return self.async_query(*all_params) diff --git a/homeassistant/components/squeezebox/services.yaml b/homeassistant/components/squeezebox/services.yaml index 05c7de07f42..0c81c369e73 100644 --- a/homeassistant/components/squeezebox/services.yaml +++ b/homeassistant/components/squeezebox/services.yaml @@ -1,4 +1,4 @@ -squeezebox_call_method: +call_method: description: Call a custom Squeezebox JSONRPC API. fields: entity_id: @@ -10,4 +10,3 @@ squeezebox_call_method: parameters: description: Array of additional parameters to pass to Logitech Media Server (p1, ..., pN in the CLI documentation). example: ["loadtracks", "album.titlesearch="] - diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index c4d71e0febd..94e256f0523 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -2,31 +2,30 @@ import asyncio from datetime import timedelta import logging -from urllib.parse import urlparse -from xml.etree import ElementTree import aiohttp +from defusedxml import ElementTree from netdisco import ssdp, util -from homeassistant.helpers.event import async_track_time_interval from homeassistant.generated.ssdp import SSDP +from homeassistant.helpers.event import async_track_time_interval DOMAIN = "ssdp" SCAN_INTERVAL = timedelta(seconds=60) -ATTR_HOST = "host" -ATTR_PORT = "port" -ATTR_SSDP_DESCRIPTION = "ssdp_description" -ATTR_ST = "ssdp_st" -ATTR_NAME = "name" -ATTR_MODEL_NAME = "model_name" -ATTR_MODEL_NUMBER = "model_number" -ATTR_SERIAL = "serial_number" -ATTR_MANUFACTURER = "manufacturer" -ATTR_MANUFACTURERURL = "manufacturerURL" -ATTR_UDN = "udn" -ATTR_UPNP_DEVICE_TYPE = "upnp_device_type" -ATTR_PRESENTATIONURL = "presentation_url" +# Attributes for accessing info from SSDP response +ATTR_SSDP_LOCATION = "ssdp_location" +ATTR_SSDP_ST = "ssdp_st" +# Attributes for accessing info from retrieved UPnP device description +ATTR_UPNP_DEVICE_TYPE = "deviceType" +ATTR_UPNP_FRIENDLY_NAME = "friendlyName" +ATTR_UPNP_MANUFACTURER = "manufacturer" +ATTR_UPNP_MANUFACTURER_URL = "manufacturerURL" +ATTR_UPNP_MODEL_NAME = "modelName" +ATTR_UPNP_MODEL_NUMBER = "modelNumber" +ATTR_UPNP_PRESENTATION_URL = "presentationURL" +ATTR_UPNP_SERIAL = "serialNumber" +ATTR_UPNP_UDN = "UDN" _LOGGER = logging.getLogger(__name__) @@ -157,24 +156,12 @@ class Scanner: def info_from_entry(entry, device_info): - """Get most important info from an entry.""" - url = urlparse(entry.location) + """Get info from an entry.""" info = { - ATTR_HOST: url.hostname, - ATTR_PORT: url.port, - ATTR_SSDP_DESCRIPTION: entry.location, - ATTR_ST: entry.st, + ATTR_SSDP_LOCATION: entry.location, + ATTR_SSDP_ST: entry.st, } - if device_info: - info[ATTR_NAME] = device_info.get("friendlyName") - info[ATTR_MODEL_NAME] = device_info.get("modelName") - info[ATTR_MODEL_NUMBER] = device_info.get("modelNumber") - info[ATTR_SERIAL] = device_info.get("serialNumber") - info[ATTR_MANUFACTURER] = device_info.get("manufacturer") - info[ATTR_MANUFACTURERURL] = device_info.get("manufacturerURL") - info[ATTR_UDN] = device_info.get("UDN") - info[ATTR_UPNP_DEVICE_TYPE] = device_info.get("deviceType") - info[ATTR_PRESENTATIONURL] = device_info.get("presentationURL") + info.update(device_info) return info diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index 1c3d56fe7fe..1a6bfa36233 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -3,6 +3,7 @@ "name": "SSDP", "documentation": "https://www.home-assistant.io/integrations/ssdp", "requirements": [ + "defusedxml==0.6.0", "netdisco==2.6.0" ], "dependencies": [ diff --git a/homeassistant/components/starline/.translations/bg.json b/homeassistant/components/starline/.translations/bg.json new file mode 100644 index 00000000000..702c061c629 --- /dev/null +++ b/homeassistant/components/starline/.translations/bg.json @@ -0,0 +1,42 @@ +{ + "config": { + "error": { + "error_auth_app": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u043d\u043e ID \u043d\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0438\u043b\u0438 \u0442\u0430\u0439\u043d\u0430", + "error_auth_mfa": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u0435\u043d \u043a\u043e\u0434", + "error_auth_user": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u043d\u043e \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435 \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u0430" + }, + "step": { + "auth_app": { + "data": { + "app_id": "ID \u043d\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435", + "app_secret": "\u0422\u0430\u0439\u043d\u0430" + }, + "description": "\u0418\u0414 \u043d\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0438 \u0442\u0430\u0435\u043d \u043a\u043e\u0434 \u043e\u0442 StarLine \u0430\u043a\u0430\u0443\u043d\u0442 \u043d\u0430 \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a", + "title": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u043e\u043d\u043d\u0438 \u0434\u0430\u043d\u043d\u0438 \u0437\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0442\u043e" + }, + "auth_captcha": { + "data": { + "captcha_code": "\u041a\u043e\u0434 \u043e\u0442 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435\u0442\u043e" + }, + "description": "{captcha_img}", + "title": "Captcha" + }, + "auth_mfa": { + "data": { + "mfa_code": "SMS \u043a\u043e\u0434" + }, + "description": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043a\u043e\u0434\u0430, \u0438\u0437\u043f\u0440\u0430\u0442\u0435\u043d \u043d\u0430 \u0442\u0435\u043b\u0435\u0444\u043e\u043d\u0435\u043d \u043d\u043e\u043c\u0435\u0440 {phone_number}", + "title": "\u0414\u0432\u0443\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430 \u043e\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f" + }, + "auth_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" + }, + "description": "\u0418\u043c\u0435\u0439\u043b \u0438 \u043f\u0430\u0440\u043e\u043b\u0430 \u0437\u0430 \u0430\u043a\u0430\u0443\u043d\u0442 \u0432 StarLine", + "title": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u0438 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u043e\u043d\u043d\u0438 \u0434\u0430\u043d\u043d\u0438" + } + }, + "title": "StarLine" + } +} \ No newline at end of file diff --git a/homeassistant/components/starline/.translations/ca.json b/homeassistant/components/starline/.translations/ca.json new file mode 100644 index 00000000000..72cf1a66760 --- /dev/null +++ b/homeassistant/components/starline/.translations/ca.json @@ -0,0 +1,42 @@ +{ + "config": { + "error": { + "error_auth_app": "ID d'aplicaci\u00f3 o secret incorrectes", + "error_auth_mfa": "Codi incorrecte", + "error_auth_user": "Nom d'usuari o contrasenya incorrectes" + }, + "step": { + "auth_app": { + "data": { + "app_id": "ID d'aplicaci\u00f3", + "app_secret": "Secret" + }, + "description": "ID d'aplicaci\u00f3 i codi secret de compte de desenvolupador de StarLine", + "title": "Credencials d'aplicaci\u00f3" + }, + "auth_captcha": { + "data": { + "captcha_code": "Codi des de imatge" + }, + "description": "{captcha_img}", + "title": "Captcha" + }, + "auth_mfa": { + "data": { + "mfa_code": "Codi SMS" + }, + "description": "Introdueix el codi rebut al n\u00famero {phone_number}", + "title": "Verificaci\u00f3 en dos passos" + }, + "auth_user": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari" + }, + "description": "Correu electr\u00f2nic i contrasenya del compte StarLine", + "title": "Credencials d\u2019usuari" + } + }, + "title": "StarLine" + } +} \ No newline at end of file diff --git a/homeassistant/components/starline/.translations/da.json b/homeassistant/components/starline/.translations/da.json new file mode 100644 index 00000000000..2a8cbcf1270 --- /dev/null +++ b/homeassistant/components/starline/.translations/da.json @@ -0,0 +1,42 @@ +{ + "config": { + "error": { + "error_auth_app": "Forkert applikations-id eller hemmelighed", + "error_auth_mfa": "Forkert kode", + "error_auth_user": "Forkert brugernavn eller adgangskode" + }, + "step": { + "auth_app": { + "data": { + "app_id": "App-id", + "app_secret": "Hemmelighed" + }, + "description": "Applikations-id og hemmelig kode fra StarLine-udviklerkonto ", + "title": "Applikations-legitimationsoplysninger" + }, + "auth_captcha": { + "data": { + "captcha_code": "Kode fra billede" + }, + "description": "{captcha_img}", + "title": "Captcha" + }, + "auth_mfa": { + "data": { + "mfa_code": "SMS-kode" + }, + "description": "Indtast koden, der er sendt til telefon {phone_number}", + "title": "Tofaktor-godkendelse" + }, + "auth_user": { + "data": { + "password": "Adgangskode", + "username": "Brugernavn" + }, + "description": "StarLine-konto email og adgangskode", + "title": "Brugeroplysninger" + } + }, + "title": "StarLine" + } +} \ No newline at end of file diff --git a/homeassistant/components/starline/.translations/de.json b/homeassistant/components/starline/.translations/de.json new file mode 100644 index 00000000000..657e6c08b1a --- /dev/null +++ b/homeassistant/components/starline/.translations/de.json @@ -0,0 +1,25 @@ +{ + "config": { + "error": { + "error_auth_mfa": "Ung\u00fcltiger Code" + }, + "step": { + "auth_captcha": { + "title": "Captcha" + }, + "auth_mfa": { + "data": { + "mfa_code": "SMS Code" + }, + "title": "2-Faktor-Authentifizierung" + }, + "auth_user": { + "data": { + "password": "Passwort", + "username": "Benutzername" + }, + "title": "Anmeldeinformationen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/starline/.translations/en.json b/homeassistant/components/starline/.translations/en.json new file mode 100644 index 00000000000..afe8f8c732b --- /dev/null +++ b/homeassistant/components/starline/.translations/en.json @@ -0,0 +1,42 @@ +{ + "config": { + "error": { + "error_auth_app": "Incorrect application id or secret", + "error_auth_mfa": "Incorrect code", + "error_auth_user": "Incorrect username or password" + }, + "step": { + "auth_app": { + "data": { + "app_id": "App ID", + "app_secret": "Secret" + }, + "description": "Application ID and secret code from StarLine developer account", + "title": "Application credentials" + }, + "auth_captcha": { + "data": { + "captcha_code": "Code from image" + }, + "description": "{captcha_img}", + "title": "Captcha" + }, + "auth_mfa": { + "data": { + "mfa_code": "SMS code" + }, + "description": "Enter the code sent to phone {phone_number}", + "title": "Two-factor authorization" + }, + "auth_user": { + "data": { + "password": "Password", + "username": "Username" + }, + "description": "StarLine account email and password", + "title": "User credentials" + } + }, + "title": "StarLine" + } +} \ No newline at end of file diff --git a/homeassistant/components/starline/.translations/es.json b/homeassistant/components/starline/.translations/es.json new file mode 100644 index 00000000000..bc881ced6a2 --- /dev/null +++ b/homeassistant/components/starline/.translations/es.json @@ -0,0 +1,42 @@ +{ + "config": { + "error": { + "error_auth_app": "Id de aplicaci\u00f3n o secreto incorrectos", + "error_auth_mfa": "C\u00f3digo incorrecto", + "error_auth_user": "Nombre de usuario o contrase\u00f1a incorrectos" + }, + "step": { + "auth_app": { + "data": { + "app_id": "ID de la aplicaci\u00f3n", + "app_secret": "Secreto" + }, + "description": "ID de la aplicaci\u00f3n y c\u00f3digo secreto de la cuenta de desarrollador de StarLine", + "title": "Credenciales de la aplicaci\u00f3n" + }, + "auth_captcha": { + "data": { + "captcha_code": "C\u00f3digo de la imagen" + }, + "description": "{captcha_img}", + "title": "Captcha" + }, + "auth_mfa": { + "data": { + "mfa_code": "C\u00f3digo SMS" + }, + "description": "Introduce el c\u00f3digo enviado al tel\u00e9fono {phone_number}", + "title": "Autorizaci\u00f3n de dos factores" + }, + "auth_user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Usuario" + }, + "description": "Correo electr\u00f3nico y contrase\u00f1a de la cuenta StarLine", + "title": "Credenciales de usuario" + } + }, + "title": "StarLine" + } +} \ No newline at end of file diff --git a/homeassistant/components/starline/.translations/fr.json b/homeassistant/components/starline/.translations/fr.json new file mode 100644 index 00000000000..d15f5c37edf --- /dev/null +++ b/homeassistant/components/starline/.translations/fr.json @@ -0,0 +1,42 @@ +{ + "config": { + "error": { + "error_auth_app": "ID applicatif ou code secret incorrect", + "error_auth_mfa": "code incorrect", + "error_auth_user": "identifiant ou mot de passe incorrect" + }, + "step": { + "auth_app": { + "data": { + "app_id": "ID de l'application", + "app_secret": "Secret" + }, + "description": "ID applicatif et code secret du compte d\u00e9veloppeur StarLine", + "title": "Informations d'identification de l'application" + }, + "auth_captcha": { + "data": { + "captcha_code": "Code de l'image" + }, + "description": "{captcha_img}", + "title": "Captcha" + }, + "auth_mfa": { + "data": { + "mfa_code": "Code SMS" + }, + "description": "Entrez le code envoy\u00e9 au t\u00e9l\u00e9phone {phone_number}", + "title": "Autorisation \u00e0 deux facteurs" + }, + "auth_user": { + "data": { + "password": "Mot de passe", + "username": "Nom d'utilisateur" + }, + "description": "Adresse e-mail et mot de passe du compte StarLine", + "title": "Informations d'identification de l'utilisateur" + } + }, + "title": "StarLine" + } +} \ No newline at end of file diff --git a/homeassistant/components/starline/.translations/it.json b/homeassistant/components/starline/.translations/it.json new file mode 100644 index 00000000000..f68732354c6 --- /dev/null +++ b/homeassistant/components/starline/.translations/it.json @@ -0,0 +1,42 @@ +{ + "config": { + "error": { + "error_auth_app": "ID applicazione o Segreto errati", + "error_auth_mfa": "Codice errato", + "error_auth_user": "Nome utente o password errati" + }, + "step": { + "auth_app": { + "data": { + "app_id": "ID applicazione", + "app_secret": "Segreto" + }, + "description": "ID applicazione e codice segreto da Account sviluppatore StarLine ", + "title": "Credenziali dell'applicazione" + }, + "auth_captcha": { + "data": { + "captcha_code": "Codice dall'immagine" + }, + "description": "{captcha_img}", + "title": "Captcha" + }, + "auth_mfa": { + "data": { + "mfa_code": "Codice SMS" + }, + "description": "Inserisci il codice inviato al telefono {phone_number}.", + "title": "Autenticazione a due fattori" + }, + "auth_user": { + "data": { + "password": "Password", + "username": "Nome utente" + }, + "description": "Email e password dell'account StarLine", + "title": "Credenziali utente" + } + }, + "title": "StarLine" + } +} \ No newline at end of file diff --git a/homeassistant/components/starline/.translations/ko.json b/homeassistant/components/starline/.translations/ko.json new file mode 100644 index 00000000000..4d7ecf427f8 --- /dev/null +++ b/homeassistant/components/starline/.translations/ko.json @@ -0,0 +1,42 @@ +{ + "config": { + "error": { + "error_auth_app": "\uc560\ud50c\ub9ac\ucf00\uc774\uc158 ID \ud639\uc740 \ubcf4\uc548\ud0a4\uac00 \uc77c\uce58\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", + "error_auth_mfa": "\ube44\ubc00\ubc88\ud638\uac00 \uc77c\uce58\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", + "error_auth_user": "\uc0ac\uc6a9\uc790 \uc774\ub984 \ub610\ub294 \ube44\ubc00\ubc88\ud638\uac00 \uc77c\uce58\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4" + }, + "step": { + "auth_app": { + "data": { + "app_id": "\uc560\ud50c\ub9ac\ucf00\uc774\uc158 ID", + "app_secret": "\ubcf4\uc548\ud0a4" + }, + "description": "StarLine \uac1c\ubc1c\uc790 \uacc4\uc815\uc758 \uc560\ud50c\ub9ac\ucf00\uc774\uc158 ID \ubc0f \ube44\ubc00\ubc88\ud638", + "title": "\uc560\ud50c\ub9ac\ucf00\uc774\uc158 \uc790\uaca9 \uc99d\uba85" + }, + "auth_captcha": { + "data": { + "captcha_code": "\uc774\ubbf8\uc9c0\uc758 \ucf54\ub4dc" + }, + "description": "{captcha_img}", + "title": "\ubcf4\uc548 \ubb38\uc790" + }, + "auth_mfa": { + "data": { + "mfa_code": "SMS \ucf54\ub4dc" + }, + "description": "{phone_number} \uc804\ud654\ub85c \uc804\uc1a1\ub41c \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694", + "title": "2\ub2e8\uacc4 \uc778\uc99d" + }, + "auth_user": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + }, + "description": "StarLine \uacc4\uc815 \uc774\uba54\uc77c \ubc0f \ube44\ubc00\ubc88\ud638", + "title": "\uc0ac\uc6a9\uc790 \uc790\uaca9 \uc99d\uba85" + } + }, + "title": "StarLine" + } +} \ No newline at end of file diff --git a/homeassistant/components/starline/.translations/lb.json b/homeassistant/components/starline/.translations/lb.json new file mode 100644 index 00000000000..527add9920b --- /dev/null +++ b/homeassistant/components/starline/.translations/lb.json @@ -0,0 +1,42 @@ +{ + "config": { + "error": { + "error_auth_app": "Ong\u00ebltege Applikatioun's ID oder Schl\u00ebssel", + "error_auth_mfa": "Ong\u00ebltegte Code", + "error_auth_user": "Ong\u00ebltege Benotzernumm oder Passwuert" + }, + "step": { + "auth_app": { + "data": { + "app_id": "App ID", + "app_secret": "Schl\u00ebssel" + }, + "description": "Applikatioun's ID an Schl\u00ebssel vum StarLine Developpeur's Kont", + "title": "Login Informatioune vun der Applikatioun" + }, + "auth_captcha": { + "data": { + "captcha_code": "Cod vum Bild" + }, + "description": "{captcha_img}", + "title": "Captcha" + }, + "auth_mfa": { + "data": { + "mfa_code": "SMS Code" + }, + "description": "Gitt de Code an deen un d'Telefondnummer {phone_number} gesch\u00e9ckt gouf", + "title": "2-Faktor-Authentifikatioun" + }, + "auth_user": { + "data": { + "password": "Passwuert", + "username": "Benotzernumm" + }, + "description": "StarLine Konto Email a Passwuert", + "title": "Login Informatiounen" + } + }, + "title": "StarLine" + } +} \ No newline at end of file diff --git a/homeassistant/components/starline/.translations/nl.json b/homeassistant/components/starline/.translations/nl.json new file mode 100644 index 00000000000..d7963446865 --- /dev/null +++ b/homeassistant/components/starline/.translations/nl.json @@ -0,0 +1,42 @@ +{ + "config": { + "error": { + "error_auth_app": "Onjuiste applicatie-ID of geheim", + "error_auth_mfa": "Ongeldige code", + "error_auth_user": "Ongeldige gebruikersnaam of wachtwoord" + }, + "step": { + "auth_app": { + "data": { + "app_id": "Toepassings-ID ", + "app_secret": "Geheime code" + }, + "description": "Toepassings-ID en de geheime code van StarLine developer account", + "title": "Inloggegevens van de applicatie" + }, + "auth_captcha": { + "data": { + "captcha_code": "Code van afbeelding" + }, + "description": "{captcha_img}", + "title": "Captcha" + }, + "auth_mfa": { + "data": { + "mfa_code": "SMS code" + }, + "description": "Voer de code in die wordt verzonden naar telefoon {phone_number}", + "title": "Tweestapsverificatie" + }, + "auth_user": { + "data": { + "password": "Wachtwoord", + "username": "Gebruikersnaam" + }, + "description": "StarLine-account e-mailadres en wachtwoord", + "title": "Gebruikersgegevens" + } + }, + "title": "StarLine" + } +} \ No newline at end of file diff --git a/homeassistant/components/starline/.translations/nn.json b/homeassistant/components/starline/.translations/nn.json new file mode 100644 index 00000000000..88b146144fb --- /dev/null +++ b/homeassistant/components/starline/.translations/nn.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "auth_captcha": { + "description": "{captcha_img}" + } + }, + "title": "StarLine" + } +} \ No newline at end of file diff --git a/homeassistant/components/starline/.translations/no.json b/homeassistant/components/starline/.translations/no.json new file mode 100644 index 00000000000..37d55aea194 --- /dev/null +++ b/homeassistant/components/starline/.translations/no.json @@ -0,0 +1,42 @@ +{ + "config": { + "error": { + "error_auth_app": "Feil applikasjons-ID eller hemmelighet", + "error_auth_mfa": "Feil kode", + "error_auth_user": "Ugyldig brukernavn eller passord" + }, + "step": { + "auth_app": { + "data": { + "app_id": "App-ID", + "app_secret": "Hemmelig" + }, + "description": "S\u00f8knads-ID og hemmelig kode fra StarLine utviklerkonto ", + "title": "Bruksanvisning" + }, + "auth_captcha": { + "data": { + "captcha_code": "Kode fra bilde" + }, + "description": "{captcha_img}", + "title": "Captcha" + }, + "auth_mfa": { + "data": { + "mfa_code": "SMS-kode" + }, + "description": "Skriv inn koden som er sendt til telefonen {phone_number}", + "title": "Tofaktorautentisering" + }, + "auth_user": { + "data": { + "password": "Passord", + "username": "Brukernavn" + }, + "description": "E-postadresse og passord for StarLine-kontoen", + "title": "Brukerlegitimasjon" + } + }, + "title": "StarLine" + } +} \ No newline at end of file diff --git a/homeassistant/components/starline/.translations/pl.json b/homeassistant/components/starline/.translations/pl.json new file mode 100644 index 00000000000..71d54bbbcd1 --- /dev/null +++ b/homeassistant/components/starline/.translations/pl.json @@ -0,0 +1,42 @@ +{ + "config": { + "error": { + "error_auth_app": "Niepoprawny identyfikator aplikacji lub tajny kod", + "error_auth_mfa": "Niepoprawny kod", + "error_auth_user": "Niepoprawna nazwa u\u017cytkownika lub has\u0142o" + }, + "step": { + "auth_app": { + "data": { + "app_id": "Identyfikator aplikacji", + "app_secret": "Tajny kod" + }, + "description": "Identyfikator aplikacji i tajny kod z konta programisty StarLine", + "title": "Po\u015bwiadczenia aplikacji" + }, + "auth_captcha": { + "data": { + "captcha_code": "Kod z obrazka" + }, + "description": "{captcha_img}", + "title": "Captcha" + }, + "auth_mfa": { + "data": { + "mfa_code": "Kod SMS" + }, + "description": "Wprowad\u017a kod wys\u0142any na numer telefonu {phone_number}", + "title": "Uwierzytelnianie dwusk\u0142adnikowe" + }, + "auth_user": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + }, + "description": "Adres e-mail i has\u0142o do konta StarLine", + "title": "Po\u015bwiadczenia u\u017cytkownika" + } + }, + "title": "StarLine" + } +} \ No newline at end of file diff --git a/homeassistant/components/starline/.translations/pt-BR.json b/homeassistant/components/starline/.translations/pt-BR.json new file mode 100644 index 00000000000..158c2b01cf9 --- /dev/null +++ b/homeassistant/components/starline/.translations/pt-BR.json @@ -0,0 +1,31 @@ +{ + "config": { + "error": { + "error_auth_mfa": "C\u00f3digo incorreto", + "error_auth_user": "Usu\u00e1rio ou senha incorretos" + }, + "step": { + "auth_app": { + "title": "Credenciais do aplicativo" + }, + "auth_captcha": { + "data": { + "captcha_code": "C\u00f3digo da imagem" + }, + "title": "Captcha" + }, + "auth_mfa": { + "data": { + "mfa_code": "C\u00f3digo SMS" + }, + "description": "Digite o c\u00f3digo enviado para o telefone {phone_number}", + "title": "Autoriza\u00e7\u00e3o de dois fatores" + }, + "auth_user": { + "data": { + "password": "Senha" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/starline/.translations/ru.json b/homeassistant/components/starline/.translations/ru.json new file mode 100644 index 00000000000..aa84c36772b --- /dev/null +++ b/homeassistant/components/starline/.translations/ru.json @@ -0,0 +1,42 @@ +{ + "config": { + "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." + }, + "step": { + "auth_app": { + "data": { + "app_id": "ID \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f", + "app_secret": "\u0421\u0435\u043a\u0440\u0435\u0442\u043d\u044b\u0439 \u043a\u043e\u0434" + }, + "description": "ID \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0438 \u0441\u0435\u043a\u0440\u0435\u0442\u043d\u044b\u0439 \u043a\u043e\u0434 \u0438\u0437 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0430 StarLine", + "title": "\u0423\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f" + }, + "auth_captcha": { + "data": { + "captcha_code": "\u041a\u043e\u0434 \u0441 \u043a\u0430\u0440\u0442\u0438\u043d\u043a\u0438" + }, + "description": "{captcha_img}", + "title": "CAPTCHA" + }, + "auth_mfa": { + "data": { + "mfa_code": "\u041a\u043e\u0434 \u0438\u0437 SMS" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434, \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u043d\u044b\u0439 \u043d\u0430 \u043d\u043e\u043c\u0435\u0440 \u0442\u0435\u043b\u0435\u0444\u043e\u043d\u0430 {phone_number}", + "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" + }, + "auth_user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u041b\u043e\u0433\u0438\u043d" + }, + "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" + } + }, + "title": "StarLine" + } +} \ No newline at end of file diff --git a/homeassistant/components/starline/.translations/sl.json b/homeassistant/components/starline/.translations/sl.json new file mode 100644 index 00000000000..3cdc5bc4bac --- /dev/null +++ b/homeassistant/components/starline/.translations/sl.json @@ -0,0 +1,42 @@ +{ + "config": { + "error": { + "error_auth_app": "Nepravilen ID ali skrivnost", + "error_auth_mfa": "Napa\u010dna koda", + "error_auth_user": "Nepravilno uporabni\u0161ko ime ali geslo" + }, + "step": { + "auth_app": { + "data": { + "app_id": "ID aplikacije", + "app_secret": "Skrivnost" + }, + "description": "ID aplikacije in tajna koda iz ra\u010duna za razvijalce StarLine ", + "title": "Poverilnice programa" + }, + "auth_captcha": { + "data": { + "captcha_code": "\u0160ifra iz slike" + }, + "description": "{captcha_img}", + "title": "Captcha" + }, + "auth_mfa": { + "data": { + "mfa_code": "SMS koda" + }, + "description": "Vnesite kodo, poslano na telefon {phone_number}", + "title": "2-faktorska avtorizacija" + }, + "auth_user": { + "data": { + "password": "Geslo", + "username": "Uporabni\u0161ko ime" + }, + "description": "StarLine e-po\u0161tni ra\u010dun in geslo", + "title": "Uporabni\u0161ke poverilnice" + } + }, + "title": "StarLine" + } +} \ No newline at end of file diff --git a/homeassistant/components/starline/.translations/zh-Hant.json b/homeassistant/components/starline/.translations/zh-Hant.json new file mode 100644 index 00000000000..0bd69d54ec6 --- /dev/null +++ b/homeassistant/components/starline/.translations/zh-Hant.json @@ -0,0 +1,42 @@ +{ + "config": { + "error": { + "error_auth_app": "\u61c9\u7528\u7a0b\u5f0f ID \u932f\u8aa4\u6216\u4e0d\u6b63\u78ba", + "error_auth_mfa": "\u5bc6\u78bc\u932f\u8aa4", + "error_auth_user": "\u4f7f\u7528\u8005\u540d\u7a31\u6216\u5bc6\u78bc\u932f\u8aa4" + }, + "step": { + "auth_app": { + "data": { + "app_id": "App ID", + "app_secret": "\u5bc6\u78bc" + }, + "description": "Application ID and secret code \u7531 StarLine \u958b\u767c\u8005\u5e33\u865f \u6240\u53d6\u5f97\u7684\u61c9\u7528\u7a0b\u5f0f ID \u8207\u5bc6\u78bc", + "title": "\u61c9\u7528\u6191\u8b49" + }, + "auth_captcha": { + "data": { + "captcha_code": "\u5716\u50cf\u5bc6\u78bc" + }, + "description": "{captcha_img}", + "title": "Captcha" + }, + "auth_mfa": { + "data": { + "mfa_code": "\u7c21\u8a0a\u5bc6\u78bc" + }, + "description": "\u8f38\u5165\u50b3\u9001\u81f3 {phone_number} \u7684\u9a57\u8b49\u78bc", + "title": "\u5169\u968e\u6bb5\u8a8d\u8b49" + }, + "auth_user": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "StarLine \u5e33\u865f\u90f5\u4ef6\u8207\u5bc6\u78bc", + "title": "\u4f7f\u7528\u8005\u6191\u8b49" + } + }, + "title": "StarLine" + } +} \ No newline at end of file diff --git a/homeassistant/components/starline/__init__.py b/homeassistant/components/starline/__init__.py new file mode 100644 index 00000000000..303507b1491 --- /dev/null +++ b/homeassistant/components/starline/__init__.py @@ -0,0 +1,86 @@ +"""The StarLine component.""" +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import Config, HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady + +from .account import StarlineAccount +from .const import ( + CONF_SCAN_INTERVAL, + DEFAULT_SCAN_INTERVAL, + DOMAIN, + PLATFORMS, + SERVICE_SET_SCAN_INTERVAL, + SERVICE_UPDATE_STATE, +) + + +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) + await account.update() + if not account.api.available: + raise ConfigEntryNotReady + + if DOMAIN not in hass.data: + hass.data[DOMAIN] = {} + hass.data[DOMAIN][config_entry.entry_id] = account + + device_registry = await hass.helpers.device_registry.async_get_registry() + for device in account.api.devices.values(): + device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, **account.device_info(device) + ) + + for domain in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, domain) + ) + + async def async_set_scan_interval(call): + """Service for set scan interval.""" + options = dict(config_entry.options) + options[CONF_SCAN_INTERVAL] = call.data[CONF_SCAN_INTERVAL] + hass.config_entries.async_update_entry(entry=config_entry, options=options) + + hass.services.async_register(DOMAIN, SERVICE_UPDATE_STATE, account.update) + hass.services.async_register( + DOMAIN, + SERVICE_SET_SCAN_INTERVAL, + async_set_scan_interval, + schema=vol.Schema( + { + vol.Required(CONF_SCAN_INTERVAL): vol.All( + vol.Coerce(int), vol.Range(min=10) + ) + } + ), + ) + + config_entry.add_update_listener(async_options_updated) + await async_options_updated(hass, config_entry) + + return True + + +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Unload a config entry.""" + for domain in PLATFORMS: + await hass.config_entries.async_forward_entry_unload(config_entry, domain) + + account: StarlineAccount = hass.data[DOMAIN][config_entry.entry_id] + account.unload() + return True + + +async def async_options_updated(hass: HomeAssistant, config_entry: ConfigEntry) -> None: + """Triggered by config entry options updates.""" + account: StarlineAccount = hass.data[DOMAIN][config_entry.entry_id] + scan_interval = config_entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) + account.set_update_interval(scan_interval) diff --git a/homeassistant/components/starline/account.py b/homeassistant/components/starline/account.py new file mode 100644 index 00000000000..8d0214d1b5c --- /dev/null +++ b/homeassistant/components/starline/account.py @@ -0,0 +1,143 @@ +"""StarLine Account.""" +from datetime import datetime, timedelta +from typing import Any, Callable, Dict, Optional + +from starline import StarlineApi, StarlineDevice + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.event import async_track_time_interval + +from .const import ( + DATA_EXPIRES, + DATA_SLID_TOKEN, + DATA_SLNET_TOKEN, + DATA_USER_ID, + DEFAULT_SCAN_INTERVAL, + DOMAIN, + LOGGER, +) + + +class StarlineAccount: + """StarLine Account class.""" + + def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry): + """Initialize StarLine account.""" + self._hass: HomeAssistant = hass + self._config_entry: ConfigEntry = config_entry + self._update_interval: int = DEFAULT_SCAN_INTERVAL + self._unsubscribe_auto_updater: Optional[Callable] = None + self._api: StarlineApi = StarlineApi( + config_entry.data[DATA_USER_ID], config_entry.data[DATA_SLNET_TOKEN] + ) + + def _check_slnet_token(self) -> None: + """Check SLNet token expiration and update if needed.""" + now = datetime.now().timestamp() + slnet_token_expires = self._config_entry.data[DATA_EXPIRES] + + if now + self._update_interval > slnet_token_expires: + self._update_slnet_token() + + def _update_slnet_token(self) -> None: + """Update SLNet token.""" + slid_token = self._config_entry.data[DATA_SLID_TOKEN] + + try: + slnet_token, slnet_token_expires, user_id = self._api.get_user_id( + slid_token + ) + self._api.set_slnet_token(slnet_token) + self._api.set_user_id(user_id) + self._hass.config_entries.async_update_entry( + self._config_entry, + data={ + **self._config_entry.data, + DATA_SLNET_TOKEN: slnet_token, + DATA_EXPIRES: slnet_token_expires, + DATA_USER_ID: user_id, + }, + ) + except Exception as err: # pylint: disable=broad-except + LOGGER.error("Error updating SLNet token: %s", err) + pass + + def _update_data(self): + """Update StarLine data.""" + self._check_slnet_token() + self._api.update() + + @property + def api(self) -> StarlineApi: + """Return the instance of the API.""" + return self._api + + async def update(self, unused=None): + """Update StarLine data.""" + await self._hass.async_add_executor_job(self._update_data) + + def set_update_interval(self, interval: int) -> None: + """Set StarLine API update interval.""" + LOGGER.debug("Setting update interval: %ds", interval) + self._update_interval = interval + if self._unsubscribe_auto_updater is not None: + self._unsubscribe_auto_updater() + + delta = timedelta(seconds=interval) + self._unsubscribe_auto_updater = async_track_time_interval( + self._hass, self.update, delta + ) + + def unload(self): + """Unload StarLine API.""" + LOGGER.debug("Unloading StarLine API.") + if self._unsubscribe_auto_updater is not None: + self._unsubscribe_auto_updater() + self._unsubscribe_auto_updater = None + + @staticmethod + def device_info(device: StarlineDevice) -> Dict[str, Any]: + """Device information for entities.""" + return { + "identifiers": {(DOMAIN, device.device_id)}, + "manufacturer": "StarLine", + "name": device.name, + "sw_version": device.fw_version, + "model": device.typename, + } + + @staticmethod + def gps_attrs(device: StarlineDevice) -> Dict[str, Any]: + """Attributes for device tracker.""" + return { + "updated": datetime.utcfromtimestamp(device.position["ts"]).isoformat(), + "online": device.online, + } + + @staticmethod + def balance_attrs(device: StarlineDevice) -> Dict[str, Any]: + """Attributes for balance sensor.""" + return { + "operator": device.balance.get("operator"), + "state": device.balance.get("state"), + "updated": device.balance.get("ts"), + } + + @staticmethod + def gsm_attrs(device: StarlineDevice) -> Dict[str, Any]: + """Attributes for GSM sensor.""" + return { + "raw": device.gsm_level, + "imei": device.imei, + "phone": device.phone, + "online": device.online, + } + + @staticmethod + def engine_attrs(device: StarlineDevice) -> Dict[str, Any]: + """Attributes for engine switch.""" + return { + "autostart": device.car_state.get("r_start"), + "ignition": device.car_state.get("run"), + } diff --git a/homeassistant/components/starline/binary_sensor.py b/homeassistant/components/starline/binary_sensor.py new file mode 100644 index 00000000000..f2288e9363b --- /dev/null +++ b/homeassistant/components/starline/binary_sensor.py @@ -0,0 +1,59 @@ +"""Reads vehicle status from StarLine API.""" +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_DOOR, + DEVICE_CLASS_LOCK, + DEVICE_CLASS_POWER, + DEVICE_CLASS_PROBLEM, + BinarySensorDevice, +) + +from .account import StarlineAccount, StarlineDevice +from .const import DOMAIN +from .entity import StarlineEntity + +SENSOR_TYPES = { + "hbrake": ["Hand Brake", DEVICE_CLASS_POWER], + "hood": ["Hood", DEVICE_CLASS_DOOR], + "trunk": ["Trunk", DEVICE_CLASS_DOOR], + "alarm": ["Alarm", DEVICE_CLASS_PROBLEM], + "door": ["Doors", DEVICE_CLASS_LOCK], +} + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the StarLine sensors.""" + account: StarlineAccount = hass.data[DOMAIN][entry.entry_id] + entities = [] + for device in account.api.devices.values(): + for key, value in SENSOR_TYPES.items(): + if key in device.car_state: + sensor = StarlineSensor(account, device, key, *value) + if sensor.is_on is not None: + entities.append(sensor) + async_add_entities(entities) + + +class StarlineSensor(StarlineEntity, BinarySensorDevice): + """Representation of a StarLine binary sensor.""" + + def __init__( + self, + account: StarlineAccount, + device: StarlineDevice, + key: str, + name: str, + device_class: str, + ): + """Initialize sensor.""" + super().__init__(account, device, key, name) + self._device_class = device_class + + @property + def device_class(self): + """Return the class of the binary sensor.""" + return self._device_class + + @property + def is_on(self): + """Return the state of the binary sensor.""" + return self._device.car_state.get(self._key) diff --git a/homeassistant/components/starline/config_flow.py b/homeassistant/components/starline/config_flow.py new file mode 100644 index 00000000000..fa559f62913 --- /dev/null +++ b/homeassistant/components/starline/config_flow.py @@ -0,0 +1,230 @@ +"""Config flow to configure StarLine component.""" +from typing import Optional + +from starline import StarlineAuth +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME + +from .const import ( # pylint: disable=unused-import + CONF_APP_ID, + CONF_APP_SECRET, + CONF_CAPTCHA_CODE, + CONF_MFA_CODE, + DATA_EXPIRES, + DATA_SLID_TOKEN, + DATA_SLNET_TOKEN, + DATA_USER_ID, + DOMAIN, + ERROR_AUTH_APP, + ERROR_AUTH_MFA, + ERROR_AUTH_USER, + LOGGER, +) + + +class StarlineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a StarLine config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + 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_code = None + self._app_token = None + self._user_slid = None + self._user_id = None + self._slnet_token = None + self._slnet_token_expires = None + self._captcha_image = None + self._captcha_sid = None + self._captcha_code = None + self._phone_number = None + + self._auth = StarlineAuth() + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + return await self.async_step_auth_app(user_input) + + async def async_step_auth_app(self, user_input=None, error=None): + """Authenticate application step.""" + if user_input is not None: + self._app_id = user_input[CONF_APP_ID] + self._app_secret = user_input[CONF_APP_SECRET] + return await self._async_authenticate_app(error) + return self._async_form_auth_app(error) + + async def async_step_auth_user(self, user_input=None, error=None): + """Authenticate user step.""" + if user_input is not None: + self._username = user_input[CONF_USERNAME] + self._password = user_input[CONF_PASSWORD] + return await self._async_authenticate_user(error) + return self._async_form_auth_user(error) + + async def async_step_auth_mfa(self, user_input=None, error=None): + """Authenticate mfa step.""" + if user_input is not None: + self._mfa_code = user_input[CONF_MFA_CODE] + return await self._async_authenticate_user(error) + return self._async_form_auth_mfa(error) + + async def async_step_auth_captcha(self, user_input=None, error=None): + """Captcha verification step.""" + if user_input is not None: + self._captcha_code = user_input[CONF_CAPTCHA_CODE] + return await self._async_authenticate_user(error) + return self._async_form_auth_captcha(error) + + def _async_form_auth_app(self, error=None): + """Authenticate application form.""" + errors = {} + if error is not None: + errors["base"] = error + + return self.async_show_form( + step_id="auth_app", + data_schema=vol.Schema( + { + vol.Required( + CONF_APP_ID, default=self._app_id or vol.UNDEFINED + ): str, + vol.Required( + CONF_APP_SECRET, default=self._app_secret or vol.UNDEFINED + ): str, + } + ), + errors=errors, + ) + + def _async_form_auth_user(self, error=None): + """Authenticate user form.""" + errors = {} + if error is not None: + errors["base"] = error + + return self.async_show_form( + step_id="auth_user", + data_schema=vol.Schema( + { + vol.Required( + CONF_USERNAME, default=self._username or vol.UNDEFINED + ): str, + vol.Required( + CONF_PASSWORD, default=self._password or vol.UNDEFINED + ): str, + } + ), + errors=errors, + ) + + def _async_form_auth_mfa(self, error=None): + """Authenticate mfa form.""" + errors = {} + if error is not None: + errors["base"] = error + + return self.async_show_form( + step_id="auth_mfa", + data_schema=vol.Schema( + { + vol.Required( + CONF_MFA_CODE, default=self._mfa_code or vol.UNDEFINED + ): str + } + ), + errors=errors, + description_placeholders={"phone_number": self._phone_number}, + ) + + def _async_form_auth_captcha(self, error=None): + """Captcha verification form.""" + errors = {} + if error is not None: + errors["base"] = error + + return self.async_show_form( + step_id="auth_captcha", + data_schema=vol.Schema( + { + vol.Required( + CONF_CAPTCHA_CODE, default=self._captcha_code or vol.UNDEFINED + ): str + } + ), + errors=errors, + description_placeholders={ + "captcha_img": '' + }, + ) + + async def _async_authenticate_app(self, error=None): + """Authenticate application.""" + try: + self._app_code = self._auth.get_app_code(self._app_id, self._app_secret) + self._app_token = self._auth.get_app_token( + self._app_id, self._app_secret, self._app_code + ) + return self._async_form_auth_user(error) + except Exception as err: # pylint: disable=broad-except + LOGGER.error("Error auth StarLine: %s", err) + return self._async_form_auth_app(ERROR_AUTH_APP) + + async def _async_authenticate_user(self, error=None): + """Authenticate user.""" + try: + state, data = self._auth.get_slid_user_token( + self._app_token, + self._username, + self._password, + self._mfa_code, + self._captcha_sid, + self._captcha_code, + ) + + if state == 1: + self._user_slid = data["user_token"] + return await self._async_get_entry() + + if "phone" in data: + self._phone_number = data["phone"] + if state == 0: + error = ERROR_AUTH_MFA + return self._async_form_auth_mfa(error) + + if "captchaSid" in data: + self._captcha_sid = data["captchaSid"] + self._captcha_image = data["captchaImg"] + return self._async_form_auth_captcha(error) + + raise Exception(data) + except Exception as err: # pylint: disable=broad-except + LOGGER.error("Error auth user: %s", err) + return self._async_form_auth_user(ERROR_AUTH_USER) + + async def _async_get_entry(self): + """Create entry.""" + ( + self._slnet_token, + self._slnet_token_expires, + self._user_id, + ) = self._auth.get_user_id(self._user_slid) + + return self.async_create_entry( + title=f"Application {self._app_id}", + data={ + DATA_USER_ID: self._user_id, + DATA_SLNET_TOKEN: self._slnet_token, + DATA_SLID_TOKEN: self._user_slid, + DATA_EXPIRES: self._slnet_token_expires, + }, + ) diff --git a/homeassistant/components/starline/const.py b/homeassistant/components/starline/const.py new file mode 100644 index 00000000000..d76cd47b100 --- /dev/null +++ b/homeassistant/components/starline/const.py @@ -0,0 +1,27 @@ +"""StarLine constants.""" +import logging + +LOGGER = logging.getLogger(__package__) + +DOMAIN = "starline" +PLATFORMS = ["device_tracker", "binary_sensor", "sensor", "lock", "switch"] + +CONF_APP_ID = "app_id" +CONF_APP_SECRET = "app_secret" +CONF_MFA_CODE = "mfa_code" +CONF_CAPTCHA_CODE = "captcha_code" + +CONF_SCAN_INTERVAL = "scan_interval" +DEFAULT_SCAN_INTERVAL = 180 # in seconds + +ERROR_AUTH_APP = "error_auth_app" +ERROR_AUTH_USER = "error_auth_user" +ERROR_AUTH_MFA = "error_auth_mfa" + +DATA_USER_ID = "user_id" +DATA_SLNET_TOKEN = "slnet_token" +DATA_SLID_TOKEN = "slid_token" +DATA_EXPIRES = "expires" + +SERVICE_UPDATE_STATE = "update_state" +SERVICE_SET_SCAN_INTERVAL = "set_scan_interval" diff --git a/homeassistant/components/starline/device_tracker.py b/homeassistant/components/starline/device_tracker.py new file mode 100644 index 00000000000..6f202bbae52 --- /dev/null +++ b/homeassistant/components/starline/device_tracker.py @@ -0,0 +1,61 @@ +"""StarLine device tracker.""" +from homeassistant.components.device_tracker.config_entry import TrackerEntity +from homeassistant.components.device_tracker.const import SOURCE_TYPE_GPS +from homeassistant.helpers.restore_state import RestoreEntity + +from .account import StarlineAccount, StarlineDevice +from .const import DOMAIN +from .entity import StarlineEntity + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up StarLine entry.""" + account: StarlineAccount = hass.data[DOMAIN][entry.entry_id] + entities = [] + for device in account.api.devices.values(): + if device.support_position: + entities.append(StarlineDeviceTracker(account, device)) + async_add_entities(entities) + + +class StarlineDeviceTracker(StarlineEntity, TrackerEntity, RestoreEntity): + """StarLine device tracker.""" + + def __init__(self, account: StarlineAccount, device: StarlineDevice): + """Set up StarLine entity.""" + super().__init__(account, device, "location", "Location") + + @property + def device_state_attributes(self): + """Return device specific attributes.""" + return self._account.gps_attrs(self._device) + + @property + def battery_level(self): + """Return the battery level of the device.""" + return self._device.battery_level + + @property + def location_accuracy(self): + """Return the gps accuracy of the device.""" + return self._device.position["r"] if "r" in self._device.position else 0 + + @property + def latitude(self): + """Return latitude value of the device.""" + return self._device.position["x"] + + @property + def longitude(self): + """Return longitude value of the device.""" + return self._device.position["y"] + + @property + def source_type(self): + """Return the source type, eg gps or router, of the device.""" + return SOURCE_TYPE_GPS + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return "mdi:map-marker-outline" diff --git a/homeassistant/components/starline/entity.py b/homeassistant/components/starline/entity.py new file mode 100644 index 00000000000..5db4d369f5e --- /dev/null +++ b/homeassistant/components/starline/entity.py @@ -0,0 +1,61 @@ +"""StarLine base entity.""" +from typing import Callable, Optional + +from homeassistant.helpers.entity import Entity + +from .account import StarlineAccount, StarlineDevice + + +class StarlineEntity(Entity): + """StarLine base entity class.""" + + def __init__( + self, account: StarlineAccount, device: StarlineDevice, key: str, name: str + ): + """Initialize StarLine entity.""" + self._account = account + self._device = device + self._key = key + self._name = name + self._unsubscribe_api: Optional[Callable] = None + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def available(self): + """Return True if entity is available.""" + return self._account.api.available + + @property + def unique_id(self): + """Return the unique ID of the entity.""" + return f"starline-{self._key}-{self._device.device_id}" + + @property + def name(self): + """Return the name of the entity.""" + return f"{self._device.name} {self._name}" + + @property + def device_info(self): + """Return the device info.""" + return self._account.device_info(self._device) + + def update(self): + """Read new state data.""" + self.schedule_update_ha_state() + + async def async_added_to_hass(self): + """Call when entity about to be added to Home Assistant.""" + await super().async_added_to_hass() + self._unsubscribe_api = self._account.api.add_update_listener(self.update) + + async def async_will_remove_from_hass(self): + """Call when entity is being removed from Home Assistant.""" + await super().async_will_remove_from_hass() + if self._unsubscribe_api is not None: + self._unsubscribe_api() + self._unsubscribe_api = None diff --git a/homeassistant/components/starline/lock.py b/homeassistant/components/starline/lock.py new file mode 100644 index 00000000000..804e8c8df2d --- /dev/null +++ b/homeassistant/components/starline/lock.py @@ -0,0 +1,73 @@ +"""Support for StarLine lock.""" +from homeassistant.components.lock import LockDevice + +from .account import StarlineAccount, StarlineDevice +from .const import DOMAIN +from .entity import StarlineEntity + + +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(): + if device.support_state: + lock = StarlineLock(account, device) + if lock.is_locked is not None: + entities.append(lock) + async_add_entities(entities) + + +class StarlineLock(StarlineEntity, LockDevice): + """Representation of a StarLine lock.""" + + def __init__(self, account: StarlineAccount, device: StarlineDevice): + """Initialize the lock.""" + super().__init__(account, device, "lock", "Security") + + @property + def available(self): + """Return True if entity is available.""" + return super().available and self._device.online + + @property + def device_state_attributes(self): + """Return the state attributes of the lock. + + Possible dictionary keys: + add_h - Additional sensor alarm status (high level) + add_l - Additional channel alarm status (low level) + door - Doors alarm status + hbrake - Hand brake alarm status + hijack - Hijack mode status + hood - Hood alarm status + ign - Ignition alarm status + pbrake - Brake pedal alarm status + shock_h - Shock sensor alarm status (high level) + shock_l - Shock sensor alarm status (low level) + tilt - Tilt sensor alarm status + trunk - Trunk alarm status + Documentation: https://developer.starline.ru/#api-Device-DeviceState + """ + return self._device.alarm_state + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return ( + "mdi:shield-check-outline" if self.is_locked else "mdi:shield-alert-outline" + ) + + @property + def is_locked(self): + """Return true if lock is locked.""" + return self._device.car_state.get("arm") + + def lock(self, **kwargs): + """Lock the car.""" + self._account.api.set_car_state(self._device.device_id, "arm", True) + + def unlock(self, **kwargs): + """Unlock the car.""" + self._account.api.set_car_state(self._device.device_id, "arm", False) diff --git a/homeassistant/components/starline/manifest.json b/homeassistant/components/starline/manifest.json new file mode 100644 index 00000000000..ef343aae4ce --- /dev/null +++ b/homeassistant/components/starline/manifest.json @@ -0,0 +1,13 @@ +{ + "domain": "starline", + "name": "StarLine", + "config_flow": true, + "documentation": "https://www.home-assistant.io/components/starline", + "requirements": [ + "starline==0.1.3" + ], + "dependencies": [], + "codeowners": [ + "@anonym-tsk" + ] +} diff --git a/homeassistant/components/starline/sensor.py b/homeassistant/components/starline/sensor.py new file mode 100644 index 00000000000..0c6cd8de683 --- /dev/null +++ b/homeassistant/components/starline/sensor.py @@ -0,0 +1,97 @@ +"""Reads vehicle status from StarLine API.""" +from homeassistant.components.sensor import DEVICE_CLASS_TEMPERATURE +from homeassistant.const import TEMP_CELSIUS +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.icon import icon_for_battery_level, icon_for_signal_level + +from .account import StarlineAccount, StarlineDevice +from .const import DOMAIN +from .entity import StarlineEntity + +SENSOR_TYPES = { + "battery": ["Battery", None, "V", None], + "balance": ["Balance", None, None, "mdi:cash-multiple"], + "ctemp": ["Interior Temperature", DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None], + "etemp": ["Engine Temperature", DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None], + "gsm_lvl": ["GSM Signal", None, "%", None], +} + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the StarLine sensors.""" + account: StarlineAccount = hass.data[DOMAIN][entry.entry_id] + entities = [] + for device in account.api.devices.values(): + for key, value in SENSOR_TYPES.items(): + sensor = StarlineSensor(account, device, key, *value) + if sensor.state is not None: + entities.append(sensor) + async_add_entities(entities) + + +class StarlineSensor(StarlineEntity, Entity): + """Representation of a StarLine sensor.""" + + def __init__( + self, + account: StarlineAccount, + device: StarlineDevice, + key: str, + name: str, + device_class: str, + unit: str, + icon: str, + ): + """Initialize StarLine sensor.""" + super().__init__(account, device, key, name) + self._device_class = device_class + self._unit = unit + self._icon = icon + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + if self._key == "battery": + return icon_for_battery_level( + battery_level=self._device.battery_level_percent, + charging=self._device.car_state.get("ign", False), + ) + if self._key == "gsm_lvl": + return icon_for_signal_level(signal_level=self._device.gsm_level_percent) + return self._icon + + @property + def state(self): + """Return the state of the sensor.""" + if self._key == "battery": + return self._device.battery_level + if self._key == "balance": + return self._device.balance.get("value") + if self._key == "ctemp": + return self._device.temp_inner + if self._key == "etemp": + return self._device.temp_engine + if self._key == "gsm_lvl": + return self._device.gsm_level_percent + return None + + @property + def unit_of_measurement(self): + """Get the unit of measurement.""" + if self._key == "balance": + return self._device.balance.get("currency") or "₽" + return self._unit + + @property + def device_class(self): + """Return the class of the sensor.""" + return self._device_class + + @property + def device_state_attributes(self): + """Return the state attributes of the sensor.""" + if self._key == "balance": + return self._account.balance_attrs(self._device) + if self._key == "gsm_lvl": + return self._account.gsm_attrs(self._device) + return None diff --git a/homeassistant/components/starline/services.yaml b/homeassistant/components/starline/services.yaml new file mode 100644 index 00000000000..bef3a16803e --- /dev/null +++ b/homeassistant/components/starline/services.yaml @@ -0,0 +1,10 @@ +update_state: + description: > + Fetch the last state of the devices from the StarLine server. +set_scan_interval: + description: > + Set update frequency. + fields: + scan_interval: + description: Update frequency (in seconds). + example: 180 diff --git a/homeassistant/components/starline/strings.json b/homeassistant/components/starline/strings.json new file mode 100644 index 00000000000..bf83f652c3c --- /dev/null +++ b/homeassistant/components/starline/strings.json @@ -0,0 +1,42 @@ +{ + "config": { + "title": "StarLine", + "step": { + "auth_app": { + "title": "Application credentials", + "description": "Application ID and secret code from StarLine developer account", + "data": { + "app_id": "App ID", + "app_secret": "Secret" + } + }, + "auth_user": { + "title": "User credentials", + "description": "StarLine account email and password", + "data": { + "username": "Username", + "password": "Password" + } + }, + "auth_mfa": { + "title": "Two-factor authorization", + "description": "Enter the code sent to phone {phone_number}", + "data": { + "mfa_code": "SMS code" + } + }, + "auth_captcha": { + "title": "Captcha", + "description": "{captcha_img}", + "data": { + "captcha_code": "Code from image" + } + } + }, + "error": { + "error_auth_app": "Incorrect application id or secret", + "error_auth_user": "Incorrect username or password", + "error_auth_mfa": "Incorrect code" + } + } +} diff --git a/homeassistant/components/starline/switch.py b/homeassistant/components/starline/switch.py new file mode 100644 index 00000000000..920fe686d9a --- /dev/null +++ b/homeassistant/components/starline/switch.py @@ -0,0 +1,87 @@ +"""Support for StarLine switch.""" +from homeassistant.components.switch import SwitchDevice + +from .account import StarlineAccount, StarlineDevice +from .const import DOMAIN +from .entity import StarlineEntity + +SWITCH_TYPES = { + "ign": ["Engine", "mdi:engine-outline", "mdi:engine-off-outline"], + "webasto": ["Webasto", "mdi:radiator", "mdi:radiator-off"], + "out": [ + "Additional Channel", + "mdi:access-point-network", + "mdi:access-point-network-off", + ], + "poke": ["Horn", "mdi:bullhorn-outline", "mdi:bullhorn-outline"], +} + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the StarLine switch.""" + account: StarlineAccount = hass.data[DOMAIN][entry.entry_id] + entities = [] + for device in account.api.devices.values(): + if device.support_state: + for key, value in SWITCH_TYPES.items(): + switch = StarlineSwitch(account, device, key, *value) + if switch.is_on is not None: + entities.append(switch) + async_add_entities(entities) + + +class StarlineSwitch(StarlineEntity, SwitchDevice): + """Representation of a StarLine switch.""" + + def __init__( + self, + account: StarlineAccount, + device: StarlineDevice, + key: str, + name: str, + icon_on: str, + icon_off: str, + ): + """Initialize the switch.""" + super().__init__(account, device, key, name) + self._icon_on = icon_on + self._icon_off = icon_off + + @property + def available(self): + """Return True if entity is available.""" + return super().available and self._device.online + + @property + def device_state_attributes(self): + """Return the state attributes of the switch.""" + if self._key == "ign": + return self._account.engine_attrs(self._device) + return None + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return self._icon_on if self.is_on else self._icon_off + + @property + def assumed_state(self): + """Return True if unable to access real state of the entity.""" + return True + + @property + def is_on(self): + """Return True if entity is on.""" + if self._key == "poke": + return False + return self._device.car_state.get(self._key) + + def turn_on(self, **kwargs): + """Turn the entity on.""" + self._account.api.set_car_state(self._device.device_id, self._key, True) + + def turn_off(self, **kwargs) -> None: + """Turn the entity off.""" + if self._key == "poke": + return + self._account.api.set_car_state(self._device.device_id, self._key, False) diff --git a/homeassistant/components/starlingbank/manifest.json b/homeassistant/components/starlingbank/manifest.json index 33dbb40f78a..d68b6ea125c 100644 --- a/homeassistant/components/starlingbank/manifest.json +++ b/homeassistant/components/starlingbank/manifest.json @@ -2,9 +2,7 @@ "domain": "starlingbank", "name": "Starlingbank", "documentation": "https://www.home-assistant.io/integrations/starlingbank", - "requirements": [ - "starlingbank==3.1" - ], + "requirements": ["starlingbank==3.2"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/starlingbank/sensor.py b/homeassistant/components/starlingbank/sensor.py index 24ca7d4809c..1e046192347 100644 --- a/homeassistant/components/starlingbank/sensor.py +++ b/homeassistant/components/starlingbank/sensor.py @@ -2,6 +2,7 @@ import logging import requests +from starlingbank import StarlingAccount import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -40,7 +41,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Sterling Bank sensor platform.""" - from starlingbank import StarlingAccount sensors = [] for account in config[CONF_ACCOUNTS]: diff --git a/homeassistant/components/statistics/manifest.json b/homeassistant/components/statistics/manifest.json index 3dab05942b9..17ade1283ce 100644 --- a/homeassistant/components/statistics/manifest.json +++ b/homeassistant/components/statistics/manifest.json @@ -4,7 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/statistics", "requirements": [], "dependencies": [], - "codeowners": [ - "@fabaff" - ] + "after_dependencies": ["recorder"], + "codeowners": ["@fabaff"] } diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index 51868c6d0a8..6e042b1536f 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -1,25 +1,26 @@ """Support for statistics for sensor values.""" +from collections import deque import logging import statistics -from collections import deque import voluptuous as vol -import homeassistant.helpers.config_validation as cv +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.const import ( - CONF_NAME, - CONF_ENTITY_ID, - EVENT_HOMEASSISTANT_START, - STATE_UNKNOWN, - STATE_UNAVAILABLE, ATTR_UNIT_OF_MEASUREMENT, + CONF_ENTITY_ID, + CONF_NAME, + EVENT_HOMEASSISTANT_START, + STATE_UNAVAILABLE, + STATE_UNKNOWN, ) 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 from homeassistant.util import dt as dt_util -from homeassistant.components.recorder.util import session_scope, execute _LOGGER = logging.getLogger(__name__) @@ -275,7 +276,6 @@ class StatisticsSensor(Entity): If MaxAge is provided then query will restrict to entries younger then current datetime - MaxAge. """ - from homeassistant.components.recorder.models import States _LOGGER.debug("%s: initializing values from the database", self.entity_id) diff --git a/homeassistant/components/stiebel_eltron/__init__.py b/homeassistant/components/stiebel_eltron/__init__.py index 20cb6607ea1..956e629dd9d 100644 --- a/homeassistant/components/stiebel_eltron/__init__.py +++ b/homeassistant/components/stiebel_eltron/__init__.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +from pystiebeleltron import pystiebeleltron import voluptuous as vol from homeassistant.components.modbus import ( @@ -55,7 +56,6 @@ class StiebelEltronData: def __init__(self, name, modbus_client): """Init the STIEBEL ELTRON data object.""" - from pystiebeleltron import pystiebeleltron self.api = pystiebeleltron.StiebelEltronAPI(modbus_client, 1) diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index a83f05820e2..d88f90a83f8 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -1,10 +1,10 @@ """Provide functionality to stream video source.""" import logging +import secrets import threading import voluptuous as vol -from homeassistant.auth.util import generate_secret from homeassistant.const import CONF_FILENAME, EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError @@ -23,12 +23,6 @@ from .const import ( from .core import PROVIDERS from .hls import async_setup_hls -try: - import uvloop -except ImportError: - uvloop = None - - _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) @@ -42,7 +36,6 @@ SERVICE_RECORD_SCHEMA = STREAM_SERVICE_SCHEMA.extend( vol.Optional(CONF_LOOKBACK, default=0): int, } ) -DATA_UVLOOP_WARN = "stream_uvloop_warn" # Set log level to error for libav logging.getLogger("libav").setLevel(logging.ERROR) @@ -53,21 +46,6 @@ def request_stream(hass, stream_source, *, fmt="hls", keepalive=False, options=N if DOMAIN not in hass.config.components: raise HomeAssistantError("Stream integration is not set up.") - if DATA_UVLOOP_WARN not in hass.data: - hass.data[DATA_UVLOOP_WARN] = True - # Warn about https://github.com/home-assistant/home-assistant/issues/22999 - if ( - uvloop is not None - and isinstance(hass.loop, uvloop.Loop) - and ( - "shell_command" in hass.config.components - or "ffmpeg" in hass.config.components - ) - ): - _LOGGER.warning( - "You are using UVLoop with stream and shell_command. This is known to cause issues. Please uninstall uvloop." - ) - if options is None: options = {} @@ -94,7 +72,7 @@ def request_stream(hass, stream_source, *, fmt="hls", keepalive=False, options=N stream.add_provider(fmt) if not stream.access_token: - stream.access_token = generate_secret() + stream.access_token = secrets.token_hex() stream.start() return hass.data[DOMAIN][ATTR_ENDPOINTS][fmt].format(stream.access_token) except Exception: @@ -104,6 +82,7 @@ def request_stream(hass, stream_source, *, fmt="hls", keepalive=False, options=N async def async_setup(hass, config): """Set up stream.""" # Keep import here so that we can import stream integration without installing reqs + # pylint: disable=import-outside-toplevel from .recorder import async_setup_recorder hass.data[DOMAIN] = {} @@ -184,6 +163,7 @@ class Stream: 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.isAlive(): diff --git a/homeassistant/components/streamlabswater/__init__.py b/homeassistant/components/streamlabswater/__init__.py index 68097794d07..836bc9b4183 100644 --- a/homeassistant/components/streamlabswater/__init__.py +++ b/homeassistant/components/streamlabswater/__init__.py @@ -1,6 +1,7 @@ """Support for Streamlabs Water Monitor devices.""" import logging +from streamlabswater import streamlabswater import voluptuous as vol from homeassistant.const import CONF_API_KEY @@ -39,7 +40,6 @@ SET_AWAY_MODE_SCHEMA = vol.Schema( def setup(hass, config): """Set up the streamlabs water component.""" - from streamlabswater import streamlabswater conf = config[DOMAIN] api_key = conf.get(CONF_API_KEY) diff --git a/homeassistant/components/suez_water/sensor.py b/homeassistant/components/suez_water/sensor.py index 05f82183e46..bfa529adb34 100644 --- a/homeassistant/components/suez_water/sensor.py +++ b/homeassistant/components/suez_water/sensor.py @@ -2,10 +2,9 @@ from datetime import timedelta import logging -import voluptuous as vol - -from pysuez.client import PySuezError from pysuez import SuezClient +from pysuez.client import PySuezError +import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, VOLUME_LITERS diff --git a/homeassistant/components/sun/__init__.py b/homeassistant/components/sun/__init__.py index e848449e61e..704f9432a0f 100644 --- a/homeassistant/components/sun/__init__.py +++ b/homeassistant/components/sun/__init__.py @@ -1,12 +1,12 @@ """Support for functionality to keep track of the sun.""" -import logging from datetime import timedelta +import logging from homeassistant.const import ( CONF_ELEVATION, + EVENT_CORE_CONFIG_UPDATE, SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET, - EVENT_CORE_CONFIG_UPDATE, ) from homeassistant.core import callback from homeassistant.helpers.entity import Entity @@ -17,7 +17,6 @@ from homeassistant.helpers.sun import ( ) from homeassistant.util import dt as dt_util - # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/supervisord/sensor.py b/homeassistant/components/supervisord/sensor.py index e1a816f91ae..c79c09248b4 100644 --- a/homeassistant/components/supervisord/sensor.py +++ b/homeassistant/components/supervisord/sensor.py @@ -6,8 +6,8 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_URL -from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/supla/__init__.py b/homeassistant/components/supla/__init__.py index 4293f187f5b..fd60254cd0a 100644 --- a/homeassistant/components/supla/__init__.py +++ b/homeassistant/components/supla/__init__.py @@ -2,6 +2,7 @@ import logging from typing import Optional +from pysupla import SuplaAPI import voluptuous as vol from homeassistant.const import CONF_ACCESS_TOKEN @@ -38,7 +39,6 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, base_config): """Set up the Supla component.""" - from pysupla import SuplaAPI server_confs = base_config[DOMAIN][CONF_SERVERS] diff --git a/homeassistant/components/supla/switch.py b/homeassistant/components/supla/switch.py index 5e7a5469950..725771e21e8 100644 --- a/homeassistant/components/supla/switch.py +++ b/homeassistant/components/supla/switch.py @@ -2,8 +2,8 @@ import logging from pprint import pformat -from homeassistant.components.switch import SwitchDevice from homeassistant.components.supla import SuplaChannel +from homeassistant.components.switch import SwitchDevice _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/swiss_hydrological_data/sensor.py b/homeassistant/components/swiss_hydrological_data/sensor.py index 2d5d0e8de3f..d4624e82bb7 100644 --- a/homeassistant/components/swiss_hydrological_data/sensor.py +++ b/homeassistant/components/swiss_hydrological_data/sensor.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +from swisshydrodata import SwissHydroData import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -12,7 +13,7 @@ from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) -ATTRIBUTION = "Data provided by the Swiss Federal Office for the " "Environment FOEN" +ATTRIBUTION = "Data provided by the Swiss Federal Office for the Environment FOEN" ATTR_DELTA_24H = "delta-24h" ATTR_MAX_1H = "max-1h" @@ -167,7 +168,6 @@ class HydrologicalData: @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data.""" - from swisshydrodata import SwissHydroData shd = SwissHydroData() self.data = shd.get_station(self.station) diff --git a/homeassistant/components/swiss_public_transport/manifest.json b/homeassistant/components/swiss_public_transport/manifest.json index c91d85fecd7..387b3da5da4 100644 --- a/homeassistant/components/swiss_public_transport/manifest.json +++ b/homeassistant/components/swiss_public_transport/manifest.json @@ -3,10 +3,10 @@ "name": "Swiss public transport", "documentation": "https://www.home-assistant.io/integrations/swiss_public_transport", "requirements": [ - "python_opendata_transport==0.1.4" + "python_opendata_transport==0.2.1" ], "dependencies": [], "codeowners": [ "@fabaff" ] -} +} \ No newline at end of file diff --git a/homeassistant/components/swiss_public_transport/sensor.py b/homeassistant/components/swiss_public_transport/sensor.py index 3cf8babf554..be967247dc7 100644 --- a/homeassistant/components/swiss_public_transport/sensor.py +++ b/homeassistant/components/swiss_public_transport/sensor.py @@ -2,6 +2,8 @@ from datetime import timedelta import logging +from opendata_transport import OpendataTransport +from opendata_transport.exceptions import OpendataTransportError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -45,7 +47,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Swiss public transport sensor.""" - from opendata_transport import OpendataTransport, exceptions name = config.get(CONF_NAME) start = config.get(CONF_START) @@ -56,7 +57,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= try: await opendata.async_get_data() - except exceptions.OpendataTransportError: + except OpendataTransportError: _LOGGER.error( "Check at http://transport.opendata.ch/examples/stationboard.html " "if your station names are valid" @@ -122,7 +123,6 @@ class SwissPublicTransportSensor(Entity): async def async_update(self): """Get the latest data from opendata.ch and update the states.""" - from opendata_transport.exceptions import OpendataTransportError try: if self._remaining_time.total_seconds() < 0: diff --git a/homeassistant/components/switch/.translations/da.json b/homeassistant/components/switch/.translations/da.json new file mode 100644 index 00000000000..2514a56a010 --- /dev/null +++ b/homeassistant/components/switch/.translations/da.json @@ -0,0 +1,19 @@ +{ + "device_automation": { + "action_type": { + "toggle": "Skift {entity_name}", + "turn_off": "Sluk {entity_name}", + "turn_on": "T\u00e6nd for {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} er fra", + "is_on": "{entity_name} er til", + "turn_off": "{entity_name} slukket", + "turn_on": "{entity_name} t\u00e6ndt" + }, + "trigger_type": { + "turned_off": "{entity_name} slukkede", + "turned_on": "{entity_name} t\u00e6ndte" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/ko.json b/homeassistant/components/switch/.translations/ko.json index 02c303f9329..d3b9b1dd169 100644 --- a/homeassistant/components/switch/.translations/ko.json +++ b/homeassistant/components/switch/.translations/ko.json @@ -6,14 +6,14 @@ "turn_on": "{entity_name} \ucf1c\uae30" }, "condition_type": { - "is_off": "{entity_name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4", - "is_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4", - "turn_off": "{entity_name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4", - "turn_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4" + "is_off": "{entity_name} \uc774(\uac00) \uaebc\uc838 \uc788\uc73c\uba74", + "is_on": "{entity_name} \uc774(\uac00) \ucf1c\uc838 \uc788\uc73c\uba74", + "turn_off": "{entity_name} \uc774(\uac00) \uaebc\uc838 \uc788\uc73c\uba74", + "turn_on": "{entity_name} \uc774(\uac00) \ucf1c\uc838 \uc788\uc73c\uba74" }, "trigger_type": { - "turned_off": "{entity_name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4", - "turned_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4" + "turned_off": "{entity_name} \uc774(\uac00) \uaebc\uc9c8 \ub54c", + "turned_on": "{entity_name} \uc774(\uac00) \ucf1c\uc9c8 \ub54c" } } } \ No newline at end of file diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index 26d5658d668..78c57e001aa 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -4,21 +4,20 @@ import logging import voluptuous as vol -from homeassistant.loader import bind_hass -from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.entity import ToggleEntity +from homeassistant.components import group +from homeassistant.const import ( + SERVICE_TOGGLE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_ON, +) from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) -from homeassistant.const import ( - STATE_ON, - SERVICE_TURN_ON, - SERVICE_TURN_OFF, - SERVICE_TOGGLE, -) -from homeassistant.components import group - +from homeassistant.helpers.entity import ToggleEntity +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.loader import bind_hass # mypy: allow-untyped-defs, no-check-untyped-defs diff --git a/homeassistant/components/switch/device_action.py b/homeassistant/components/switch/device_action.py index a65c1acc512..a50131f094c 100644 --- a/homeassistant/components/switch/device_action.py +++ b/homeassistant/components/switch/device_action.py @@ -1,13 +1,14 @@ """Provides device actions for switches.""" from typing import List + import voluptuous as vol -from homeassistant.core import HomeAssistant, Context from homeassistant.components.device_automation import toggle_entity from homeassistant.const import CONF_DOMAIN -from homeassistant.helpers.typing import TemplateVarsType, ConfigType -from . import DOMAIN +from homeassistant.core import Context, HomeAssistant +from homeassistant.helpers.typing import ConfigType, TemplateVarsType +from . import DOMAIN ACTION_SCHEMA = toggle_entity.ACTION_SCHEMA.extend({vol.Required(CONF_DOMAIN): DOMAIN}) diff --git a/homeassistant/components/switch/device_condition.py b/homeassistant/components/switch/device_condition.py index 56f8f6c196e..87aefdb616d 100644 --- a/homeassistant/components/switch/device_condition.py +++ b/homeassistant/components/switch/device_condition.py @@ -1,14 +1,15 @@ """Provides device conditions for switches.""" from typing import Dict, List + import voluptuous as vol -from homeassistant.core import HomeAssistant from homeassistant.components.device_automation import toggle_entity from homeassistant.const import CONF_DOMAIN -from homeassistant.helpers.typing import ConfigType +from homeassistant.core import HomeAssistant from homeassistant.helpers.condition import ConditionCheckerType -from . import DOMAIN +from homeassistant.helpers.typing import ConfigType +from . import DOMAIN CONDITION_SCHEMA = toggle_entity.CONDITION_SCHEMA.extend( {vol.Required(CONF_DOMAIN): DOMAIN} diff --git a/homeassistant/components/switch/device_trigger.py b/homeassistant/components/switch/device_trigger.py index 7f0458b3e9f..cb5d5f7aa0e 100644 --- a/homeassistant/components/switch/device_trigger.py +++ b/homeassistant/components/switch/device_trigger.py @@ -1,14 +1,15 @@ """Provides device triggers for switches.""" from typing import List + import voluptuous as vol -from homeassistant.core import HomeAssistant, CALLBACK_TYPE from homeassistant.components.automation import AutomationActionType from homeassistant.components.device_automation import toggle_entity from homeassistant.const import CONF_DOMAIN +from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers.typing import ConfigType -from . import DOMAIN +from . import DOMAIN TRIGGER_SCHEMA = toggle_entity.TRIGGER_SCHEMA.extend( {vol.Required(CONF_DOMAIN): DOMAIN} diff --git a/homeassistant/components/switch/light.py b/homeassistant/components/switch/light.py index 1bdc1d39083..5486b8d880c 100644 --- a/homeassistant/components/switch/light.py +++ b/homeassistant/components/switch/light.py @@ -1,10 +1,11 @@ """Light support for switch entities.""" import logging -from typing import cast, Callable, Dict, Optional, Sequence +from typing import Callable, Dict, Optional, Sequence, cast import voluptuous as vol from homeassistant.components import switch +from homeassistant.components.light import PLATFORM_SCHEMA, Light from homeassistant.const import ( ATTR_ENTITY_ID, CONF_ENTITY_ID, @@ -18,9 +19,6 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from homeassistant.components.light import PLATFORM_SCHEMA, Light - - # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/switch/reproduce_state.py b/homeassistant/components/switch/reproduce_state.py index 7ed1f70cb97..d2bfc569956 100644 --- a/homeassistant/components/switch/reproduce_state.py +++ b/homeassistant/components/switch/reproduce_state.py @@ -5,10 +5,10 @@ from typing import Iterable, Optional from homeassistant.const import ( ATTR_ENTITY_ID, - STATE_ON, - STATE_OFF, SERVICE_TURN_OFF, SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, ) from homeassistant.core import Context, State from homeassistant.helpers.typing import HomeAssistantType diff --git a/homeassistant/components/switch/services.yaml b/homeassistant/components/switch/services.yaml index 46b1237f57c..352ffb6feec 100644 --- a/homeassistant/components/switch/services.yaml +++ b/homeassistant/components/switch/services.yaml @@ -20,44 +20,3 @@ toggle: entity_id: description: Name(s) of entities to toggle. example: 'switch.living_room' - -mysensors_send_ir_code: - description: Set an IR code as a state attribute for a MySensors IR device switch and turn the switch on. - fields: - entity_id: - description: Name(s) of entities that should have the IR code set and be turned on. Platform dependent. - example: 'switch.living_room_1_1' - V_IR_SEND: - description: IR code to send. - example: '0xC284' - -xiaomi_miio_set_wifi_led_on: - description: Turn the wifi led on. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'switch.xiaomi_miio_device' -xiaomi_miio_set_wifi_led_off: - description: Turn the wifi led off. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'switch.xiaomi_miio_device' -xiaomi_miio_set_power_price: - description: Set the power price. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'switch.xiaomi_miio_device' - mode: - description: Power price, between 0 and 999. - example: 31 -xiaomi_miio_set_power_mode: - description: Set the power mode. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'switch.xiaomi_miio_device' - mode: - description: Power mode, valid values are 'normal' and 'green'. - example: 'green' diff --git a/homeassistant/components/switchbot/switch.py b/homeassistant/components/switchbot/switch.py index bc1fc34a890..55e2a8b9641 100644 --- a/homeassistant/components/switchbot/switch.py +++ b/homeassistant/components/switchbot/switch.py @@ -2,11 +2,13 @@ import logging from typing import Any, Dict +# pylint: disable=import-error, no-member +import switchbot import voluptuous as vol +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice +from homeassistant.const import CONF_MAC, CONF_NAME import homeassistant.helpers.config_validation as cv -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, CONF_MAC from homeassistant.helpers.restore_state import RestoreEntity _LOGGER = logging.getLogger(__name__) @@ -33,8 +35,6 @@ class SwitchBot(SwitchDevice, RestoreEntity): def __init__(self, mac, name) -> None: """Initialize the Switchbot.""" - # pylint: disable=import-error, no-member - import switchbot self._state = None self._last_run_success = None diff --git a/homeassistant/components/switcher_kis/__init__.py b/homeassistant/components/switcher_kis/__init__.py index 9f4347d61d2..e7e8d2d270c 100644 --- a/homeassistant/components/switcher_kis/__init__.py +++ b/homeassistant/components/switcher_kis/__init__.py @@ -5,6 +5,8 @@ from datetime import datetime, timedelta from logging import getLogger from typing import Dict, Optional +from aioswitcher.api import SwitcherV2Api +from aioswitcher.bridge import SwitcherV2Bridge import voluptuous as vol from homeassistant.auth.permissions.const import POLICY_EDIT @@ -88,7 +90,6 @@ async def _validate_edit_permission( async def async_setup(hass: HomeAssistantType, config: Dict) -> bool: """Set up the switcher component.""" - from aioswitcher.bridge import SwitcherV2Bridge phone_id = config[DOMAIN][CONF_PHONE_ID] device_id = config[DOMAIN][CONF_DEVICE_ID] @@ -122,7 +123,6 @@ async def async_setup(hass: HomeAssistantType, config: Dict) -> bool: async def async_set_auto_off_service(service: ServiceCallType) -> None: """Use for handling setting device auto-off service calls.""" - from aioswitcher.api import SwitcherV2Api await _validate_edit_permission( hass, service.context, service.data[CONF_ENTITY_ID] diff --git a/homeassistant/components/switcher_kis/switch.py b/homeassistant/components/switcher_kis/switch.py index 454baca4eef..c8eaddcb5bd 100644 --- a/homeassistant/components/switcher_kis/switch.py +++ b/homeassistant/components/switcher_kis/switch.py @@ -1,7 +1,16 @@ """Home Assistant Switcher Component Switch platform.""" from logging import getLogger -from typing import Callable, Dict, TYPE_CHECKING +from typing import TYPE_CHECKING, Callable, Dict + +from aioswitcher.api import SwitcherV2Api +from aioswitcher.consts import ( + COMMAND_OFF, + COMMAND_ON, + STATE_OFF as SWITCHER_STATE_OFF, + STATE_ON as SWITCHER_STATE_ON, + WAITING_TEXT, +) from homeassistant.components.switch import ATTR_CURRENT_POWER_W, SwitchDevice from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -16,6 +25,7 @@ from . import ( SIGNAL_SWITCHER_DEVICE_UPDATE, ) +# pylint: disable=ungrouped-imports if TYPE_CHECKING: from aioswitcher.devices import SwitcherV2Device from aioswitcher.api.messages import SwitcherV2ControlResponseMSG @@ -70,7 +80,6 @@ class SwitcherControl(SwitchDevice): @property def is_on(self) -> bool: """Return True if entity is on.""" - from aioswitcher.consts import STATE_ON as SWITCHER_STATE_ON return self._state == SWITCHER_STATE_ON @@ -82,7 +91,6 @@ class SwitcherControl(SwitchDevice): @property def device_state_attributes(self) -> Dict: """Return the optional state attributes.""" - from aioswitcher.consts import WAITING_TEXT attribs = {} @@ -96,10 +104,6 @@ class SwitcherControl(SwitchDevice): @property def available(self) -> bool: """Return True if entity is available.""" - from aioswitcher.consts import ( - STATE_OFF as SWITCHER_STATE_OFF, - STATE_ON as SWITCHER_STATE_ON, - ) return self._state in [SWITCHER_STATE_ON, SWITCHER_STATE_OFF] @@ -135,13 +139,6 @@ class SwitcherControl(SwitchDevice): async def _control_device(self, send_on: bool) -> None: """Turn the entity on or off.""" - from aioswitcher.api import SwitcherV2Api - from aioswitcher.consts import ( - COMMAND_OFF, - COMMAND_ON, - STATE_OFF as SWITCHER_STATE_OFF, - STATE_ON as SWITCHER_STATE_ON, - ) response: "SwitcherV2ControlResponseMSG" = None async with SwitcherV2Api( diff --git a/homeassistant/components/switchmate/switch.py b/homeassistant/components/switchmate/switch.py index 6abbfd5fae5..ddb0db3feee 100644 --- a/homeassistant/components/switchmate/switch.py +++ b/homeassistant/components/switchmate/switch.py @@ -2,7 +2,7 @@ from datetime import timedelta import logging -# pylint: disable=import-error, no-member, no-value-for-parameter +# pylint: disable=import-error, no-member import switchmate import voluptuous as vol diff --git a/homeassistant/components/syncthru/sensor.py b/homeassistant/components/syncthru/sensor.py index 1258732223b..e981154a81a 100644 --- a/homeassistant/components/syncthru/sensor.py +++ b/homeassistant/components/syncthru/sensor.py @@ -1,13 +1,15 @@ """Support for Samsung Printers with SyncThru web interface.""" import logging + +from pysyncthru import SyncThru import voluptuous as vol -from homeassistant.const import CONF_RESOURCE, CONF_HOST, CONF_NAME -from homeassistant.helpers import aiohttp_client -from homeassistant.helpers.entity import Entity -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_RESOURCE +from homeassistant.helpers import aiohttp_client +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -33,7 +35,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the SyncThru component.""" - from pysyncthru import SyncThru if discovery_info is not None: _LOGGER.info( diff --git a/homeassistant/components/synology/camera.py b/homeassistant/components/synology/camera.py index 8c176f48803..c144a251608 100644 --- a/homeassistant/components/synology/camera.py +++ b/homeassistant/components/synology/camera.py @@ -2,18 +2,19 @@ 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_USERNAME, CONF_PASSWORD, - CONF_URL, - CONF_WHITELIST, - CONF_VERIFY_SSL, CONF_TIMEOUT, + CONF_URL, + CONF_USERNAME, + CONF_VERIFY_SSL, + CONF_WHITELIST, ) -from homeassistant.components.camera import Camera, PLATFORM_SCHEMA from homeassistant.helpers.aiohttp_client import ( async_aiohttp_proxy_web, async_get_clientsession, @@ -44,8 +45,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= timeout = config.get(CONF_TIMEOUT) try: - from synology.surveillance_station import SurveillanceStation - surveillance = SurveillanceStation( config.get(CONF_URL), config.get(CONF_USERNAME), @@ -62,7 +61,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= # add cameras devices = [] for camera in cameras: - if not config.get(CONF_WHITELIST): + if not config[CONF_WHITELIST] or camera.name in config[CONF_WHITELIST]: device = SynologyCamera(surveillance, camera.camera_id, verify_ssl) devices.append(device) diff --git a/homeassistant/components/synology_chat/notify.py b/homeassistant/components/synology_chat/notify.py index c67ef79f5d5..3e1aeb4ce13 100644 --- a/homeassistant/components/synology_chat/notify.py +++ b/homeassistant/components/synology_chat/notify.py @@ -5,14 +5,13 @@ import logging import requests import voluptuous as vol -from homeassistant.const import CONF_RESOURCE, CONF_VERIFY_SSL -import homeassistant.helpers.config_validation as cv - from homeassistant.components.notify import ( ATTR_DATA, PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.const import CONF_RESOURCE, CONF_VERIFY_SSL +import homeassistant.helpers.config_validation as cv ATTR_FILE_URL = "file_url" diff --git a/homeassistant/components/synologydsm/sensor.py b/homeassistant/components/synologydsm/sensor.py index e19f6ada809..d415d009252 100644 --- a/homeassistant/components/synologydsm/sensor.py +++ b/homeassistant/components/synologydsm/sensor.py @@ -1,24 +1,25 @@ """Support for Synology NAS Sensors.""" -import logging from datetime import timedelta +import logging +from SynologyDSM import SynologyDSM import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, + ATTR_ATTRIBUTION, + CONF_DISKS, CONF_HOST, - CONF_USERNAME, + CONF_MONITORED_CONDITIONS, + CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_SSL, - ATTR_ATTRIBUTION, - TEMP_CELSIUS, - CONF_MONITORED_CONDITIONS, + CONF_USERNAME, EVENT_HOMEASSISTANT_START, - CONF_DISKS, + TEMP_CELSIUS, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -119,24 +120,26 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ] # Handle all volumes - for volume in config.get(CONF_VOLUMES, api.storage.volumes): - sensors += [ - SynoNasStorageSensor( - api, name, variable, _STORAGE_VOL_MON_COND[variable], volume - ) - for variable in monitored_conditions - if variable in _STORAGE_VOL_MON_COND - ] + if api.storage.volumes is not None: + for volume in config.get(CONF_VOLUMES, api.storage.volumes): + sensors += [ + SynoNasStorageSensor( + api, name, variable, _STORAGE_VOL_MON_COND[variable], volume + ) + for variable in monitored_conditions + if variable in _STORAGE_VOL_MON_COND + ] # Handle all disks - for disk in config.get(CONF_DISKS, api.storage.disks): - sensors += [ - SynoNasStorageSensor( - api, name, variable, _STORAGE_DSK_MON_COND[variable], disk - ) - for variable in monitored_conditions - if variable in _STORAGE_DSK_MON_COND - ] + if api.storage.disks is not None: + for disk in config.get(CONF_DISKS, api.storage.disks): + sensors += [ + SynoNasStorageSensor( + api, name, variable, _STORAGE_DSK_MON_COND[variable], disk + ) + for variable in monitored_conditions + if variable in _STORAGE_DSK_MON_COND + ] add_entities(sensors, True) @@ -149,7 +152,6 @@ class SynoApi: def __init__(self, host, port, username, password, temp_unit, use_ssl): """Initialize the API wrapper class.""" - from SynologyDSM import SynologyDSM self.temp_unit = temp_unit diff --git a/homeassistant/components/system_health/__init__.py b/homeassistant/components/system_health/__init__.py index c6b48350959..778ddf601dc 100644 --- a/homeassistant/components/system_health/__init__.py +++ b/homeassistant/components/system_health/__init__.py @@ -44,7 +44,7 @@ async def _info_wrapper(hass, info_callback): return await info_callback(hass) except asyncio.TimeoutError: return {"error": "Fetching info timed out"} - except Exception as err: # pylint: disable=W0703 + except Exception as err: # pylint: disable=broad-except _LOGGER.exception("Error fetching info") return {"error": str(err)} diff --git a/homeassistant/components/system_log/__init__.py b/homeassistant/components/system_log/__init__.py index 68561d45f8f..44ff9c49a01 100644 --- a/homeassistant/components/system_log/__init__.py +++ b/homeassistant/components/system_log/__init__.py @@ -8,8 +8,8 @@ import voluptuous as vol from homeassistant import __path__ as HOMEASSISTANT_PATH from homeassistant.components.http import HomeAssistantView -import homeassistant.helpers.config_validation as cv from homeassistant.const import EVENT_HOMEASSISTANT_STOP +import homeassistant.helpers.config_validation as cv CONF_MAX_ENTRIES = "max_entries" CONF_FIRE_EVENT = "fire_event" @@ -57,6 +57,7 @@ def _figure_out_source(record, call_stack, hass): paths = [HOMEASSISTANT_PATH[0], hass.config.config_dir] try: # If netdisco is installed check its path too. + # pylint: disable=import-outside-toplevel from netdisco import __path__ as netdisco_path paths.append(netdisco_path[0]) diff --git a/homeassistant/components/systemmonitor/manifest.json b/homeassistant/components/systemmonitor/manifest.json index 67677bc2572..0614ef4b91c 100644 --- a/homeassistant/components/systemmonitor/manifest.json +++ b/homeassistant/components/systemmonitor/manifest.json @@ -3,7 +3,7 @@ "name": "Systemmonitor", "documentation": "https://www.home-assistant.io/integrations/systemmonitor", "requirements": [ - "psutil==5.6.5" + "psutil==5.6.7" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index ad66a594a86..ebf605bdc75 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -1,30 +1,37 @@ """Support for the (unofficial) Tado API.""" +from datetime import timedelta import logging import urllib -from datetime import timedelta +from PyTado.interface import Tado import voluptuous as vol -from homeassistant.helpers.discovery import load_platform +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers import config_validation as cv -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD +from homeassistant.helpers.discovery import load_platform +from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.util import Throttle +from .const import CONF_FALLBACK + _LOGGER = logging.getLogger(__name__) -DATA_TADO = "tado_data" DOMAIN = "tado" -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) +SIGNAL_TADO_UPDATE_RECEIVED = "tado_update_received_{}_{}" TADO_COMPONENTS = ["sensor", "climate"] +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) +SCAN_INTERVAL = timedelta(seconds=15) + CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( { vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_FALLBACK, default=True): cv.boolean, } ) }, @@ -37,93 +44,106 @@ def setup(hass, config): username = config[DOMAIN][CONF_USERNAME] password = config[DOMAIN][CONF_PASSWORD] - from PyTado.interface import Tado - - try: - tado = Tado(username, password) - tado.setDebugging(True) - except (RuntimeError, urllib.error.HTTPError): - _LOGGER.error("Unable to connect to mytado with username and password") + tadoconnector = TadoConnector(hass, username, password) + if not tadoconnector.setup(): return False - hass.data[DATA_TADO] = TadoDataStore(tado) + hass.data[DOMAIN] = tadoconnector + # Do first update + tadoconnector.update() + + # Load components for component in TADO_COMPONENTS: - load_platform(hass, component, DOMAIN, {}, config) + load_platform( + hass, + component, + DOMAIN, + {CONF_FALLBACK: config[DOMAIN][CONF_FALLBACK]}, + config, + ) + + # Poll for updates in the background + hass.helpers.event.track_time_interval( + lambda now: tadoconnector.update(), SCAN_INTERVAL + ) return True -class TadoDataStore: +class TadoConnector: """An object to store the Tado data.""" - def __init__(self, tado): - """Initialize Tado data store.""" - self.tado = tado + def __init__(self, hass, username, password): + """Initialize Tado Connector.""" + self.hass = hass + self._username = username + self._password = password - self.sensors = {} - self.data = {} + self.tado = None + self.zones = None + self.devices = None + self.data = { + "zone": {}, + "device": {}, + } + + def setup(self): + """Connect to Tado and fetch the zones.""" + try: + self.tado = Tado(self._username, self._password) + except (RuntimeError, urllib.error.HTTPError) as exc: + _LOGGER.error("Unable to connect: %s", exc) + return False + + self.tado.setDebugging(True) + + # Load zones and devices + self.zones = self.tado.getZones() + self.devices = self.tado.getMe()["homes"] + + return True @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): - """Update the internal data from mytado.com.""" - for data_id, sensor in list(self.sensors.items()): - data = None + """Update the registered zones.""" + for zone in self.zones: + self.update_sensor("zone", zone["id"]) + for device in self.devices: + self.update_sensor("device", device["id"]) - try: - if "zone" in sensor: - _LOGGER.debug( - "Querying mytado.com for zone %s %s", - sensor["id"], - sensor["name"], - ) - data = self.tado.getState(sensor["id"]) + def update_sensor(self, sensor_type, sensor): + """Update the internal data from Tado.""" + _LOGGER.debug("Updating %s %s", sensor_type, sensor) + try: + if sensor_type == "zone": + data = self.tado.getState(sensor) + elif sensor_type == "device": + data = self.tado.getDevices()[0] + else: + _LOGGER.debug("Unknown sensor: %s", sensor_type) + return + except RuntimeError: + _LOGGER.error( + "Unable to connect to Tado while updating %s %s", sensor_type, sensor, + ) + return - if "device" in sensor: - _LOGGER.debug( - "Querying mytado.com for device %s %s", - sensor["id"], - sensor["name"], - ) - data = self.tado.getDevices()[0] + self.data[sensor_type][sensor] = data - except RuntimeError: - _LOGGER.error( - "Unable to connect to myTado. %s %s", sensor["id"], sensor["id"] - ) + _LOGGER.debug("Dispatching update to %s %s: %s", sensor_type, sensor, data) + dispatcher_send( + self.hass, SIGNAL_TADO_UPDATE_RECEIVED.format(sensor_type, sensor) + ) - self.data[data_id] = data - - def add_sensor(self, data_id, sensor): - """Add a sensor to update in _update().""" - self.sensors[data_id] = sensor - self.data[data_id] = None - - def get_data(self, data_id): - """Get the cached data.""" - data = {"error": "no data"} - - if data_id in self.data: - data = self.data[data_id] - - return data - - def get_zones(self): - """Wrap for getZones().""" - return self.tado.getZones() - - def get_capabilities(self, tado_id): - """Wrap for getCapabilities(..).""" - return self.tado.getCapabilities(tado_id) - - def get_me(self): - """Wrap for getMet().""" - return self.tado.getMe() + def get_capabilities(self, zone_id): + """Return the capabilities of the devices.""" + return self.tado.getCapabilities(zone_id) def reset_zone_overlay(self, zone_id): - """Wrap for resetZoneOverlay(..).""" + """Reset the zone back to the default operation.""" self.tado.resetZoneOverlay(zone_id) - self.update(no_throttle=True) # pylint: disable=unexpected-keyword-arg + self.update_sensor("zone", zone_id) def set_zone_overlay( self, @@ -134,13 +154,32 @@ class TadoDataStore: device_type="HEATING", mode=None, ): - """Wrap for setZoneOverlay(..).""" - self.tado.setZoneOverlay( - zone_id, overlay_mode, temperature, duration, device_type, "ON", mode + """Set a zone overlay.""" + _LOGGER.debug( + "Set overlay for zone %s: mode=%s, temp=%s, duration=%s, type=%s, mode=%s", + zone_id, + overlay_mode, + temperature, + duration, + device_type, + mode, ) - self.update(no_throttle=True) # pylint: disable=unexpected-keyword-arg + try: + self.tado.setZoneOverlay( + zone_id, overlay_mode, temperature, duration, device_type, "ON", mode + ) + except urllib.error.HTTPError as exc: + _LOGGER.error("Could not set zone overlay: %s", exc.read()) + + self.update_sensor("zone", zone_id) def set_zone_off(self, zone_id, overlay_mode, device_type="HEATING"): """Set a zone to off.""" - self.tado.setZoneOverlay(zone_id, overlay_mode, None, None, device_type, "OFF") - self.update(no_throttle=True) # pylint: disable=unexpected-keyword-arg + try: + self.tado.setZoneOverlay( + zone_id, overlay_mode, None, None, device_type, "OFF" + ) + except urllib.error.HTTPError as exc: + _LOGGER.error("Could not set zone overlay: %s", exc.read()) + + self.update_sensor("zone", zone_id) diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index 1108b32af4e..88433db0991 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -1,20 +1,19 @@ -"""Support for Tado to create a climate device for each zone.""" +"""Support for Tado thermostats.""" import logging -from typing import Optional, List from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( - CURRENT_HVAC_OFF, CURRENT_HVAC_COOL, CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, + CURRENT_HVAC_OFF, FAN_HIGH, FAN_LOW, FAN_MIDDLE, FAN_OFF, HVAC_MODE_AUTO, - HVAC_MODE_HEAT, HVAC_MODE_COOL, + HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, PRESET_AWAY, @@ -23,47 +22,40 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ATTR_TEMPERATURE, PRECISION_TENTHS, TEMP_CELSIUS -from homeassistant.util.temperature import convert as convert_temperature +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect -from . import DATA_TADO +from . import CONF_FALLBACK, DOMAIN, SIGNAL_TADO_UPDATE_RECEIVED +from .const import ( + CONST_MODE_OFF, + CONST_MODE_SMART_SCHEDULE, + CONST_OVERLAY_MANUAL, + CONST_OVERLAY_TADO_MODE, + TYPE_AIR_CONDITIONING, +) _LOGGER = logging.getLogger(__name__) -CONST_MODE_SMART_SCHEDULE = "SMART_SCHEDULE" # Default mytado mode -CONST_MODE_OFF = "OFF" # Switch off heating in a zone - -# When we change the temperature setting, we need an overlay mode -# wait until tado changes the mode automatic -CONST_OVERLAY_TADO_MODE = "TADO_MODE" -# the user has change the temperature or mode manually -CONST_OVERLAY_MANUAL = "MANUAL" -# the temperature will be reset after a timespan -CONST_OVERLAY_TIMER = "TIMER" - -CONST_MODE_FAN_HIGH = "HIGH" -CONST_MODE_FAN_MIDDLE = "MIDDLE" -CONST_MODE_FAN_LOW = "LOW" - FAN_MAP_TADO = {"HIGH": FAN_HIGH, "MIDDLE": FAN_MIDDLE, "LOW": FAN_LOW} HVAC_MAP_TADO_HEAT = { "MANUAL": HVAC_MODE_HEAT, - "TIMER": HVAC_MODE_AUTO, - "TADO_MODE": HVAC_MODE_AUTO, + "TIMER": HVAC_MODE_HEAT, + "TADO_MODE": HVAC_MODE_HEAT, "SMART_SCHEDULE": HVAC_MODE_AUTO, "OFF": HVAC_MODE_OFF, } HVAC_MAP_TADO_COOL = { "MANUAL": HVAC_MODE_COOL, - "TIMER": HVAC_MODE_AUTO, - "TADO_MODE": HVAC_MODE_AUTO, + "TIMER": HVAC_MODE_COOL, + "TADO_MODE": HVAC_MODE_COOL, "SMART_SCHEDULE": HVAC_MODE_AUTO, "OFF": HVAC_MODE_OFF, } HVAC_MAP_TADO_HEAT_COOL = { "MANUAL": HVAC_MODE_HEAT_COOL, - "TIMER": HVAC_MODE_AUTO, - "TADO_MODE": HVAC_MODE_AUTO, + "TIMER": HVAC_MODE_HEAT_COOL, + "TADO_MODE": HVAC_MODE_HEAT_COOL, "SMART_SCHEDULE": HVAC_MODE_AUTO, "OFF": HVAC_MODE_OFF, } @@ -78,34 +70,29 @@ SUPPORT_PRESET = [PRESET_AWAY, PRESET_HOME] def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Tado climate platform.""" - tado = hass.data[DATA_TADO] + tado = hass.data[DOMAIN] - try: - zones = tado.get_zones() - except RuntimeError: - _LOGGER.error("Unable to get zone info from mytado") - return + entities = [] + for zone in tado.zones: + entity = create_climate_entity( + tado, zone["name"], zone["id"], discovery_info[CONF_FALLBACK] + ) + if entity: + entities.append(entity) - climate_devices = [] - for zone in zones: - device = create_climate_device(tado, hass, zone, zone["name"], zone["id"]) - if not device: - continue - climate_devices.append(device) - - if climate_devices: - add_entities(climate_devices, True) + if entities: + add_entities(entities, True) -def create_climate_device(tado, hass, zone, name, zone_id): - """Create a Tado climate device.""" +def create_climate_entity(tado, name: str, zone_id: int, fallback: bool): + """Create a Tado climate entity.""" capabilities = tado.get_capabilities(zone_id) + _LOGGER.debug("Capabilities for zone %s: %s", zone_id, capabilities) + + zone_type = capabilities["type"] - unit = TEMP_CELSIUS - ac_device = capabilities["type"] == "AIR_CONDITIONING" ac_support_heat = False - - if ac_device: + if zone_type == TYPE_AIR_CONDITIONING: # Only use heat if available # (you don't have to setup a heat mode, but cool is required) # Heat is preferred as it generally has a lower minimum temperature @@ -117,64 +104,56 @@ def create_climate_device(tado, hass, zone, name, zone_id): elif "temperatures" in capabilities: temperatures = capabilities["temperatures"] else: - _LOGGER.debug("Received zone %s has no temperature; not adding", name) - return + _LOGGER.debug("Not adding zone %s since it has no temperature", name) + return None min_temp = float(temperatures["celsius"]["min"]) max_temp = float(temperatures["celsius"]["max"]) step = temperatures["celsius"].get("step", PRECISION_TENTHS) - data_id = f"zone {name} {zone_id}" - device = TadoClimate( + entity = TadoClimate( tado, name, zone_id, - data_id, - hass.config.units.temperature(min_temp, unit), - hass.config.units.temperature(max_temp, unit), - step, - ac_device, - ac_support_heat, - ) - - tado.add_sensor( - data_id, {"id": zone_id, "zone": zone, "name": name, "climate": device} - ) - - return device - - -class TadoClimate(ClimateDevice): - """Representation of a Tado climate device.""" - - def __init__( - self, - store, - zone_name, - zone_id, - data_id, + zone_type, min_temp, max_temp, step, - ac_device, ac_support_heat, - tolerance=0.3, + fallback, + ) + return entity + + +class TadoClimate(ClimateDevice): + """Representation of a Tado climate entity.""" + + def __init__( + self, + tado, + zone_name, + zone_id, + zone_type, + min_temp, + max_temp, + step, + ac_support_heat, + fallback, ): - """Initialize of Tado climate device.""" - self._store = store - self._data_id = data_id + """Initialize of Tado climate entity.""" + self._tado = tado self.zone_name = zone_name self.zone_id = zone_id + self.zone_type = zone_type - self._ac_device = ac_device + self._ac_device = zone_type == TYPE_AIR_CONDITIONING self._ac_support_heat = ac_support_heat self._cooling = False self._active = False self._device_is_active = False - self._unit = TEMP_CELSIUS self._cur_temp = None self._cur_humidity = None self._is_away = False @@ -182,12 +161,34 @@ class TadoClimate(ClimateDevice): self._max_temp = max_temp self._step = step self._target_temp = None - self._tolerance = tolerance + + if fallback: + _LOGGER.debug("Default overlay is set to TADO MODE") + # Fallback to Smart Schedule at next Schedule switch + self._default_overlay = CONST_OVERLAY_TADO_MODE + else: + _LOGGER.debug("Default overlay is set to MANUAL MODE") + # Don't fallback to Smart Schedule, but keep in manual mode + self._default_overlay = CONST_OVERLAY_MANUAL self._current_fan = CONST_MODE_OFF self._current_operation = CONST_MODE_SMART_SCHEDULE self._overlay_mode = CONST_MODE_SMART_SCHEDULE + async def async_added_to_hass(self): + """Register for sensor updates.""" + + @callback + def async_update_callback(): + """Schedule an entity update.""" + self.async_schedule_update_ha_state(True) + + async_dispatcher_connect( + self.hass, + SIGNAL_TADO_UPDATE_RECEIVED.format("zone", self.zone_id), + async_update_callback, + ) + @property def supported_features(self): """Return the list of supported features.""" @@ -195,18 +196,19 @@ class TadoClimate(ClimateDevice): @property def name(self): - """Return the name of the device.""" + """Return the name of the entity.""" return self.zone_name + @property + def should_poll(self) -> bool: + """Do not poll.""" + return False + @property def current_humidity(self): """Return the current humidity.""" return self._cur_humidity - def set_humidity(self, humidity: int) -> None: - """Set new target humidity.""" - pass - @property def current_temperature(self): """Return the sensor temperature.""" @@ -230,9 +232,9 @@ class TadoClimate(ClimateDevice): Need to be a subset of HVAC_MODES. """ - if self._ac_device and self._ac_support_heat: - return SUPPORT_HVAC_HEAT_COOL - if self._ac_device and not self._ac_support_heat: + if self._ac_device: + if self._ac_support_heat: + return SUPPORT_HVAC_HEAT_COOL return SUPPORT_HVAC_COOL return SUPPORT_HVAC_HEAT @@ -244,16 +246,10 @@ class TadoClimate(ClimateDevice): """ if not self._device_is_active: return CURRENT_HVAC_OFF - if self._ac_device and self._ac_support_heat and self._cooling: - if self._active: - return CURRENT_HVAC_COOL - return CURRENT_HVAC_IDLE - if self._ac_device and self._ac_support_heat and not self._cooling: - if self._active: - return CURRENT_HVAC_HEAT - return CURRENT_HVAC_IDLE - if self._ac_device and not self._ac_support_heat: + if self._ac_device: if self._active: + if self._ac_support_heat and not self._cooling: + return CURRENT_HVAC_HEAT return CURRENT_HVAC_COOL return CURRENT_HVAC_IDLE if self._active: @@ -280,7 +276,7 @@ class TadoClimate(ClimateDevice): @property def preset_mode(self): - """Return the current preset mode, e.g., home, away, temp.""" + """Return the current preset mode (home, away).""" if self._is_away: return PRESET_AWAY return PRESET_HOME @@ -297,7 +293,7 @@ class TadoClimate(ClimateDevice): @property def temperature_unit(self): """Return the unit of measurement used by the platform.""" - return self._unit + return TEMP_CELSIUS @property def target_temperature_step(self): @@ -309,23 +305,13 @@ class TadoClimate(ClimateDevice): """Return the temperature we try to reach.""" return self._target_temp - @property - def target_temperature_high(self): - """Return the upper bound temperature we try to reach.""" - return None - - @property - def target_temperature_low(self): - """Return the lower bound temperature we try to reach.""" - return None - def set_temperature(self, **kwargs): """Set new target temperature.""" temperature = kwargs.get(ATTR_TEMPERATURE) if temperature is None: return - self._current_operation = CONST_OVERLAY_MANUAL + self._current_operation = self._default_overlay self._overlay_mode = None self._target_temp = temperature self._control_heating() @@ -339,50 +325,51 @@ class TadoClimate(ClimateDevice): elif hvac_mode == HVAC_MODE_AUTO: mode = CONST_MODE_SMART_SCHEDULE elif hvac_mode == HVAC_MODE_HEAT: - mode = CONST_OVERLAY_MANUAL + mode = self._default_overlay elif hvac_mode == HVAC_MODE_COOL: - mode = CONST_OVERLAY_MANUAL + mode = self._default_overlay elif hvac_mode == HVAC_MODE_HEAT_COOL: - mode = CONST_OVERLAY_MANUAL + mode = self._default_overlay self._current_operation = mode self._overlay_mode = None - if self._target_temp is None and self._ac_device: - self._target_temp = 27 + + # Set a target temperature if we don't have any + # This can happen when we switch from Off to On + if self._target_temp is None: + if self._ac_device: + self._target_temp = self.max_temp + else: + self._target_temp = self.min_temp + self.schedule_update_ha_state() + self._control_heating() @property def min_temp(self): """Return the minimum temperature.""" - return convert_temperature( - self._min_temp, self._unit, self.hass.config.units.temperature_unit - ) + return self._min_temp @property def max_temp(self): """Return the maximum temperature.""" - return convert_temperature( - self._max_temp, self._unit, self.hass.config.units.temperature_unit - ) + return self._max_temp def update(self): - """Update the state of this climate device.""" - self._store.update() - - data = self._store.get_data(self._data_id) - - if data is None: - _LOGGER.debug("Received no data for zone %s", self.zone_name) + """Handle update callbacks.""" + _LOGGER.debug("Updating climate platform for zone %d", self.zone_id) + try: + data = self._tado.data["zone"][self.zone_id] + except KeyError: + _LOGGER.debug("No data") return if "sensorDataPoints" in data: sensor_data = data["sensorDataPoints"] - unit = TEMP_CELSIUS - if "insideTemperature" in sensor_data: temperature = float(sensor_data["insideTemperature"]["celsius"]) - self._cur_temp = self.hass.config.units.temperature(temperature, unit) + self._cur_temp = temperature if "humidity" in sensor_data: humidity = float(sensor_data["humidity"]["percentage"]) @@ -394,7 +381,7 @@ class TadoClimate(ClimateDevice): and data["setting"]["temperature"] is not None ): setting = float(data["setting"]["temperature"]["celsius"]) - self._target_temp = self.hass.config.units.temperature(setting, unit) + self._target_temp = setting if "tadoMode" in data: mode = data["tadoMode"] @@ -464,111 +451,38 @@ class TadoClimate(ClimateDevice): self._current_fan = fan_speed def _control_heating(self): - """Send new target temperature to mytado.""" - if None not in (self._cur_temp, self._target_temp): - _LOGGER.info( - "Obtained current (%d) and target temperature (%d). " - "Tado thermostat active", - self._cur_temp, - self._target_temp, - ) - + """Send new target temperature to Tado.""" if self._current_operation == CONST_MODE_SMART_SCHEDULE: - _LOGGER.info( - "Switching mytado.com to SCHEDULE (default) for zone %s (%d)", + _LOGGER.debug( + "Switching to SMART_SCHEDULE for zone %s (%d)", self.zone_name, self.zone_id, ) - self._store.reset_zone_overlay(self.zone_id) + self._tado.reset_zone_overlay(self.zone_id) self._overlay_mode = self._current_operation return if self._current_operation == CONST_MODE_OFF: - if self._ac_device: - _LOGGER.info( - "Switching mytado.com to OFF for zone %s (%d) - AIR_CONDITIONING", - self.zone_name, - self.zone_id, - ) - self._store.set_zone_off( - self.zone_id, CONST_OVERLAY_MANUAL, "AIR_CONDITIONING" - ) - else: - _LOGGER.info( - "Switching mytado.com to OFF for zone %s (%d) - HEATING", - self.zone_name, - self.zone_id, - ) - self._store.set_zone_off(self.zone_id, CONST_OVERLAY_MANUAL, "HEATING") + _LOGGER.debug( + "Switching to OFF for zone %s (%d)", self.zone_name, self.zone_id + ) + self._tado.set_zone_off(self.zone_id, CONST_OVERLAY_MANUAL, self.zone_type) self._overlay_mode = self._current_operation return - if self._ac_device: - _LOGGER.info( - "Switching mytado.com to %s mode for zone %s (%d). Temp (%s) - AIR_CONDITIONING", - self._current_operation, - self.zone_name, - self.zone_id, - self._target_temp, - ) - self._store.set_zone_overlay( - self.zone_id, - self._current_operation, - self._target_temp, - None, - "AIR_CONDITIONING", - "COOL", - ) - else: - _LOGGER.info( - "Switching mytado.com to %s mode for zone %s (%d). Temp (%s) - HEATING", - self._current_operation, - self.zone_name, - self.zone_id, - self._target_temp, - ) - self._store.set_zone_overlay( - self.zone_id, - self._current_operation, - self._target_temp, - None, - "HEATING", - ) - + _LOGGER.debug( + "Switching to %s for zone %s (%d) with temperature %s °C", + self._current_operation, + self.zone_name, + self.zone_id, + self._target_temp, + ) + self._tado.set_zone_overlay( + self.zone_id, + self._current_operation, + self._target_temp, + None, + self.zone_type, + "COOL" if self._ac_device else None, + ) self._overlay_mode = self._current_operation - - @property - def is_aux_heat(self) -> Optional[bool]: - """Return true if aux heater. - - Requires SUPPORT_AUX_HEAT. - """ - return None - - def turn_aux_heat_on(self) -> None: - """Turn auxiliary heater on.""" - pass - - def turn_aux_heat_off(self) -> None: - """Turn auxiliary heater off.""" - pass - - @property - def swing_mode(self) -> Optional[str]: - """Return the swing setting. - - Requires SUPPORT_SWING_MODE. - """ - return None - - @property - def swing_modes(self) -> Optional[List[str]]: - """Return the list of available swing modes. - - Requires SUPPORT_SWING_MODE. - """ - return None - - def set_swing_mode(self, swing_mode: str) -> None: - """Set new target swing operation.""" - pass diff --git a/homeassistant/components/tado/const.py b/homeassistant/components/tado/const.py new file mode 100644 index 00000000000..3c0232c8ba2 --- /dev/null +++ b/homeassistant/components/tado/const.py @@ -0,0 +1,18 @@ +"""Constant values for the Tado component.""" + +# Configuration +CONF_FALLBACK = "fallback" + +# Types +TYPE_AIR_CONDITIONING = "AIR_CONDITIONING" +TYPE_HEATING = "HEATING" +TYPE_HOT_WATER = "HOT_WATER" + +# Base modes +CONST_MODE_SMART_SCHEDULE = "SMART_SCHEDULE" # Use the schedule +CONST_MODE_OFF = "OFF" # Switch off heating in a zone + +# When we change the temperature setting, we need an overlay mode +CONST_OVERLAY_TADO_MODE = "TADO_MODE" # wait until tado changes the mode automatic +CONST_OVERLAY_MANUAL = "MANUAL" # the user has change the temperature or mode manually +CONST_OVERLAY_TIMER = "TIMER" # the temperature will be reset after a timespan diff --git a/homeassistant/components/tado/device_tracker.py b/homeassistant/components/tado/device_tracker.py index 797692bf32c..ea797754da8 100644 --- a/homeassistant/components/tado/device_tracker.py +++ b/homeassistant/components/tado/device_tracker.py @@ -1,22 +1,22 @@ """Support for Tado Smart device trackers.""" -import logging -from datetime import timedelta -from collections import namedtuple - import asyncio +from collections import namedtuple +from datetime import timedelta +import logging + import aiohttp import async_timeout import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD -from homeassistant.util import Throttle from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner, ) +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers.aiohttp_client import async_create_clientsession +import homeassistant.helpers.config_validation as cv +from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -60,9 +60,7 @@ class TadoDeviceScanner(DeviceScanner): if self.home_id is None: self.tadoapiurl = "https://my.tado.com/api/v2/me" else: - self.tadoapiurl = ( - "https://my.tado.com/api/v2" "/homes/{home_id}/mobileDevices" - ) + self.tadoapiurl = "https://my.tado.com/api/v2/homes/{home_id}/mobileDevices" # The API URL always needs a username and password self.tadoapiurl += "?username={username}&password={password}" diff --git a/homeassistant/components/tado/manifest.json b/homeassistant/components/tado/manifest.json index 9a884fa010c..4728f1622ed 100644 --- a/homeassistant/components/tado/manifest.json +++ b/homeassistant/components/tado/manifest.json @@ -6,5 +6,7 @@ "python-tado==0.2.9" ], "dependencies": [], - "codeowners": [] + "codeowners": [ + "@michaelarnauts" + ] } diff --git a/homeassistant/components/tado/sensor.py b/homeassistant/components/tado/sensor.py index 7b4bd643f3d..a928b61a508 100644 --- a/homeassistant/components/tado/sensor.py +++ b/homeassistant/components/tado/sensor.py @@ -1,129 +1,110 @@ """Support for Tado sensors for each zone.""" import logging -from homeassistant.const import ATTR_ID, ATTR_NAME, TEMP_CELSIUS +from homeassistant.const import TEMP_CELSIUS +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from . import DATA_TADO +from . import DOMAIN, SIGNAL_TADO_UPDATE_RECEIVED +from .const import TYPE_AIR_CONDITIONING, TYPE_HEATING, TYPE_HOT_WATER _LOGGER = logging.getLogger(__name__) -ATTR_DATA_ID = "data_id" -ATTR_DEVICE = "device" -ATTR_ZONE = "zone" +ZONE_SENSORS = { + TYPE_HEATING: [ + "temperature", + "humidity", + "power", + "link", + "heating", + "tado mode", + "overlay", + "early start", + "open window", + ], + TYPE_AIR_CONDITIONING: [ + "temperature", + "humidity", + "power", + "link", + "ac", + "tado mode", + "overlay", + ], + TYPE_HOT_WATER: ["power", "link", "tado mode", "overlay"], +} -CLIMATE_HEAT_SENSOR_TYPES = [ - "temperature", - "humidity", - "power", - "link", - "heating", - "tado mode", - "overlay", -] - -CLIMATE_COOL_SENSOR_TYPES = [ - "temperature", - "humidity", - "power", - "link", - "ac", - "tado mode", - "overlay", -] - -HOT_WATER_SENSOR_TYPES = ["power", "link", "tado mode", "overlay"] +DEVICE_SENSORS = ["tado bridge status"] def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the sensor platform.""" - tado = hass.data[DATA_TADO] + tado = hass.data[DOMAIN] - try: - zones = tado.get_zones() - except RuntimeError: - _LOGGER.error("Unable to get zone info from mytado") - return - - sensor_items = [] - for zone in zones: - if zone["type"] == "HEATING": - for variable in CLIMATE_HEAT_SENSOR_TYPES: - sensor_items.append( - create_zone_sensor(tado, zone, zone["name"], zone["id"], variable) - ) - elif zone["type"] == "HOT_WATER": - for variable in HOT_WATER_SENSOR_TYPES: - sensor_items.append( - create_zone_sensor(tado, zone, zone["name"], zone["id"], variable) - ) - elif zone["type"] == "AIR_CONDITIONING": - for variable in CLIMATE_COOL_SENSOR_TYPES: - sensor_items.append( - create_zone_sensor(tado, zone, zone["name"], zone["id"], variable) - ) - - me_data = tado.get_me() - sensor_items.append( - create_device_sensor( - tado, - me_data, - me_data["homes"][0]["name"], - me_data["homes"][0]["id"], - "tado bridge status", + # Create zone sensors + entities = [] + for zone in tado.zones: + entities.extend( + [ + create_zone_sensor(tado, zone["name"], zone["id"], variable) + for variable in ZONE_SENSORS.get(zone["type"]) + ] ) - ) - if sensor_items: - add_entities(sensor_items, True) + # Create device sensors + for home in tado.devices: + entities.extend( + [ + create_device_sensor(tado, home["name"], home["id"], variable) + for variable in DEVICE_SENSORS + ] + ) + + add_entities(entities, True) -def create_zone_sensor(tado, zone, name, zone_id, variable): +def create_zone_sensor(tado, name, zone_id, variable): """Create a zone sensor.""" - data_id = f"zone {name} {zone_id}" - - tado.add_sensor( - data_id, - {ATTR_ZONE: zone, ATTR_NAME: name, ATTR_ID: zone_id, ATTR_DATA_ID: data_id}, - ) - - return TadoSensor(tado, name, zone_id, variable, data_id) + return TadoSensor(tado, name, "zone", zone_id, variable) -def create_device_sensor(tado, device, name, device_id, variable): +def create_device_sensor(tado, name, device_id, variable): """Create a device sensor.""" - data_id = f"device {name} {device_id}" - - tado.add_sensor( - data_id, - { - ATTR_DEVICE: device, - ATTR_NAME: name, - ATTR_ID: device_id, - ATTR_DATA_ID: data_id, - }, - ) - - return TadoSensor(tado, name, device_id, variable, data_id) + return TadoSensor(tado, name, "device", device_id, variable) class TadoSensor(Entity): """Representation of a tado Sensor.""" - def __init__(self, store, zone_name, zone_id, zone_variable, data_id): + def __init__(self, tado, zone_name, sensor_type, zone_id, zone_variable): """Initialize of the Tado Sensor.""" - self._store = store + self._tado = tado self.zone_name = zone_name self.zone_id = zone_id self.zone_variable = zone_variable + self.sensor_type = sensor_type self._unique_id = f"{zone_variable} {zone_id}" - self._data_id = data_id self._state = None self._state_attributes = None + async def async_added_to_hass(self): + """Register for sensor updates.""" + + @callback + def async_update_callback(): + """Schedule an entity update.""" + self.async_schedule_update_ha_state(True) + + async_dispatcher_connect( + self.hass, + SIGNAL_TADO_UPDATE_RECEIVED.format(self.sensor_type, self.zone_id), + async_update_callback, + ) + @property def unique_id(self): """Return the unique id.""" @@ -164,14 +145,16 @@ class TadoSensor(Entity): if self.zone_variable == "humidity": return "mdi:water-percent" + @property + def should_poll(self) -> bool: + """Do not poll.""" + return False + def update(self): - """Update method called when should_poll is true.""" - self._store.update() - - data = self._store.get_data(self._data_id) - - if data is None: - _LOGGER.debug("Received no data for zone %s", self.zone_name) + """Handle update callbacks.""" + try: + data = self._tado.data[self.sensor_type][self.zone_id] + except KeyError: return unit = TEMP_CELSIUS @@ -252,3 +235,15 @@ class TadoSensor(Entity): else: self._state = False self._state_attributes = {} + + elif self.zone_variable == "early start": + if "preparation" in data and data["preparation"] is not None: + self._state = True + else: + self._state = False + + elif self.zone_variable == "open window": + if "openWindowDetected" in data: + self._state = data["openWindowDetected"] + else: + self._state = False diff --git a/homeassistant/components/tahoma/__init__.py b/homeassistant/components/tahoma/__init__.py index 9fc8ca3cf2e..6bb4fc200af 100644 --- a/homeassistant/components/tahoma/__init__.py +++ b/homeassistant/components/tahoma/__init__.py @@ -1,12 +1,13 @@ """Support for Tahoma devices.""" from collections import defaultdict import logging -import voluptuous as vol -from requests.exceptions import RequestException -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_EXCLUDE -from homeassistant.helpers import discovery -from homeassistant.helpers import config_validation as cv +from requests.exceptions import RequestException +from tahoma_api import Action, TahomaApi +import voluptuous as vol + +from homeassistant.const import CONF_EXCLUDE, CONF_PASSWORD, CONF_USERNAME +from homeassistant.helpers import config_validation as cv, discovery from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -44,7 +45,9 @@ TAHOMA_TYPES = { "io:RollerShutterWithLowSpeedManagementIOComponent": "cover", "io:SomfyBasicContactIOSystemSensor": "sensor", "io:SomfyContactIOSystemSensor": "sensor", + "io:TemperatureIOSystemSensor": "sensor", "io:VerticalExteriorAwningIOComponent": "cover", + "io:VerticalInteriorBlindVeluxIOComponent": "cover", "io:WindowOpenerVeluxIOComponent": "cover", "io:GarageOpenerIOComponent": "cover", "io:DiscreteGarageOpenerIOComponent": "cover", @@ -57,13 +60,13 @@ TAHOMA_TYPES = { "rts:ExteriorVenetianBlindRTSComponent": "cover", "rts:GarageDoor4TRTSComponent": "switch", "rts:RollerShutterRTSComponent": "cover", + "rts:OnOffRTSComponent": "switch", "rts:VenetianBlindRTSComponent": "cover", } def setup(hass, config): """Activate Tahoma component.""" - from tahoma_api import TahomaApi conf = config[DOMAIN] username = conf.get(CONF_USERNAME) @@ -133,7 +136,6 @@ class TahomaDevice(Entity): def apply_action(self, cmd_name, *args): """Apply Action to Device.""" - from tahoma_api import Action action = Action(self.tahoma_device.url) action.add_command(cmd_name, *args) diff --git a/homeassistant/components/tahoma/cover.py b/homeassistant/components/tahoma/cover.py index 6c5dcbd807c..e11c2f4cdf5 100644 --- a/homeassistant/components/tahoma/cover.py +++ b/homeassistant/components/tahoma/cover.py @@ -35,6 +35,7 @@ TAHOMA_DEVICE_CLASSES = { "io:RollerShutterVeluxIOComponent": DEVICE_CLASS_SHUTTER, "io:RollerShutterWithLowSpeedManagementIOComponent": DEVICE_CLASS_SHUTTER, "io:VerticalExteriorAwningIOComponent": DEVICE_CLASS_AWNING, + "io:VerticalInteriorBlindVeluxIOComponent": DEVICE_CLASS_BLIND, "io:WindowOpenerVeluxIOComponent": DEVICE_CLASS_WINDOW, "io:GarageOpenerIOComponent": DEVICE_CLASS_GARAGE, "io:DiscreteGarageOpenerIOComponent": DEVICE_CLASS_GARAGE, @@ -163,10 +164,15 @@ class TahomaCover(TahomaDevice, CoverDevice): def set_cover_position(self, **kwargs): """Move the cover to a specific position.""" - if self.tahoma_device.type == HORIZONTAL_AWNING: - self.apply_action("setPosition", kwargs.get(ATTR_POSITION, 0)) + if self.tahoma_device.type == "io:WindowOpenerVeluxIOComponent": + command = "setClosure" else: - self.apply_action("setPosition", 100 - kwargs.get(ATTR_POSITION, 0)) + command = "setPosition" + + if self.tahoma_device.type == HORIZONTAL_AWNING: + self.apply_action(command, kwargs.get(ATTR_POSITION, 0)) + else: + self.apply_action(command, 100 - kwargs.get(ATTR_POSITION, 0)) @property def is_closed(self): @@ -235,6 +241,8 @@ class TahomaCover(TahomaDevice, CoverDevice): HORIZONTAL_AWNING, "io:RollerShutterGenericIOComponent", "io:VerticalExteriorAwningIOComponent", + "io:VerticalInteriorBlindVeluxIOComponent", + "io:WindowOpenerVeluxIOComponent", ): self.apply_action("stop") else: diff --git a/homeassistant/components/tahoma/manifest.json b/homeassistant/components/tahoma/manifest.json index 1e99d4b288d..4efe9aed97f 100644 --- a/homeassistant/components/tahoma/manifest.json +++ b/homeassistant/components/tahoma/manifest.json @@ -3,7 +3,7 @@ "name": "Tahoma", "documentation": "https://www.home-assistant.io/integrations/tahoma", "requirements": [ - "tahoma-api==0.0.14" + "tahoma-api==0.0.16" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/tahoma/sensor.py b/homeassistant/components/tahoma/sensor.py index 5279b160d9c..85ccb55761d 100644 --- a/homeassistant/components/tahoma/sensor.py +++ b/homeassistant/components/tahoma/sensor.py @@ -2,7 +2,7 @@ from datetime import timedelta import logging -from homeassistant.const import ATTR_BATTERY_LEVEL +from homeassistant.const import ATTR_BATTERY_LEVEL, TEMP_CELSIUS from homeassistant.helpers.entity import Entity from . import DOMAIN as TAHOMA_DOMAIN, TahomaDevice @@ -40,8 +40,8 @@ class TahomaSensor(TahomaDevice, Entity): @property def unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" - if self.tahoma_device.type == "Temperature Sensor": - return None + if self.tahoma_device.type == "io:TemperatureIOSystemSensor": + return TEMP_CELSIUS if self.tahoma_device.type == "io:SomfyContactIOSystemSensor": return None if self.tahoma_device.type == "io:SomfyBasicContactIOSystemSensor": @@ -79,6 +79,11 @@ class TahomaSensor(TahomaDevice, Entity): if self.tahoma_device.type == "rtds:RTDSMotionSensor": self.current_value = self.tahoma_device.active_states["core:OccupancyState"] self._available = True + if self.tahoma_device.type == "io:TemperatureIOSystemSensor": + self.current_value = round( + float(self.tahoma_device.active_states["core:TemperatureState"]), 1 + ) + self._available = True _LOGGER.debug("Update %s, value: %d", self._name, self.current_value) diff --git a/homeassistant/components/tahoma/switch.py b/homeassistant/components/tahoma/switch.py index a0a95ab47ce..1612120f313 100644 --- a/homeassistant/components/tahoma/switch.py +++ b/homeassistant/components/tahoma/switch.py @@ -45,9 +45,14 @@ class TahomaSwitch(TahomaDevice, SwitchDevice): else: self._state = STATE_OFF - self._available = bool( - self.tahoma_device.active_states.get("core:StatusState") == "available" - ) + # A RTS power socket doesn't have a feedback channel, + # so we must assume the socket is available. + if self.tahoma_device.type == "rts:OnOffRTSComponent": + self._available = True + else: + self._available = bool( + self.tahoma_device.active_states.get("core:StatusState") == "available" + ) _LOGGER.debug("Update %s, state: %s", self._name, self._state) diff --git a/homeassistant/components/tank_utility/sensor.py b/homeassistant/components/tank_utility/sensor.py index 78fcd47099d..23446257eab 100644 --- a/homeassistant/components/tank_utility/sensor.py +++ b/homeassistant/components/tank_utility/sensor.py @@ -4,14 +4,14 @@ import datetime import logging import requests +from tank_utility import auth, device as tank_monitor import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_DEVICES, CONF_EMAIL, CONF_PASSWORD +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity - _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = datetime.timedelta(hours=1) @@ -41,7 +41,6 @@ SENSOR_ATTRS = [ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Tank Utility sensor.""" - from tank_utility import auth email = config.get(CONF_EMAIL) password = config.get(CONF_PASSWORD) @@ -109,19 +108,20 @@ class TankUtilitySensor(Entity): Flatten dictionary to map device to map of device data. """ - from tank_utility import auth, device data = {} try: - data = device.get_device_data(self._token, self.device) + data = tank_monitor.get_device_data(self._token, self.device) except requests.exceptions.HTTPError as http_error: if ( http_error.response.status_code == requests.codes.unauthorized # pylint: disable=no-member + or http_error.response.status_code + == requests.codes.bad_request # pylint: disable=no-member ): _LOGGER.info("Getting new token") self._token = auth.get_token(self._email, self._password, force=True) - data = device.get_device_data(self._token, self.device) + data = tank_monitor.get_device_data(self._token, self.device) else: raise http_error data.update(data.pop("device", {})) diff --git a/homeassistant/components/tapsaff/binary_sensor.py b/homeassistant/components/tapsaff/binary_sensor.py index fe6b01ced4e..e54bc7298b0 100644 --- a/homeassistant/components/tapsaff/binary_sensor.py +++ b/homeassistant/components/tapsaff/binary_sensor.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +from tapsaff import TapsAff import voluptuous as vol from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice @@ -62,7 +63,6 @@ class TapsAffData: def __init__(self, location): """Initialize the data object.""" - from tapsaff import TapsAff self._is_taps_aff = None self.taps_aff = TapsAff(location) diff --git a/homeassistant/components/tautulli/sensor.py b/homeassistant/components/tautulli/sensor.py index 14b67838906..b800bf6af1e 100644 --- a/homeassistant/components/tautulli/sensor.py +++ b/homeassistant/components/tautulli/sensor.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +from pytautulli import Tautulli import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -10,10 +11,10 @@ from homeassistant.const import ( CONF_HOST, CONF_MONITORED_CONDITIONS, CONF_NAME, + CONF_PATH, CONF_PORT, CONF_SSL, CONF_VERIFY_SSL, - CONF_PATH, ) from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -50,7 +51,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Create the Tautulli sensor.""" - from pytautulli import Tautulli name = config.get(CONF_NAME) host = config[CONF_HOST] diff --git a/homeassistant/components/tcp/sensor.py b/homeassistant/components/tcp/sensor.py index a387b3fc0bb..2732f2d6bd1 100644 --- a/homeassistant/components/tcp/sensor.py +++ b/homeassistant/components/tcp/sensor.py @@ -1,23 +1,23 @@ """Support for TCP socket based sensors.""" import logging -import socket import select +import socket import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, CONF_HOST, - CONF_PORT, + CONF_NAME, CONF_PAYLOAD, + CONF_PORT, CONF_TIMEOUT, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, ) from homeassistant.exceptions import TemplateError -from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/teksavvy/sensor.py b/homeassistant/components/teksavvy/sensor.py index dc8b16b8ce1..fe183129eaa 100644 --- a/homeassistant/components/teksavvy/sensor.py +++ b/homeassistant/components/teksavvy/sensor.py @@ -1,8 +1,8 @@ """Support for TekSavvy Bandwidth Monitor.""" from datetime import timedelta import logging -import async_timeout +import async_timeout import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA diff --git a/homeassistant/components/telegram/notify.py b/homeassistant/components/telegram/notify.py index 23c36e3bafa..ceb660d9e1d 100644 --- a/homeassistant/components/telegram/notify.py +++ b/homeassistant/components/telegram/notify.py @@ -3,8 +3,6 @@ import logging import voluptuous as vol -from homeassistant.const import ATTR_LOCATION - from homeassistant.components.notify import ( ATTR_DATA, ATTR_MESSAGE, @@ -13,6 +11,7 @@ from homeassistant.components.notify import ( PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.const import ATTR_LOCATION _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index 7acf4985def..fc37121f3f9 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -1,8 +1,8 @@ """Support to send and receive Telegram messages.""" -import io -from ipaddress import ip_network from functools import partial import importlib +import io +from ipaddress import ip_network import logging import requests @@ -19,7 +19,6 @@ from telegram.parsemode import ParseMode from telegram.utils.request import Request import voluptuous as vol -from homeassistant.components.notify import ATTR_DATA, ATTR_MESSAGE, ATTR_TITLE from homeassistant.const import ( ATTR_COMMAND, ATTR_LATITUDE, @@ -27,14 +26,18 @@ from homeassistant.const import ( CONF_API_KEY, CONF_PLATFORM, CONF_TIMEOUT, - HTTP_DIGEST_AUTHENTICATION, CONF_URL, + HTTP_DIGEST_AUTHENTICATION, ) -import homeassistant.helpers.config_validation as cv from homeassistant.exceptions import TemplateError +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) +ATTR_DATA = "data" +ATTR_MESSAGE = "message" +ATTR_TITLE = "title" + ATTR_ARGS = "args" ATTR_AUTHENTICATION = "authentication" ATTR_CALLBACK_QUERY = "callback_query" @@ -625,7 +628,7 @@ class TelegramNotificationService: """Answer a callback originated with a press in an inline keyboard.""" params = self._get_msg_kwargs(kwargs) _LOGGER.debug( - "Answer callback query with callback ID %s: %s, " "alert: %s.", + "Answer callback query with callback ID %s: %s, alert: %s.", callback_query_id, message, show_alert, @@ -731,6 +734,8 @@ class BaseTelegramBotEntity: ATTR_USER_ID: msg_data["from"]["id"], ATTR_FROM_FIRST: msg_data["from"]["first_name"], } + if "message_id" in msg_data: + data[ATTR_MSGID] = msg_data["message_id"] if "last_name" in msg_data["from"]: data[ATTR_FROM_LAST] = msg_data["from"]["last_name"] if "chat" in msg_data: @@ -752,6 +757,9 @@ class BaseTelegramBotEntity: if event_data is None: return message_ok + if ATTR_MSGID in data: + event_data[ATTR_MSGID] = data[ATTR_MSGID] + if "text" in data: if data["text"][0] == "/": pieces = data["text"].split(" ") diff --git a/homeassistant/components/telegram_bot/polling.py b/homeassistant/components/telegram_bot/polling.py index 314cb31a373..8bdeef25118 100644 --- a/homeassistant/components/telegram_bot/polling.py +++ b/homeassistant/components/telegram_bot/polling.py @@ -2,8 +2,8 @@ import logging from telegram import Update -from telegram.error import TelegramError, TimedOut, NetworkError, RetryAfter -from telegram.ext import Updater, Handler +from telegram.error import NetworkError, RetryAfter, TelegramError, TimedOut +from telegram.ext import Handler, Updater from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback @@ -55,7 +55,7 @@ def message_handler(handler): """Initialize the messages handler instance.""" super().__init__(handler) - def check_update(self, update): # pylint: disable=no-self-use + def check_update(self, update): """Check is update valid.""" return isinstance(update, Update) diff --git a/homeassistant/components/tellduslive/.translations/ca.json b/homeassistant/components/tellduslive/.translations/ca.json index fafa8798401..a337474c96b 100644 --- a/homeassistant/components/tellduslive/.translations/ca.json +++ b/homeassistant/components/tellduslive/.translations/ca.json @@ -18,6 +18,7 @@ "data": { "host": "Amfitri\u00f3" }, + "description": "buit", "title": "Selecci\u00f3 extrem" } }, diff --git a/homeassistant/components/tellduslive/.translations/es.json b/homeassistant/components/tellduslive/.translations/es.json index 677e0389d45..0cee7ade0d7 100644 --- a/homeassistant/components/tellduslive/.translations/es.json +++ b/homeassistant/components/tellduslive/.translations/es.json @@ -11,7 +11,7 @@ }, "step": { "auth": { - "description": "Para vincular tu cuenta de TelldusLivet:\n 1. Pulsa el siguiente enlace\n 2. Inicia sesi\u00f3n en Telldus Live\n 3. Autoriza **{app_name}** (pulsa en **Yes**).\n 4. Vuelve aqu\u00ed y pulsa **ENVIAR**.\n\n [Link TelldusLive account]({auth_url})", + "description": "Para vincular tu cuenta de Telldus Live:\n 1. Pulsa el siguiente enlace\n 2. Inicia sesi\u00f3n en Telldus Live\n 3. Autoriza **{app_name}** (pulsa en **Yes**).\n 4. Vuelve atr\u00e1s y pulsa **ENVIAR**.\n\n [Link TelldusLive account]({auth_url})", "title": "Autenticaci\u00f3n contra TelldusLive" }, "user": { diff --git a/homeassistant/components/tellduslive/.translations/ru.json b/homeassistant/components/tellduslive/.translations/ru.json index 41dc39146e8..fa5b7e2d319 100644 --- a/homeassistant/components/tellduslive/.translations/ru.json +++ b/homeassistant/components/tellduslive/.translations/ru.json @@ -18,6 +18,7 @@ "data": { "host": "\u0425\u043e\u0441\u0442" }, + "description": "\u043f\u0443\u0441\u0442\u043e", "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043a\u043e\u043d\u0435\u0447\u043d\u0443\u044e \u0442\u043e\u0447\u043a\u0443." } }, diff --git a/homeassistant/components/tellduslive/__init__.py b/homeassistant/components/tellduslive/__init__.py index 313699e6f1c..917b927691e 100644 --- a/homeassistant/components/tellduslive/__init__.py +++ b/homeassistant/components/tellduslive/__init__.py @@ -1,15 +1,17 @@ """Support for Telldus Live.""" import asyncio -import logging from functools import partial +import logging +from tellduslive import DIM, TURNON, UP, Session import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant import config_entries from homeassistant.const import CONF_SCAN_INTERVAL +import homeassistant.helpers.config_validation as cv 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, @@ -51,7 +53,6 @@ INTERVAL_TRACKER = f"{DOMAIN}_INTERVAL" async def async_setup_entry(hass, entry): """Create a tellduslive session.""" - from tellduslive import Session conf = entry.data[KEY_SESSION] @@ -159,7 +160,6 @@ class TelldusLiveClient: """Find out what type of HA component to create.""" if device.is_sensor: return "sensor" - from tellduslive import DIM, UP, TURNON if device.methods & DIM: return "light" diff --git a/homeassistant/components/tellduslive/config_flow.py b/homeassistant/components/tellduslive/config_flow.py index 19f82dd18f4..893f3b80456 100644 --- a/homeassistant/components/tellduslive/config_flow.py +++ b/homeassistant/components/tellduslive/config_flow.py @@ -4,6 +4,7 @@ import logging import os import async_timeout +from tellduslive import Session, supports_local_api import voluptuous as vol from homeassistant import config_entries @@ -43,7 +44,6 @@ class FlowHandler(config_entries.ConfigFlow): self._scan_interval = SCAN_INTERVAL def _get_auth_url(self): - from tellduslive import Session self._session = Session( public_key=PUBLIC_KEY, @@ -116,7 +116,6 @@ class FlowHandler(config_entries.ConfigFlow): async def async_step_discovery(self, user_input): """Run when a Tellstick is discovered.""" - from tellduslive import supports_local_api _LOGGER.info("Discovered tellstick device: %s", user_input) if supports_local_api(user_input[1]): diff --git a/homeassistant/components/tellduslive/entry.py b/homeassistant/components/tellduslive/entry.py index ecd428d3b15..50a219bf7a1 100644 --- a/homeassistant/components/tellduslive/entry.py +++ b/homeassistant/components/tellduslive/entry.py @@ -2,6 +2,8 @@ from datetime import datetime import logging +from tellduslive import BATTERY_LOW, BATTERY_OK, BATTERY_UNKNOWN + from homeassistant.const import ATTR_BATTERY_LEVEL, DEVICE_DEFAULT_NAME from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -91,7 +93,6 @@ class TelldusLiveEntity(Entity): @property def _battery_level(self): """Return the battery level of a device.""" - from tellduslive import BATTERY_LOW, BATTERY_UNKNOWN, BATTERY_OK if self.device.battery == BATTERY_LOW: return 1 diff --git a/homeassistant/components/telnet/switch.py b/homeassistant/components/telnet/switch.py index 87fb70bb888..a99fe044c46 100644 --- a/homeassistant/components/telnet/switch.py +++ b/homeassistant/components/telnet/switch.py @@ -16,9 +16,9 @@ from homeassistant.const import ( CONF_COMMAND_STATE, CONF_NAME, CONF_PORT, - CONF_TIMEOUT, CONF_RESOURCE, CONF_SWITCHES, + CONF_TIMEOUT, CONF_VALUE_TEMPLATE, ) import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/temper/sensor.py b/homeassistant/components/temper/sensor.py index a32de3da10f..8b782ae4d79 100644 --- a/homeassistant/components/temper/sensor.py +++ b/homeassistant/components/temper/sensor.py @@ -1,5 +1,7 @@ """Support for getting temperature from TEMPer devices.""" import logging + +from temperusb.temper import TemperHandler import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -24,7 +26,6 @@ TEMPER_SENSORS = [] def get_temper_devices(): """Scan the Temper devices from temperusb.""" - from temperusb.temper import TemperHandler return TemperHandler().get_devices() diff --git a/homeassistant/components/template/__init__.py b/homeassistant/components/template/__init__.py index 0c205a0196c..f100d663d8c 100644 --- a/homeassistant/components/template/__init__.py +++ b/homeassistant/components/template/__init__.py @@ -1 +1,59 @@ """The template component.""" + +from itertools import chain +import logging + +from homeassistant.const import MATCH_ALL + +_LOGGER = logging.getLogger(__name__) + + +def initialise_templates(hass, templates, attribute_templates=None): + """Initialise templates and attribute templates.""" + if attribute_templates is None: + attribute_templates = dict() + for template in chain(templates.values(), attribute_templates.values()): + if template is None: + continue + template.hass = hass + + +def extract_entities( + device_name, device_type, manual_entity_ids, templates, attribute_templates=None +): + """Extract entity ids from templates and attribute templates.""" + if attribute_templates is None: + attribute_templates = dict() + entity_ids = set() + if manual_entity_ids is None: + invalid_templates = [] + for template_name, template in chain( + templates.items(), attribute_templates.items() + ): + if template is None: + continue + + template_entity_ids = template.extract_entities() + + if template_entity_ids != MATCH_ALL: + entity_ids |= set(template_entity_ids) + else: + invalid_templates.append(template_name.replace("_template", "")) + + if invalid_templates: + entity_ids = MATCH_ALL + _LOGGER.warning( + "Template %s '%s' has no entity ids configured to track nor" + " were we able to extract the entities to track from the %s " + "template(s). This entity will only be able to be updated " + "manually.", + device_type, + device_name, + ", ".join(invalid_templates), + ) + else: + entity_ids = list(entity_ids) + else: + entity_ids = manual_entity_ids + + return entity_ids diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index d5ade703c97..7de43ea0702 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -1,31 +1,32 @@ """Support for exposing a templated binary sensor.""" import logging -from itertools import chain import voluptuous as vol -from homeassistant.core import callback from homeassistant.components.binary_sensor import ( - BinarySensorDevice, + DEVICE_CLASSES_SCHEMA, ENTITY_ID_FORMAT, PLATFORM_SCHEMA, - DEVICE_CLASSES_SCHEMA, + BinarySensorDevice, ) from homeassistant.const import ( - ATTR_FRIENDLY_NAME, ATTR_ENTITY_ID, - CONF_VALUE_TEMPLATE, - CONF_ICON_TEMPLATE, - CONF_ENTITY_PICTURE_TEMPLATE, - CONF_SENSORS, + ATTR_FRIENDLY_NAME, CONF_DEVICE_CLASS, + CONF_ENTITY_PICTURE_TEMPLATE, + CONF_ICON_TEMPLATE, + CONF_SENSORS, + CONF_VALUE_TEMPLATE, EVENT_HOMEASSISTANT_START, MATCH_ALL, ) +from homeassistant.core import callback from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id -from homeassistant.helpers.event import async_track_state_change, async_track_same_state +from homeassistant.helpers.event import async_track_same_state, async_track_state_change + +from . import extract_entities, initialise_templates from .const import CONF_AVAILABILITY_TEMPLATE _LOGGER = logging.getLogger(__name__) @@ -63,11 +64,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= 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) - entity_ids = set() - manual_entity_ids = device_config.get(ATTR_ENTITY_ID) attribute_templates = device_config.get(CONF_ATTRIBUTE_TEMPLATES, {}) - invalid_templates = [] + friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device) + device_class = device_config.get(CONF_DEVICE_CLASS) + delay_on = device_config.get(CONF_DELAY_ON) + delay_off = device_config.get(CONF_DELAY_OFF) templates = { CONF_VALUE_TEMPLATE: value_template, @@ -76,41 +78,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= CONF_AVAILABILITY_TEMPLATE: availability_template, } - for tpl_name, template in chain(templates.items(), attribute_templates.items()): - if template is None: - continue - template.hass = hass - - if manual_entity_ids is not None: - continue - - template_entity_ids = template.extract_entities() - if template_entity_ids == MATCH_ALL: - entity_ids = MATCH_ALL - # Cut off _template from name - invalid_templates.append(tpl_name.replace("_template", "")) - elif entity_ids != MATCH_ALL: - entity_ids |= set(template_entity_ids) - - if manual_entity_ids is not None: - entity_ids = manual_entity_ids - elif entity_ids != MATCH_ALL: - entity_ids = list(entity_ids) - - if invalid_templates: - _LOGGER.warning( - "Template binary sensor %s has no entity ids configured to" - " track nor were we able to extract the entities to track" - " from the %s template(s). This entity will only be able" - " to be updated manually.", - device, - ", ".join(invalid_templates), - ) - - friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device) - device_class = device_config.get(CONF_DEVICE_CLASS) - delay_on = device_config.get(CONF_DELAY_ON) - delay_off = device_config.get(CONF_DELAY_OFF) + initialise_templates(hass, templates, attribute_templates) + entity_ids = extract_entities( + device, + "binary sensor", + device_config.get(ATTR_ENTITY_ID), + templates, + attribute_templates, + ) sensors.append( BinarySensorTemplate( @@ -248,7 +223,7 @@ class BinarySensorTemplate(BinarySensorDevice): ): # Common during HA startup - so just a warning _LOGGER.warning( - "Could not render template %s, " "the state is unknown", self._name + "Could not render template %s, the state is unknown", self._name ) return _LOGGER.error("Could not render template %s: %s", self._name, ex) @@ -284,7 +259,7 @@ class BinarySensorTemplate(BinarySensorDevice): ): # Common during HA startup - so just a warning _LOGGER.warning( - "Could not render %s template %s," " the state is unknown.", + "Could not render %s template %s, the state is unknown.", friendly_property_name, self._name, ) diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index 483ee1ae872..f6678067d70 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -3,41 +3,42 @@ import logging import voluptuous as vol -from homeassistant.core import callback from homeassistant.components.cover import ( - ENTITY_ID_FORMAT, - CoverDevice, - PLATFORM_SCHEMA, - DEVICE_CLASSES_SCHEMA, - SUPPORT_OPEN_TILT, - SUPPORT_CLOSE_TILT, - SUPPORT_STOP_TILT, - SUPPORT_SET_TILT_POSITION, - SUPPORT_OPEN, - SUPPORT_CLOSE, - SUPPORT_STOP, - SUPPORT_SET_POSITION, ATTR_POSITION, ATTR_TILT_POSITION, + DEVICE_CLASSES_SCHEMA, + ENTITY_ID_FORMAT, + PLATFORM_SCHEMA, + SUPPORT_CLOSE, + SUPPORT_CLOSE_TILT, + SUPPORT_OPEN, + SUPPORT_OPEN_TILT, + SUPPORT_SET_POSITION, + SUPPORT_SET_TILT_POSITION, + SUPPORT_STOP, + SUPPORT_STOP_TILT, + CoverDevice, ) from homeassistant.const import ( - CONF_FRIENDLY_NAME, - CONF_ENTITY_ID, - EVENT_HOMEASSISTANT_START, - MATCH_ALL, - CONF_VALUE_TEMPLATE, - CONF_ICON_TEMPLATE, CONF_DEVICE_CLASS, + CONF_ENTITY_ID, CONF_ENTITY_PICTURE_TEMPLATE, + CONF_FRIENDLY_NAME, + CONF_ICON_TEMPLATE, CONF_OPTIMISTIC, - STATE_OPEN, + CONF_VALUE_TEMPLATE, + EVENT_HOMEASSISTANT_START, STATE_CLOSED, + STATE_OPEN, ) +from homeassistant.core import callback from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.script import Script + +from . import extract_entities, initialise_templates from .const import CONF_AVAILABILITY_TEMPLATE _LOGGER = logging.getLogger(__name__) @@ -100,13 +101,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= covers = [] for device, device_config in config[CONF_COVERS].items(): - friendly_name = device_config.get(CONF_FRIENDLY_NAME, device) state_template = device_config.get(CONF_VALUE_TEMPLATE) position_template = device_config.get(CONF_POSITION_TEMPLATE) tilt_template = device_config.get(CONF_TILT_TEMPLATE) icon_template = device_config.get(CONF_ICON_TEMPLATE) availability_template = device_config.get(CONF_AVAILABILITY_TEMPLATE) entity_picture_template = device_config.get(CONF_ENTITY_PICTURE_TEMPLATE) + + friendly_name = device_config.get(CONF_FRIENDLY_NAME, device) device_class = device_config.get(CONF_DEVICE_CLASS) open_action = device_config.get(OPEN_ACTION) close_action = device_config.get(CLOSE_ACTION) @@ -121,41 +123,18 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= "Must specify at least one of %s" or "%s", OPEN_ACTION, POSITION_ACTION ) continue - template_entity_ids = set() - if state_template is not None: - temp_ids = state_template.extract_entities() - if str(temp_ids) != MATCH_ALL: - template_entity_ids |= set(temp_ids) - if position_template is not None: - temp_ids = position_template.extract_entities() - if str(temp_ids) != MATCH_ALL: - template_entity_ids |= set(temp_ids) + templates = { + CONF_VALUE_TEMPLATE: state_template, + CONF_POSITION_TEMPLATE: position_template, + CONF_TILT_TEMPLATE: tilt_template, + CONF_ICON_TEMPLATE: icon_template, + CONF_AVAILABILITY_TEMPLATE: availability_template, + CONF_ENTITY_PICTURE_TEMPLATE: entity_picture_template, + } - if tilt_template is not None: - temp_ids = tilt_template.extract_entities() - if str(temp_ids) != MATCH_ALL: - template_entity_ids |= set(temp_ids) - - if icon_template is not None: - temp_ids = icon_template.extract_entities() - if str(temp_ids) != MATCH_ALL: - template_entity_ids |= set(temp_ids) - - if entity_picture_template is not None: - temp_ids = entity_picture_template.extract_entities() - if str(temp_ids) != MATCH_ALL: - template_entity_ids |= set(temp_ids) - - if availability_template is not None: - temp_ids = availability_template.extract_entities() - if str(temp_ids) != MATCH_ALL: - template_entity_ids |= set(temp_ids) - - if not template_entity_ids: - template_entity_ids = MATCH_ALL - - entity_ids = device_config.get(CONF_ENTITY_ID, template_entity_ids) + initialise_templates(hass, templates) + entity_ids = extract_entities(device, "cover", None, templates) covers.append( CoverTemplate( @@ -458,8 +437,7 @@ class CoverTemplate(CoverDevice): if state < 0 or state > 100: self._tilt_value = None _LOGGER.error( - "Tilt value must be between 0 and 100." " Value was: %.2f", - state, + "Tilt value must be between 0 and 100. Value was: %.2f", state, ) else: self._tilt_value = state @@ -487,7 +465,7 @@ class CoverTemplate(CoverDevice): ): # Common during HA startup - so just a warning _LOGGER.warning( - "Could not render %s template %s," " the state is unknown.", + "Could not render %s template %s, the state is unknown.", friendly_property_name, self._name, ) diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py index 606f18e5fe1..89f54444376 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -3,36 +3,37 @@ import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.fan import ( - SPEED_LOW, - SPEED_MEDIUM, - SPEED_HIGH, - SUPPORT_SET_SPEED, - SUPPORT_OSCILLATE, - FanEntity, - ATTR_SPEED, + ATTR_DIRECTION, ATTR_OSCILLATING, - ENTITY_ID_FORMAT, - SUPPORT_DIRECTION, + ATTR_SPEED, DIRECTION_FORWARD, DIRECTION_REVERSE, - ATTR_DIRECTION, + ENTITY_ID_FORMAT, + SPEED_HIGH, + SPEED_LOW, + SPEED_MEDIUM, + SUPPORT_DIRECTION, + SUPPORT_OSCILLATE, + SUPPORT_SET_SPEED, + FanEntity, ) from homeassistant.const import ( + CONF_ENTITY_ID, CONF_FRIENDLY_NAME, CONF_VALUE_TEMPLATE, - CONF_ENTITY_ID, - STATE_ON, - STATE_OFF, - MATCH_ALL, EVENT_HOMEASSISTANT_START, + STATE_OFF, + STATE_ON, STATE_UNKNOWN, ) from homeassistant.core import callback from homeassistant.exceptions import TemplateError +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.script import Script + +from . import extract_entities, initialise_templates from .const import CONF_AVAILABILITY_TEMPLATE _LOGGER = logging.getLogger(__name__) @@ -98,33 +99,16 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= speed_list = device_config[CONF_SPEED_LIST] - entity_ids = set() - manual_entity_ids = device_config.get(CONF_ENTITY_ID) + templates = { + CONF_VALUE_TEMPLATE: state_template, + CONF_SPEED_TEMPLATE: speed_template, + CONF_OSCILLATING_TEMPLATE: oscillating_template, + CONF_DIRECTION_TEMPLATE: direction_template, + CONF_AVAILABILITY_TEMPLATE: availability_template, + } - for template in ( - state_template, - speed_template, - oscillating_template, - direction_template, - availability_template, - ): - if template is None: - continue - template.hass = hass - - if entity_ids == MATCH_ALL or manual_entity_ids is not None: - continue - - template_entity_ids = template.extract_entities() - if template_entity_ids == MATCH_ALL: - entity_ids = MATCH_ALL - else: - entity_ids |= set(template_entity_ids) - - if manual_entity_ids is not None: - entity_ids = manual_entity_ids - elif entity_ids != MATCH_ALL: - entity_ids = list(entity_ids) + initialise_templates(hass, templates) + entity_ids = extract_entities(device, "fan", None, templates) fans.append( TemplateFan( diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index 552c21f170d..f4682fa903d 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -3,31 +3,32 @@ import logging import voluptuous as vol -from homeassistant.core import callback from homeassistant.components.light import ( ATTR_BRIGHTNESS, ENTITY_ID_FORMAT, - Light, SUPPORT_BRIGHTNESS, + Light, ) from homeassistant.const import ( - CONF_VALUE_TEMPLATE, - CONF_ICON_TEMPLATE, - CONF_ENTITY_PICTURE_TEMPLATE, CONF_ENTITY_ID, + CONF_ENTITY_PICTURE_TEMPLATE, CONF_FRIENDLY_NAME, - STATE_ON, - STATE_OFF, - EVENT_HOMEASSISTANT_START, - MATCH_ALL, + CONF_ICON_TEMPLATE, CONF_LIGHTS, + CONF_VALUE_TEMPLATE, + EVENT_HOMEASSISTANT_START, + STATE_OFF, + STATE_ON, ) -from homeassistant.helpers.config_validation import PLATFORM_SCHEMA +from homeassistant.core import callback from homeassistant.exceptions import TemplateError 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.event import async_track_state_change from homeassistant.helpers.script import Script + +from . import extract_entities, initialise_templates from .const import CONF_AVAILABILITY_TEMPLATE _LOGGER = logging.getLogger(__name__) @@ -64,46 +65,27 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= for device, device_config in config[CONF_LIGHTS].items(): friendly_name = device_config.get(CONF_FRIENDLY_NAME, device) + state_template = device_config.get(CONF_VALUE_TEMPLATE) 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) + level_template = device_config.get(CONF_LEVEL_TEMPLATE) + on_action = device_config[CONF_ON_ACTION] off_action = device_config[CONF_OFF_ACTION] level_action = device_config.get(CONF_LEVEL_ACTION) - level_template = device_config.get(CONF_LEVEL_TEMPLATE) - template_entity_ids = set() + templates = { + CONF_VALUE_TEMPLATE: state_template, + CONF_ICON_TEMPLATE: icon_template, + CONF_ENTITY_PICTURE_TEMPLATE: entity_picture_template, + CONF_AVAILABILITY_TEMPLATE: availability_template, + CONF_LEVEL_TEMPLATE: level_template, + } - if state_template is not None: - temp_ids = state_template.extract_entities() - if str(temp_ids) != MATCH_ALL: - template_entity_ids |= set(temp_ids) - - if level_template is not None: - temp_ids = level_template.extract_entities() - if str(temp_ids) != MATCH_ALL: - template_entity_ids |= set(temp_ids) - - if icon_template is not None: - temp_ids = icon_template.extract_entities() - if str(temp_ids) != MATCH_ALL: - template_entity_ids |= set(temp_ids) - - if entity_picture_template is not None: - temp_ids = entity_picture_template.extract_entities() - if str(temp_ids) != MATCH_ALL: - template_entity_ids |= set(temp_ids) - - if availability_template is not None: - temp_ids = availability_template.extract_entities() - if str(temp_ids) != MATCH_ALL: - template_entity_ids |= set(temp_ids) - - if not template_entity_ids: - template_entity_ids = MATCH_ALL - - entity_ids = device_config.get(CONF_ENTITY_ID, template_entity_ids) + initialise_templates(hass, templates) + entity_ids = extract_entities(device, "light", None, templates) lights.append( LightTemplate( @@ -338,7 +320,7 @@ class LightTemplate(Light): ): # Common during HA startup - so just a warning _LOGGER.warning( - "Could not render %s template %s," " the state is unknown.", + "Could not render %s template %s, the state is unknown.", friendly_property_name, self._name, ) diff --git a/homeassistant/components/template/lock.py b/homeassistant/components/template/lock.py index aa8cc8b1224..f4a6b55dd18 100644 --- a/homeassistant/components/template/lock.py +++ b/homeassistant/components/template/lock.py @@ -3,22 +3,23 @@ import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv - -from homeassistant.core import callback -from homeassistant.components.lock import LockDevice, PLATFORM_SCHEMA +from homeassistant.components.lock import PLATFORM_SCHEMA, LockDevice from homeassistant.const import ( CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE, EVENT_HOMEASSISTANT_START, - STATE_ON, - STATE_LOCKED, MATCH_ALL, + STATE_LOCKED, + STATE_ON, ) +from homeassistant.core import callback from homeassistant.exceptions import TemplateError +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.script import Script + +from . import extract_entities, initialise_templates from .const import CONF_AVAILABILITY_TEMPLATE _LOGGER = logging.getLogger(__name__) @@ -43,39 +44,26 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the Template lock.""" - name = config.get(CONF_NAME) + device = config.get(CONF_NAME) value_template = config.get(CONF_VALUE_TEMPLATE) - value_template.hass = hass - value_template_entity_ids = value_template.extract_entities() - - if value_template_entity_ids == MATCH_ALL: - _LOGGER.warning( - "Template lock '%s' has no entity ids configured to track nor " - "were we able to extract the entities to track from the '%s' " - "template. This entity will only be able to be updated " - "manually.", - name, - CONF_VALUE_TEMPLATE, - ) - - template_entity_ids = set() - template_entity_ids |= set(value_template_entity_ids) - availability_template = config.get(CONF_AVAILABILITY_TEMPLATE) - if availability_template is not None: - availability_template.hass = hass - temp_ids = availability_template.extract_entities() - if str(temp_ids) != MATCH_ALL: - template_entity_ids |= set(temp_ids) + + templates = { + CONF_VALUE_TEMPLATE: value_template, + CONF_AVAILABILITY_TEMPLATE: availability_template, + } + + initialise_templates(hass, templates) + entity_ids = extract_entities(device, "lock", None, templates) async_add_devices( [ TemplateLock( hass, - name, + device, value_template, availability_template, - template_entity_ids, + entity_ids, config.get(CONF_LOCK), config.get(CONF_UNLOCK), config.get(CONF_OPTIMISTIC), diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py index a8768193736..0ca5571515a 100644 --- a/homeassistant/components/template/sensor.py +++ b/homeassistant/components/template/sensor.py @@ -1,34 +1,34 @@ """Allows the creation of a sensor that breaks out state_attributes.""" import logging from typing import Optional -from itertools import chain import voluptuous as vol -from homeassistant.core import callback from homeassistant.components.sensor import ( + DEVICE_CLASSES_SCHEMA, ENTITY_ID_FORMAT, PLATFORM_SCHEMA, - DEVICE_CLASSES_SCHEMA, ) from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, - CONF_VALUE_TEMPLATE, - CONF_ICON_TEMPLATE, - CONF_ENTITY_PICTURE_TEMPLATE, - ATTR_ENTITY_ID, - CONF_SENSORS, - EVENT_HOMEASSISTANT_START, - CONF_FRIENDLY_NAME_TEMPLATE, - MATCH_ALL, CONF_DEVICE_CLASS, + CONF_ENTITY_PICTURE_TEMPLATE, + CONF_FRIENDLY_NAME_TEMPLATE, + CONF_ICON_TEMPLATE, + CONF_SENSORS, + CONF_VALUE_TEMPLATE, + EVENT_HOMEASSISTANT_START, + MATCH_ALL, ) - +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.event import async_track_state_change + +from . import extract_entities, initialise_templates from .const import CONF_AVAILABILITY_TEMPLATE CONF_ATTRIBUTE_TEMPLATES = "attribute_templates" @@ -72,10 +72,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= device_class = device_config.get(CONF_DEVICE_CLASS) attribute_templates = device_config[CONF_ATTRIBUTE_TEMPLATES] - entity_ids = set() - manual_entity_ids = device_config.get(ATTR_ENTITY_ID) - invalid_templates = [] - templates = { CONF_VALUE_TEMPLATE: state_template, CONF_ICON_TEMPLATE: icon_template, @@ -84,36 +80,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= CONF_AVAILABILITY_TEMPLATE: availability_template, } - for tpl_name, template in chain(templates.items(), attribute_templates.items()): - if template is None: - continue - template.hass = hass - - if manual_entity_ids is not None: - continue - - template_entity_ids = template.extract_entities() - if template_entity_ids == MATCH_ALL: - entity_ids = MATCH_ALL - # Cut off _template from name - invalid_templates.append(tpl_name.replace("_template", "")) - elif entity_ids != MATCH_ALL: - entity_ids |= set(template_entity_ids) - - if invalid_templates: - _LOGGER.warning( - "Template sensor %s has no entity ids configured to track nor" - " were we able to extract the entities to track from the %s " - "template(s). This entity will only be able to be updated " - "manually.", - device, - ", ".join(invalid_templates), - ) - - if manual_entity_ids is not None: - entity_ids = manual_entity_ids - elif entity_ids != MATCH_ALL: - entity_ids = list(entity_ids) + initialise_templates(hass, templates, attribute_templates) + entity_ids = extract_entities( + device, + "sensor", + device_config.get(ATTR_ENTITY_ID), + templates, + attribute_templates, + ) sensors.append( SensorTemplate( @@ -131,7 +105,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= attribute_templates, ) ) + async_add_entities(sensors) + return True @@ -254,7 +230,7 @@ class SensorTemplate(Entity): ): # Common during HA startup - so just a warning _LOGGER.warning( - "Could not render template %s," " the state is unknown.", self._name + "Could not render template %s, the state is unknown.", self._name ) else: self._state = None @@ -292,7 +268,7 @@ class SensorTemplate(Entity): ): # Common during HA startup - so just a warning _LOGGER.warning( - "Could not render %s template %s," " the state is unknown.", + "Could not render %s template %s, the state is unknown.", friendly_property_name, self._name, ) diff --git a/homeassistant/components/template/switch.py b/homeassistant/components/template/switch.py index 2d4dda032ca..c2d8e8158c1 100644 --- a/homeassistant/components/template/switch.py +++ b/homeassistant/components/template/switch.py @@ -3,29 +3,30 @@ import logging import voluptuous as vol -from homeassistant.core import callback from homeassistant.components.switch import ( ENTITY_ID_FORMAT, - SwitchDevice, PLATFORM_SCHEMA, + SwitchDevice, ) from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, - CONF_VALUE_TEMPLATE, - CONF_ICON_TEMPLATE, CONF_ENTITY_PICTURE_TEMPLATE, + CONF_ICON_TEMPLATE, + CONF_SWITCHES, + CONF_VALUE_TEMPLATE, + EVENT_HOMEASSISTANT_START, STATE_OFF, STATE_ON, - ATTR_ENTITY_ID, - CONF_SWITCHES, - EVENT_HOMEASSISTANT_START, - MATCH_ALL, ) +from homeassistant.core import callback from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.script import Script + +from . import extract_entities, initialise_templates from .const import CONF_AVAILABILITY_TEMPLATE _LOGGER = logging.getLogger(__name__) @@ -64,8 +65,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= availability_template = device_config.get(CONF_AVAILABILITY_TEMPLATE) on_action = device_config[ON_ACTION] off_action = device_config[OFF_ACTION] - manual_entity_ids = device_config.get(ATTR_ENTITY_ID) - entity_ids = set() templates = { CONF_VALUE_TEMPLATE: state_template, @@ -73,35 +72,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= CONF_ENTITY_PICTURE_TEMPLATE: entity_picture_template, CONF_AVAILABILITY_TEMPLATE: availability_template, } - invalid_templates = [] - for template_name, template in templates.items(): - if template is not None: - template.hass = hass - - if manual_entity_ids is not None: - continue - - template_entity_ids = template.extract_entities() - if template_entity_ids == MATCH_ALL: - invalid_templates.append(template_name.replace("_template", "")) - entity_ids = MATCH_ALL - elif entity_ids != MATCH_ALL: - entity_ids |= set(template_entity_ids) - if invalid_templates: - _LOGGER.warning( - "Template sensor %s has no entity ids configured to track nor" - " were we able to extract the entities to track from the %s " - "template(s). This entity will only be able to be updated " - "manually.", - device, - ", ".join(invalid_templates), - ) - else: - if manual_entity_ids is None: - entity_ids = list(entity_ids) - else: - entity_ids = manual_entity_ids + initialise_templates(hass, templates) + entity_ids = extract_entities( + device, "switch", device_config.get(ATTR_ENTITY_ID), templates + ) switches.append( SwitchTemplate( @@ -117,6 +92,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= entity_ids, ) ) + if not switches: _LOGGER.error("No switches added") return False @@ -257,7 +233,7 @@ class SwitchTemplate(SwitchDevice): ): # Common during HA startup - so just a warning _LOGGER.warning( - "Could not render %s template %s," " the state is unknown.", + "Could not render %s template %s, the state is unknown.", friendly_property_name, self._name, ) diff --git a/homeassistant/components/template/vacuum.py b/homeassistant/components/template/vacuum.py index 6a6523514c4..4946d54edc3 100644 --- a/homeassistant/components/template/vacuum.py +++ b/homeassistant/components/template/vacuum.py @@ -3,7 +3,6 @@ import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.vacuum import ( ATTR_FAN_SPEED, DOMAIN, @@ -14,36 +13,38 @@ from homeassistant.components.vacuum import ( SERVICE_SET_FAN_SPEED, SERVICE_START, SERVICE_STOP, + STATE_CLEANING, + STATE_DOCKED, + STATE_ERROR, + STATE_IDLE, + STATE_PAUSED, + STATE_RETURNING, SUPPORT_BATTERY, SUPPORT_CLEAN_SPOT, SUPPORT_FAN_SPEED, SUPPORT_LOCATE, SUPPORT_PAUSE, SUPPORT_RETURN_HOME, - SUPPORT_STOP, - SUPPORT_STATE, SUPPORT_START, + SUPPORT_STATE, + SUPPORT_STOP, StateVacuumDevice, - STATE_CLEANING, - STATE_DOCKED, - STATE_PAUSED, - STATE_IDLE, - STATE_RETURNING, - STATE_ERROR, ) from homeassistant.const import ( + CONF_ENTITY_ID, CONF_FRIENDLY_NAME, CONF_VALUE_TEMPLATE, - CONF_ENTITY_ID, - MATCH_ALL, EVENT_HOMEASSISTANT_START, + MATCH_ALL, STATE_UNKNOWN, ) from homeassistant.core import callback from homeassistant.exceptions import TemplateError +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.script import Script +from . import extract_entities, initialise_templates from .const import CONF_AVAILABILITY_TEMPLATE _LOGGER = logging.getLogger(__name__) @@ -109,45 +110,15 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= fan_speed_list = device_config[CONF_FAN_SPEED_LIST] - entity_ids = set() - manual_entity_ids = device_config.get(CONF_ENTITY_ID) - invalid_templates = [] + templates = { + CONF_VALUE_TEMPLATE: state_template, + CONF_BATTERY_LEVEL_TEMPLATE: battery_level_template, + CONF_FAN_SPEED_TEMPLATE: fan_speed_template, + CONF_AVAILABILITY_TEMPLATE: availability_template, + } - for tpl_name, template in ( - (CONF_VALUE_TEMPLATE, state_template), - (CONF_BATTERY_LEVEL_TEMPLATE, battery_level_template), - (CONF_FAN_SPEED_TEMPLATE, fan_speed_template), - (CONF_AVAILABILITY_TEMPLATE, availability_template), - ): - if template is None: - continue - template.hass = hass - - if manual_entity_ids is not None: - continue - - template_entity_ids = template.extract_entities() - if template_entity_ids == MATCH_ALL: - entity_ids = MATCH_ALL - # Cut off _template from name - invalid_templates.append(tpl_name[:-9]) - elif entity_ids != MATCH_ALL: - entity_ids |= set(template_entity_ids) - - if invalid_templates: - _LOGGER.warning( - "Template vacuum %s has no entity ids configured to track nor" - " were we able to extract the entities to track from the %s " - "template(s). This entity will only be able to be updated " - "manually.", - device, - ", ".join(invalid_templates), - ) - - if manual_entity_ids is not None: - entity_ids = manual_entity_ids - elif entity_ids != MATCH_ALL: - entity_ids = list(entity_ids) + initialise_templates(hass, templates) + entity_ids = extract_entities(device, "vacuum", None, templates) vacuums.append( TemplateVacuum( diff --git a/homeassistant/components/tensorflow/image_processing.py b/homeassistant/components/tensorflow/image_processing.py index c9c1b00aae4..dee2a021829 100644 --- a/homeassistant/components/tensorflow/image_processing.py +++ b/homeassistant/components/tensorflow/image_processing.py @@ -15,11 +15,11 @@ from homeassistant.components.image_processing import ( CONF_SOURCE, PLATFORM_SCHEMA, ImageProcessingEntity, - draw_box, ) from homeassistant.core import split_entity_id from homeassistant.helpers import template import homeassistant.helpers.config_validation as cv +from homeassistant.util.pil import draw_box _LOGGER = logging.getLogger(__name__) @@ -104,8 +104,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): try: # Display warning that PIL will be used if no OpenCV is found. - # pylint: disable=unused-import,unused-variable - import cv2 # noqa: F401 + import cv2 # noqa: F401 pylint: disable=unused-import except ImportError: _LOGGER.warning( "No OpenCV library found. TensorFlow will process image with " diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index e0a8728b295..627fa8d6bd6 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -4,9 +4,10 @@ "documentation": "https://www.home-assistant.io/integrations/tensorflow", "requirements": [ "tensorflow==1.13.2", - "numpy==1.17.3", - "protobuf==3.6.1" + "numpy==1.17.4", + "protobuf==3.6.1", + "pillow==6.2.1" ], "dependencies": [], "codeowners": [] -} \ No newline at end of file +} diff --git a/homeassistant/components/tesla/.translations/ca.json b/homeassistant/components/tesla/.translations/ca.json new file mode 100644 index 00000000000..cb4840dea7a --- /dev/null +++ b/homeassistant/components/tesla/.translations/ca.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "connection_error": "Error de connexi\u00f3; comprova la xarxa i torna-ho a intentar", + "identifier_exists": "Correu electr\u00f2nic ja registrat", + "invalid_credentials": "Credencials inv\u00e0lides", + "unknown_error": "Error desconegut, si us plau, envia la informaci\u00f3 del registre" + }, + "step": { + "user": { + "data": { + "password": "Contrasenya", + "username": "Correu electr\u00f2nic" + }, + "description": "Introdueix la teva informaci\u00f3.", + "title": "Configuraci\u00f3 de Tesla" + } + }, + "title": "Tesla" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Segons entre escanejos" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla/.translations/da.json b/homeassistant/components/tesla/.translations/da.json new file mode 100644 index 00000000000..85091c350d8 --- /dev/null +++ b/homeassistant/components/tesla/.translations/da.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "connection_error": "Fejl ved tilslutning; tjek netv\u00e6rk og pr\u00f8v igen", + "identifier_exists": "Email er allerede registreret", + "invalid_credentials": "Ugyldige legitimationsoplysninger", + "unknown_error": "Ukendt fejl, rapporter venligst loginfo" + }, + "step": { + "user": { + "data": { + "password": "Adgangskode", + "username": "Email-adresse" + }, + "description": "Indtast dine oplysninger.", + "title": "Tesla - Konfiguration" + } + }, + "title": "Tesla" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Sekunder mellem scanninger" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla/.translations/en.json b/homeassistant/components/tesla/.translations/en.json new file mode 100644 index 00000000000..8c43f28e04e --- /dev/null +++ b/homeassistant/components/tesla/.translations/en.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "connection_error": "Error connecting; check network and retry", + "identifier_exists": "Email already registered", + "invalid_credentials": "Invalid credentials", + "unknown_error": "Unknown error, please report log info" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Email Address" + }, + "description": "Please enter your information.", + "title": "Tesla - Configuration" + } + }, + "title": "Tesla" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Seconds between scans" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla/.translations/es.json b/homeassistant/components/tesla/.translations/es.json new file mode 100644 index 00000000000..64bab24ee3f --- /dev/null +++ b/homeassistant/components/tesla/.translations/es.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "connection_error": "Error de conexi\u00f3n; compruebe la red y vuelva a intentarlo", + "identifier_exists": "Correo electr\u00f3nico ya registrado", + "invalid_credentials": "Credenciales no v\u00e1lidas", + "unknown_error": "Error desconocido, por favor reporte la informaci\u00f3n de registro" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Direcci\u00f3n de correo electr\u00f3nico" + }, + "description": "Por favor, introduzca su informaci\u00f3n.", + "title": "Tesla - Configuraci\u00f3n" + } + }, + "title": "Tesla" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Segundos entre escaneos" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla/.translations/fr.json b/homeassistant/components/tesla/.translations/fr.json new file mode 100644 index 00000000000..69742d3370c --- /dev/null +++ b/homeassistant/components/tesla/.translations/fr.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "connection_error": "Erreur de connexion; v\u00e9rifier le r\u00e9seau et r\u00e9essayer", + "identifier_exists": "Email d\u00e9j\u00e0 enregistr\u00e9", + "invalid_credentials": "Informations d'identification invalides", + "unknown_error": "Erreur inconnue, veuillez signaler les informations du journal" + }, + "step": { + "user": { + "data": { + "password": "Mot de passe", + "username": "Adresse e-mail" + }, + "description": "Veuillez saisir vos informations.", + "title": "Tesla - Configuration" + } + }, + "title": "Tesla" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Secondes entre les scans" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla/.translations/hu.json b/homeassistant/components/tesla/.translations/hu.json new file mode 100644 index 00000000000..7a9a3deff49 --- /dev/null +++ b/homeassistant/components/tesla/.translations/hu.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "connection_error": "Hiba a csatlakoz\u00e1skor; ellen\u0151rizd a h\u00e1l\u00f3zatot \u00e9s pr\u00f3b\u00e1ld \u00fajra", + "identifier_exists": "Az e-mail c\u00edm m\u00e1r regisztr\u00e1lva van", + "invalid_credentials": "\u00c9rv\u00e9nytelen hiteles\u00edt\u0151 adatok", + "unknown_error": "Ismeretlen hiba, k\u00e9rlek jelentsd a napl\u00f3f\u00e1jlban l\u00e9v\u0151 adatokat" + }, + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "Email c\u00edm" + }, + "description": "K\u00e9rlek, add meg az adataidat.", + "title": "Tesla - Konfigur\u00e1ci\u00f3" + } + }, + "title": "Tesla" + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla/.translations/it.json b/homeassistant/components/tesla/.translations/it.json new file mode 100644 index 00000000000..0e254cf2843 --- /dev/null +++ b/homeassistant/components/tesla/.translations/it.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "connection_error": "Errore durante la connessione; controllare la rete e riprovare", + "identifier_exists": "E-mail gi\u00e0 registrata", + "invalid_credentials": "Credenziali non valide", + "unknown_error": "Errore sconosciuto, si prega di segnalare le informazioni del registro" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Indirizzo E-Mail" + }, + "description": "Si prega di inserire le tue informazioni.", + "title": "Tesla - Configurazione" + } + }, + "title": "Tesla" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Secondi tra le scansioni" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla/.translations/ko.json b/homeassistant/components/tesla/.translations/ko.json new file mode 100644 index 00000000000..8b7dc9ce93c --- /dev/null +++ b/homeassistant/components/tesla/.translations/ko.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "connection_error": "\uc5f0\uacb0 \uc624\ub958; \ub124\ud2b8\uc6cc\ud06c\ub97c \ud655\uc778\ud558\uace0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694", + "identifier_exists": "\uc774\uba54\uc77c \uc8fc\uc18c\uac00 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "invalid_credentials": "\uc774\uba54\uc77c \uc8fc\uc18c \ud639\uc740 \ube44\ubc00\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown_error": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uc785\ub2c8\ub2e4. \ub85c\uadf8 \ub0b4\uc6a9\uc744 \uc54c\ub824\uc8fc\uc138\uc694" + }, + "step": { + "user": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc774\uba54\uc77c \uc8fc\uc18c" + }, + "description": "\uc0ac\uc6a9\uc790 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694", + "title": "Tesla - \uad6c\uc131" + } + }, + "title": "Tesla" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\uc2a4\uce94 \uac04\uaca9(\ucd08)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla/.translations/lb.json b/homeassistant/components/tesla/.translations/lb.json new file mode 100644 index 00000000000..fa63c5a289a --- /dev/null +++ b/homeassistant/components/tesla/.translations/lb.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "connection_error": "Feeler beim verbannen, Iwwerpr\u00e9ift Netzwierk a prob\u00e9iert nach emol", + "identifier_exists": "E-Mail ass scho registr\u00e9iert", + "invalid_credentials": "Ong\u00eblteg Login Informatioune", + "unknown_error": "Onbekannte Feeler, mellt w.e.g. Logbuch Info" + }, + "step": { + "user": { + "data": { + "password": "Passwuert", + "username": "E-Mail Adress" + }, + "description": "F\u00ebllt \u00e4r Informatiounen aus.", + "title": "Tesla - Konfiguratioun" + } + }, + "title": "Tesla" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Sekonnen t\u00ebscht Scannen" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla/.translations/nl.json b/homeassistant/components/tesla/.translations/nl.json new file mode 100644 index 00000000000..5f3e83dd248 --- /dev/null +++ b/homeassistant/components/tesla/.translations/nl.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "connection_error": "Fout bij verbinden; controleer het netwerk en probeer het opnieuw", + "identifier_exists": "E-mail al geregistreerd", + "invalid_credentials": "Ongeldige inloggegevens", + "unknown_error": "Onbekende fout, meldt u log info" + }, + "step": { + "user": { + "data": { + "password": "Wachtwoord", + "username": "E-mailadres" + }, + "description": "Vul alstublieft uw gegevens in.", + "title": "Tesla - Configuratie" + } + }, + "title": "Tesla" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Seconden tussen scans" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla/.translations/no.json b/homeassistant/components/tesla/.translations/no.json new file mode 100644 index 00000000000..0d73908f417 --- /dev/null +++ b/homeassistant/components/tesla/.translations/no.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "connection_error": "Feil ved tilkobling; sjekk nettverket og pr\u00f8v p\u00e5 nytt", + "identifier_exists": "E-post er allerede registrert", + "invalid_credentials": "Ugyldig brukerinformasjon", + "unknown_error": "Ukjent feil, Vennligst rapporter informasjon fra Loggen" + }, + "step": { + "user": { + "data": { + "password": "Passord", + "username": "E-postadresse" + }, + "description": "Vennligst skriv inn informasjonen din.", + "title": "Tesla - Konfigurasjon" + } + }, + "title": "Tesla" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Sekunder mellom skanninger" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla/.translations/pl.json b/homeassistant/components/tesla/.translations/pl.json new file mode 100644 index 00000000000..5a8a3d2ebd3 --- /dev/null +++ b/homeassistant/components/tesla/.translations/pl.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "connection_error": "B\u0142\u0105d po\u0142\u0105czenia; sprawd\u017a sie\u0107 i spr\u00f3buj ponownie", + "identifier_exists": "Adres e-mail ju\u017c zarejestrowany", + "invalid_credentials": "Nieprawid\u0142owe po\u015bwiadczenia", + "unknown_error": "Nieznany b\u0142\u0105d, prosz\u0119 zg\u0142osi\u0107 dane z loga" + }, + "step": { + "user": { + "data": { + "password": "Has\u0142o", + "username": "Adres e-mail" + }, + "description": "Wprowad\u017a dane", + "title": "Tesla - konfiguracja" + } + }, + "title": "Tesla" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Cz\u0119stotliwo\u015b\u0107 aktualizacji" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla/.translations/ru.json b/homeassistant/components/tesla/.translations/ru.json new file mode 100644 index 00000000000..15eeabf6136 --- /dev/null +++ b/homeassistant/components/tesla/.translations/ru.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "connection_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0441\u0435\u0442\u044c \u0438 \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443.", + "identifier_exists": "\u0423\u0447\u0451\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_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", + "unknown_error": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438.", + "title": "Tesla" + } + }, + "title": "Tesla" + }, + "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 (\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 new file mode 100644 index 00000000000..776a80da7fb --- /dev/null +++ b/homeassistant/components/tesla/.translations/zh-Hant.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "connection_error": "\u9023\u7dda\u932f\u8aa4\uff1b\u8acb\u6aa2\u5bdf\u7db2\u8def\u5f8c\u518d\u8a66\u4e00\u6b21", + "identifier_exists": "\u90f5\u4ef6\u5df2\u8a3b\u518a", + "invalid_credentials": "\u6191\u8b49\u7121\u6548", + "unknown_error": "\u672a\u77e5\u932f\u8aa4\uff0c\u8acb\u56de\u5831\u7d00\u9304" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u96fb\u5b50\u90f5\u4ef6\u5730\u5740" + }, + "description": "\u8acb\u8f38\u5165\u8cc7\u8a0a\u3002", + "title": "Tesla - \u8a2d\u5b9a" + } + }, + "title": "Tesla" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u6383\u63cf\u9593\u9694\u79d2\u6578" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla/__init__.py b/homeassistant/components/tesla/__init__.py index a3d45eed01c..1ae65f66821 100644 --- a/homeassistant/components/tesla/__init__.py +++ b/homeassistant/components/tesla/__init__.py @@ -1,21 +1,32 @@ """Support for Tesla cars.""" +import asyncio from collections import defaultdict import logging -from teslajsonpy import Controller as teslaAPI, TeslaException +from teslajsonpy import Controller as TeslaAPI, TeslaException import voluptuous as vol +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_BATTERY_LEVEL, + CONF_ACCESS_TOKEN, CONF_PASSWORD, CONF_SCAN_INTERVAL, + CONF_TOKEN, CONF_USERNAME, ) -from homeassistant.helpers import aiohttp_client, config_validation as cv, discovery +from homeassistant.core import callback +from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import slugify -from .const import DOMAIN, TESLA_COMPONENTS +from .config_flow import ( + CannotConnect, + InvalidAuth, + configured_instances, + validate_input, +) +from .const import DATA_LISTENER, DOMAIN, ICONS, TESLA_COMPONENTS _LOGGER = logging.getLogger(__name__) @@ -34,72 +45,148 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -NOTIFICATION_ID = "tesla_integration_notification" -NOTIFICATION_TITLE = "Tesla integration setup" + +@callback +def _async_save_tokens(hass, config_entry, access_token, refresh_token): + hass.config_entries.async_update_entry( + config_entry, + data={ + **config_entry.data, + CONF_ACCESS_TOKEN: access_token, + CONF_TOKEN: refresh_token, + }, + ) async def async_setup(hass, base_config): """Set up of Tesla component.""" - config = base_config.get(DOMAIN) - email = config.get(CONF_USERNAME) - password = config.get(CONF_PASSWORD) - update_interval = config.get(CONF_SCAN_INTERVAL) - if hass.data.get(DOMAIN) is None: + def _update_entry(email, data=None, options=None): + data = data or {} + options = options or {CONF_SCAN_INTERVAL: 300} + for entry in hass.config_entries.async_entries(DOMAIN): + if email != entry.title: + continue + hass.config_entries.async_update_entry(entry, data=data, options=options) + + config = base_config.get(DOMAIN) + if not config: + return True + email = config[CONF_USERNAME] + password = config[CONF_PASSWORD] + scan_interval = config[CONF_SCAN_INTERVAL] + if email in configured_instances(hass): try: - websession = aiohttp_client.async_get_clientsession(hass) - controller = teslaAPI( - websession, - email=email, - password=password, - update_interval=update_interval, - ) - await controller.connect(test_login=False) - hass.data[DOMAIN] = {"controller": controller, "devices": defaultdict(list)} - _LOGGER.debug("Connected to the Tesla API.") - except TeslaException as ex: - if ex.code == 401: - hass.components.persistent_notification.create( - "Error:
Please check username and password." - "You will need to restart Home Assistant after fixing.", - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID, - ) - else: - hass.components.persistent_notification.create( - "Error:
Can't communicate with Tesla API.
" - "Error code: {} Reason: {}" - "You will need to restart Home Assistant after fixing." - "".format(ex.code, ex.message), - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID, - ) - _LOGGER.error("Unable to communicate with Tesla API: %s", ex.message) + info = await validate_input(hass, config) + except (CannotConnect, InvalidAuth): return False - all_devices = controller.get_homeassistant_components() + _update_entry( + email, + data={ + CONF_ACCESS_TOKEN: info[CONF_ACCESS_TOKEN], + CONF_TOKEN: info[CONF_TOKEN], + }, + options={CONF_SCAN_INTERVAL: scan_interval}, + ) + else: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={CONF_USERNAME: email, CONF_PASSWORD: password}, + ) + ) + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][email] = {CONF_SCAN_INTERVAL: scan_interval} + return True + + +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) + 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] + hass.config_entries.async_update_entry( + config_entry, options={CONF_SCAN_INTERVAL: scan_interval} + ) + hass.data[DOMAIN].pop(email) + try: + controller = TeslaAPI( + websession, + refresh_token=config[CONF_TOKEN], + update_interval=config_entry.options.get(CONF_SCAN_INTERVAL, 300), + ) + (refresh_token, access_token) = await controller.connect() + except TeslaException as ex: + _LOGGER.error("Unable to communicate with Tesla API: %s", ex.message) + return False + _async_save_tokens(hass, config_entry, access_token, refresh_token) + entry_data = hass.data[DOMAIN][config_entry.entry_id] = { + "controller": controller, + "devices": defaultdict(list), + DATA_LISTENER: [config_entry.add_update_listener(update_listener)], + } + _LOGGER.debug("Connected to the Tesla API.") + all_devices = entry_data["controller"].get_homeassistant_components() + if not all_devices: return False for device in all_devices: - hass.data[DOMAIN]["devices"][device.hass_type].append(device) + entry_data["devices"][device.hass_type].append(device) for component in TESLA_COMPONENTS: + _LOGGER.debug("Loading %s", component) hass.async_create_task( - discovery.async_load_platform(hass, component, DOMAIN, {}, base_config) + hass.config_entries.async_forward_entry_setup(config_entry, component) ) return True +async def async_unload_entry(hass, config_entry) -> bool: + """Unload a config entry.""" + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(config_entry, component) + for component in TESLA_COMPONENTS + ] + ) + for listener in hass.data[DOMAIN][config_entry.entry_id][DATA_LISTENER]: + listener() + username = config_entry.title + hass.data[DOMAIN].pop(config_entry.entry_id) + _LOGGER.debug("Unloaded entry for %s", username) + return True + + +async def update_listener(hass, config_entry): + """Update when config_entry options update.""" + controller = hass.data[DOMAIN][config_entry.entry_id]["controller"] + old_update_interval = controller.update_interval + controller.update_interval = config_entry.options.get(CONF_SCAN_INTERVAL) + _LOGGER.debug( + "Changing scan_interval from %s to %s", + old_update_interval, + controller.update_interval, + ) + + class TeslaDevice(Entity): """Representation of a Tesla device.""" - def __init__(self, tesla_device, controller): + def __init__(self, tesla_device, controller, config_entry): """Initialise the Tesla device.""" self.tesla_device = tesla_device self.controller = controller + self.config_entry = config_entry self._name = self.tesla_device.name self.tesla_id = slugify(self.tesla_device.uniq_name) self._attributes = {} + self._icon = ICONS.get(self.tesla_device.type) @property def name(self): @@ -111,6 +198,11 @@ class TeslaDevice(Entity): """Return a unique ID.""" return self.tesla_id + @property + def icon(self): + """Return the icon of the sensor.""" + return self._icon + @property def should_poll(self): """Return the polling state.""" @@ -124,6 +216,17 @@ class TeslaDevice(Entity): attr[ATTR_BATTERY_LEVEL] = self.tesla_device.battery_level() return attr + @property + def device_info(self): + """Return the device_info of the device.""" + return { + "identifiers": {(DOMAIN, self.tesla_device.id())}, + "name": self.tesla_device.car_name(), + "manufacturer": "Tesla", + "model": self.tesla_device.car_type, + "sw_version": self.tesla_device.car_version, + } + async def async_added_to_hass(self): """Register state update callback.""" pass @@ -134,4 +237,10 @@ class TeslaDevice(Entity): async def async_update(self): """Update the state of the device.""" + if self.controller.is_token_refreshed(): + (refresh_token, access_token) = self.controller.get_tokens() + _async_save_tokens( + self.hass, self.config_entry, access_token, refresh_token + ) + _LOGGER.debug("Saving new tokens in config_entry") await self.tesla_device.async_update() diff --git a/homeassistant/components/tesla/binary_sensor.py b/homeassistant/components/tesla/binary_sensor.py index 738533a9b56..8f610d960b3 100644 --- a/homeassistant/components/tesla/binary_sensor.py +++ b/homeassistant/components/tesla/binary_sensor.py @@ -8,21 +8,35 @@ from . import DOMAIN as TESLA_DOMAIN, TeslaDevice _LOGGER = logging.getLogger(__name__) -async def async_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 Tesla binary sensor.""" - devices = [ - TeslaBinarySensor(device, hass.data[TESLA_DOMAIN]["controller"], "connectivity") - for device in hass.data[TESLA_DOMAIN]["devices"]["binary_sensor"] - ] - add_entities(devices, True) + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Tesla binary_sensors by config_entry.""" + async_add_entities( + [ + TeslaBinarySensor( + device, + hass.data[TESLA_DOMAIN][config_entry.entry_id]["controller"], + "connectivity", + config_entry, + ) + for device in hass.data[TESLA_DOMAIN][config_entry.entry_id]["devices"][ + "binary_sensor" + ] + ], + True, + ) class TeslaBinarySensor(TeslaDevice, BinarySensorDevice): """Implement an Tesla binary sensor for parking and charger.""" - def __init__(self, tesla_device, controller, sensor_type): + def __init__(self, tesla_device, controller, sensor_type, config_entry): """Initialise of a Tesla binary sensor.""" - super().__init__(tesla_device, controller) + super().__init__(tesla_device, controller, config_entry) self._state = False self._sensor_type = sensor_type diff --git a/homeassistant/components/tesla/climate.py b/homeassistant/components/tesla/climate.py index 85fd8a8e258..d7f21d7895f 100644 --- a/homeassistant/components/tesla/climate.py +++ b/homeassistant/components/tesla/climate.py @@ -3,7 +3,7 @@ import logging from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( - HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, SUPPORT_TARGET_TEMPERATURE, ) @@ -13,24 +13,37 @@ from . import DOMAIN as TESLA_DOMAIN, TeslaDevice _LOGGER = logging.getLogger(__name__) -SUPPORT_HVAC = [HVAC_MODE_HEAT, HVAC_MODE_OFF] +SUPPORT_HVAC = [HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF] -async def async_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 Tesla climate platform.""" - devices = [ - TeslaThermostat(device, hass.data[TESLA_DOMAIN]["controller"]) - for device in hass.data[TESLA_DOMAIN]["devices"]["climate"] - ] - add_entities(devices, True) + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Tesla binary_sensors by config_entry.""" + async_add_entities( + [ + TeslaThermostat( + device, + hass.data[TESLA_DOMAIN][config_entry.entry_id]["controller"], + config_entry, + ) + for device in hass.data[TESLA_DOMAIN][config_entry.entry_id]["devices"][ + "climate" + ] + ], + True, + ) class TeslaThermostat(TeslaDevice, ClimateDevice): """Representation of a Tesla climate.""" - def __init__(self, tesla_device, controller): + def __init__(self, tesla_device, controller, config_entry): """Initialize the Tesla device.""" - super().__init__(tesla_device, controller) + super().__init__(tesla_device, controller, config_entry) self._target_temperature = None self._temperature = None @@ -46,7 +59,7 @@ class TeslaThermostat(TeslaDevice, ClimateDevice): Need to be one of HVAC_MODE_*. """ if self.tesla_device.is_hvac_enabled(): - return HVAC_MODE_HEAT + return HVAC_MODE_HEAT_COOL return HVAC_MODE_OFF @property @@ -95,5 +108,5 @@ class TeslaThermostat(TeslaDevice, ClimateDevice): _LOGGER.debug("Setting mode for: %s", self._name) if hvac_mode == HVAC_MODE_OFF: await self.tesla_device.set_status(False) - elif hvac_mode == HVAC_MODE_HEAT: + elif hvac_mode == HVAC_MODE_HEAT_COOL: await self.tesla_device.set_status(True) diff --git a/homeassistant/components/tesla/config_flow.py b/homeassistant/components/tesla/config_flow.py new file mode 100644 index 00000000000..2d2bc0158d2 --- /dev/null +++ b/homeassistant/components/tesla/config_flow.py @@ -0,0 +1,143 @@ +"""Tesla Config Flow.""" +import logging + +from teslajsonpy import Controller as TeslaAPI, TeslaException +import voluptuous as vol + +from homeassistant import config_entries, core, exceptions +from homeassistant.const import ( + CONF_ACCESS_TOKEN, + CONF_PASSWORD, + CONF_SCAN_INTERVAL, + CONF_TOKEN, + CONF_USERNAME, +) +from homeassistant.core import callback +from homeassistant.helpers import aiohttp_client, config_validation as cv + +from .const import DOMAIN + +_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 set(entry.title for entry in hass.config_entries.async_entries(DOMAIN)) + + +class TeslaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Tesla.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + 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.""" + + 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: "identifier_exists"}, + 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": "connection_error"}, + description_placeholders={}, + ) + except InvalidAuth: + return self.async_show_form( + step_id="user", + data_schema=DATA_SCHEMA, + errors={"base": "invalid_credentials"}, + description_placeholders={}, + ) + return self.async_create_entry(title=user_input[CONF_USERNAME], data=info) + + @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 Tesla.""" + + 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_SCAN_INTERVAL, + default=self.config_entry.options.get(CONF_SCAN_INTERVAL, 300), + ): vol.All(cv.positive_int, vol.Clamp(min=300)) + } + ) + return self.async_show_form(step_id="init", data_schema=data_schema) + + +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. + """ + + config = {} + websession = aiohttp_client.async_get_clientsession(hass) + try: + controller = TeslaAPI( + websession, + email=data[CONF_USERNAME], + password=data[CONF_PASSWORD], + update_interval=300, + ) + (config[CONF_TOKEN], config[CONF_ACCESS_TOKEN]) = await controller.connect( + test_login=True + ) + except TeslaException as ex: + if ex.code == 401: + _LOGGER.error("Invalid credentials: %s", ex) + raise InvalidAuth() + _LOGGER.error("Unable to communicate with Tesla API: %s", ex) + raise CannotConnect() + _LOGGER.debug("Credentials successfully connected to the Tesla API") + return config + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(exceptions.HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/tesla/const.py b/homeassistant/components/tesla/const.py index 30a58b733ed..be460a430ac 100644 --- a/homeassistant/components/tesla/const.py +++ b/homeassistant/components/tesla/const.py @@ -9,7 +9,7 @@ TESLA_COMPONENTS = [ "device_tracker", "switch", ] -SENSOR_ICONS = { +ICONS = { "battery sensor": "mdi:battery", "range sensor": "mdi:gauge", "mileage sensor": "mdi:counter", diff --git a/homeassistant/components/tesla/device_tracker.py b/homeassistant/components/tesla/device_tracker.py index c205cc587eb..08e5d58ba6e 100644 --- a/homeassistant/components/tesla/device_tracker.py +++ b/homeassistant/components/tesla/device_tracker.py @@ -1,45 +1,70 @@ """Support for tracking Tesla cars.""" import logging -from homeassistant.helpers.event import async_track_utc_time_change -from homeassistant.util import slugify +from homeassistant.components.device_tracker import SOURCE_TYPE_GPS +from homeassistant.components.device_tracker.config_entry import TrackerEntity -from . import DOMAIN as TESLA_DOMAIN +from . import DOMAIN as TESLA_DOMAIN, TeslaDevice _LOGGER = logging.getLogger(__name__) -async def async_setup_scanner(hass, config, async_see, discovery_info=None): - """Set up the Tesla tracker.""" - tracker = TeslaDeviceTracker( - hass, config, async_see, hass.data[TESLA_DOMAIN]["devices"]["devices_tracker"] - ) - await tracker.update_info() - async_track_utc_time_change(hass, tracker.update_info, second=range(0, 60, 30)) - return True +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Tesla binary_sensors by config_entry.""" + entities = [ + TeslaDeviceEntity( + device, + hass.data[TESLA_DOMAIN][config_entry.entry_id]["controller"], + config_entry, + ) + for device in hass.data[TESLA_DOMAIN][config_entry.entry_id]["devices"][ + "devices_tracker" + ] + ] + async_add_entities(entities, True) -class TeslaDeviceTracker: +class TeslaDeviceEntity(TeslaDevice, TrackerEntity): """A class representing a Tesla device.""" - def __init__(self, hass, config, see, tesla_devices): + def __init__(self, tesla_device, controller, config_entry): """Initialize the Tesla device scanner.""" - self.hass = hass - self.see = see - self.devices = tesla_devices + super().__init__(tesla_device, controller, config_entry) + self._latitude = None + self._longitude = None + self._attributes = {"trackr_id": self.unique_id} + self._listener = None - async def update_info(self, now=None): + async def async_update(self): """Update the device info.""" - for device in self.devices: - await device.async_update() - name = device.name - _LOGGER.debug("Updating device position: %s", name) - dev_id = slugify(device.uniq_name) - location = device.get_location() - if location: - lat = location["latitude"] - lon = location["longitude"] - attrs = {"trackr_id": dev_id, "id": dev_id, "name": name} - await self.see( - dev_id=dev_id, host_name=name, gps=(lat, lon), attributes=attrs - ) + _LOGGER.debug("Updating device position: %s", self.name) + await super().async_update() + location = self.tesla_device.get_location() + if location: + self._latitude = location["latitude"] + self._longitude = location["longitude"] + self._attributes = { + "trackr_id": self.unique_id, + "heading": location["heading"], + "speed": location["speed"], + } + + @property + def latitude(self) -> float: + """Return latitude value of the device.""" + return self._latitude + + @property + def longitude(self) -> float: + """Return longitude value of the device.""" + return self._longitude + + @property + def should_poll(self): + """Return whether polling is needed.""" + return True + + @property + def source_type(self): + """Return the source type, eg gps or router, of the device.""" + return SOURCE_TYPE_GPS diff --git a/homeassistant/components/tesla/lock.py b/homeassistant/components/tesla/lock.py index 5e97602357d..33eed8cf7c1 100644 --- a/homeassistant/components/tesla/lock.py +++ b/homeassistant/components/tesla/lock.py @@ -9,22 +9,31 @@ from . import DOMAIN as TESLA_DOMAIN, TeslaDevice _LOGGER = logging.getLogger(__name__) -async def async_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 Tesla lock platform.""" - devices = [ - TeslaLock(device, hass.data[TESLA_DOMAIN]["controller"]) - for device in hass.data[TESLA_DOMAIN]["devices"]["lock"] + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Tesla binary_sensors by config_entry.""" + entities = [ + TeslaLock( + device, + hass.data[TESLA_DOMAIN][config_entry.entry_id]["controller"], + config_entry, + ) + for device in hass.data[TESLA_DOMAIN][config_entry.entry_id]["devices"]["lock"] ] - add_entities(devices, True) + async_add_entities(entities, True) class TeslaLock(TeslaDevice, LockDevice): """Representation of a Tesla door lock.""" - def __init__(self, tesla_device, controller): + def __init__(self, tesla_device, controller, config_entry): """Initialise of the lock.""" self._state = None - super().__init__(tesla_device, controller) + super().__init__(tesla_device, controller, config_entry) async def async_lock(self, **kwargs): """Send the lock command.""" diff --git a/homeassistant/components/tesla/manifest.json b/homeassistant/components/tesla/manifest.json index a2021092413..09a579373d6 100644 --- a/homeassistant/components/tesla/manifest.json +++ b/homeassistant/components/tesla/manifest.json @@ -1,8 +1,9 @@ { "domain": "tesla", "name": "Tesla", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tesla", - "requirements": ["teslajsonpy==0.2.0"], + "requirements": ["teslajsonpy==0.2.1"], "dependencies": [], - "codeowners": ["@zabuldon"] + "codeowners": ["@zabuldon", "@alandtse"] } diff --git a/homeassistant/components/tesla/sensor.py b/homeassistant/components/tesla/sensor.py index 1cce37f232a..a282f65f9e1 100644 --- a/homeassistant/components/tesla/sensor.py +++ b/homeassistant/components/tesla/sensor.py @@ -8,36 +8,41 @@ from homeassistant.const import ( TEMP_FAHRENHEIT, ) from homeassistant.helpers.entity import Entity +from homeassistant.util.distance import convert from . import DOMAIN as TESLA_DOMAIN, TeslaDevice _LOGGER = logging.getLogger(__name__) -async def async_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 Tesla sensor platform.""" - controller = hass.data[TESLA_DOMAIN]["devices"]["controller"] - devices = [] + pass - for device in hass.data[TESLA_DOMAIN]["devices"]["sensor"]: - if device.bin_type == 0x4: - devices.append(TeslaSensor(device, controller, "inside")) - devices.append(TeslaSensor(device, controller, "outside")) - elif device.bin_type in [0xA, 0xB, 0x5]: - devices.append(TeslaSensor(device, controller)) - add_entities(devices, True) + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Tesla binary_sensors by config_entry.""" + controller = hass.data[TESLA_DOMAIN][config_entry.entry_id]["controller"] + entities = [] + for device in hass.data[TESLA_DOMAIN][config_entry.entry_id]["devices"]["sensor"]: + if device.type == "temperature sensor": + entities.append(TeslaSensor(device, controller, config_entry, "inside")) + entities.append(TeslaSensor(device, controller, config_entry, "outside")) + else: + entities.append(TeslaSensor(device, controller, config_entry)) + async_add_entities(entities, True) class TeslaSensor(TeslaDevice, Entity): """Representation of Tesla sensors.""" - def __init__(self, tesla_device, controller, sensor_type=None): + def __init__(self, tesla_device, controller, config_entry, sensor_type=None): """Initialize of the sensor.""" self.current_value = None - self._unit = None + self.units = None self.last_changed_time = None self.type = sensor_type - super().__init__(tesla_device, controller) + super().__init__(tesla_device, controller, config_entry) if self.type: self._name = f"{self.tesla_device.name} ({self.type})" @@ -57,7 +62,7 @@ class TeslaSensor(TeslaDevice, Entity): @property def unit_of_measurement(self): """Return the unit_of_measurement of the device.""" - return self._unit + return self.units async def async_update(self): """Update the state from the sensor.""" @@ -65,32 +70,34 @@ class TeslaSensor(TeslaDevice, Entity): await super().async_update() units = self.tesla_device.measurement - if self.tesla_device.bin_type == 0x4: + if self.tesla_device.type == "temperature sensor": if self.type == "outside": self.current_value = self.tesla_device.get_outside_temp() else: self.current_value = self.tesla_device.get_inside_temp() if units == "F": - self._unit = TEMP_FAHRENHEIT + self.units = TEMP_FAHRENHEIT else: - self._unit = TEMP_CELSIUS - elif self.tesla_device.bin_type == 0xA or self.tesla_device.bin_type == 0xB: + self.units = TEMP_CELSIUS + elif self.tesla_device.type in ["range sensor", "mileage sensor"]: self.current_value = self.tesla_device.get_value() - tesla_dist_unit = self.tesla_device.measurement - if tesla_dist_unit == "LENGTH_MILES": - self._unit = LENGTH_MILES + if units == "LENGTH_MILES": + self.units = LENGTH_MILES else: - self._unit = LENGTH_KILOMETERS - self.current_value /= 0.621371 - self.current_value = round(self.current_value, 2) + self.units = LENGTH_KILOMETERS + self.current_value = round( + convert(self.current_value, LENGTH_MILES, LENGTH_KILOMETERS), 2 + ) + elif self.tesla_device.type == "charging rate sensor": + self.current_value = self.tesla_device.charging_rate + self.units = units + self._attributes = { + "time_left": self.tesla_device.time_left, + "added_range": self.tesla_device.added_range, + "charge_current_request": self.tesla_device.charge_current_request, + "charger_actual_current": self.tesla_device.charger_actual_current, + "charger_voltage": self.tesla_device.charger_voltage, + } else: self.current_value = self.tesla_device.get_value() - if self.tesla_device.bin_type == 0x5: - self._unit = units - elif self.tesla_device.bin_type in (0xA, 0xB): - if units == "LENGTH_MILES": - self._unit = LENGTH_MILES - else: - self._unit = LENGTH_KILOMETERS - self.current_value /= 0.621371 - self.current_value = round(self.current_value, 2) + self.units = units diff --git a/homeassistant/components/tesla/strings.json b/homeassistant/components/tesla/strings.json new file mode 100644 index 00000000000..831406a0d63 --- /dev/null +++ b/homeassistant/components/tesla/strings.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "connection_error": "Error connecting; check network and retry", + "identifier_exists": "Email already registered", + "invalid_credentials": "Invalid credentials", + "unknown_error": "Unknown error, please report log info" + }, + "step": { + "user": { + "data": { + "username": "Email Address", + "password": "Password" + }, + "description": "Please enter your information.", + "title": "Tesla - Configuration" + } + }, + "title": "Tesla" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Seconds between scans" + } + } + } + } +} diff --git a/homeassistant/components/tesla/switch.py b/homeassistant/components/tesla/switch.py index 5f432875aeb..fc9b5e1ba88 100644 --- a/homeassistant/components/tesla/switch.py +++ b/homeassistant/components/tesla/switch.py @@ -9,26 +9,31 @@ from . import DOMAIN as TESLA_DOMAIN, TeslaDevice _LOGGER = logging.getLogger(__name__) -async def async_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 Tesla switch platform.""" - controller = hass.data[TESLA_DOMAIN]["controller"] - devices = [] - for device in hass.data[TESLA_DOMAIN]["devices"]["switch"]: - if device.bin_type == 0x8: - devices.append(ChargerSwitch(device, controller)) - devices.append(UpdateSwitch(device, controller)) - elif device.bin_type == 0x9: - devices.append(RangeSwitch(device, controller)) - add_entities(devices, True) + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Tesla binary_sensors by config_entry.""" + controller = hass.data[TESLA_DOMAIN][config_entry.entry_id]["controller"] + entities = [] + for device in hass.data[TESLA_DOMAIN][config_entry.entry_id]["devices"]["switch"]: + if device.type == "charger switch": + entities.append(ChargerSwitch(device, controller, config_entry)) + entities.append(UpdateSwitch(device, controller, config_entry)) + elif device.type == "maxrange switch": + entities.append(RangeSwitch(device, controller, config_entry)) + async_add_entities(entities, True) class ChargerSwitch(TeslaDevice, SwitchDevice): """Representation of a Tesla charger switch.""" - def __init__(self, tesla_device, controller): + def __init__(self, tesla_device, controller, config_entry): """Initialise of the switch.""" self._state = None - super().__init__(tesla_device, controller) + super().__init__(tesla_device, controller, config_entry) async def async_turn_on(self, **kwargs): """Send the on command.""" @@ -55,10 +60,10 @@ class ChargerSwitch(TeslaDevice, SwitchDevice): class RangeSwitch(TeslaDevice, SwitchDevice): """Representation of a Tesla max range charging switch.""" - def __init__(self, tesla_device, controller): + def __init__(self, tesla_device, controller, config_entry): """Initialise the switch.""" self._state = None - super().__init__(tesla_device, controller) + super().__init__(tesla_device, controller, config_entry) async def async_turn_on(self, **kwargs): """Send the on command.""" @@ -85,11 +90,11 @@ class RangeSwitch(TeslaDevice, SwitchDevice): class UpdateSwitch(TeslaDevice, SwitchDevice): """Representation of a Tesla update switch.""" - def __init__(self, tesla_device, controller): + def __init__(self, tesla_device, controller, config_entry): """Initialise the switch.""" self._state = None tesla_device.type = "update switch" - super().__init__(tesla_device, controller) + super().__init__(tesla_device, controller, config_entry) self._name = self._name.replace("charger", "update") self.tesla_id = self.tesla_id.replace("charger", "update") diff --git a/homeassistant/components/thinkingcleaner/sensor.py b/homeassistant/components/thinkingcleaner/sensor.py index 2c2194f6ace..7a45be7eb61 100644 --- a/homeassistant/components/thinkingcleaner/sensor.py +++ b/homeassistant/components/thinkingcleaner/sensor.py @@ -1,6 +1,8 @@ """Support for ThinkingCleaner sensors.""" -import logging from datetime import timedelta +import logging + +from pythinkingcleaner import Discovery from homeassistant import util from homeassistant.helpers.entity import Entity @@ -46,7 +48,6 @@ STATES = { def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the ThinkingCleaner platform.""" - from pythinkingcleaner import Discovery discovery = Discovery() devices = discovery.discover() diff --git a/homeassistant/components/thinkingcleaner/switch.py b/homeassistant/components/thinkingcleaner/switch.py index aa57077734a..88d87e4e5fe 100644 --- a/homeassistant/components/thinkingcleaner/switch.py +++ b/homeassistant/components/thinkingcleaner/switch.py @@ -1,10 +1,12 @@ """Support for ThinkingCleaner switches.""" -import time -import logging from datetime import timedelta +import logging +import time + +from pythinkingcleaner import Discovery from homeassistant import util -from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.helpers.entity import ToggleEntity _LOGGER = logging.getLogger(__name__) @@ -24,7 +26,6 @@ SWITCH_TYPES = { def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the ThinkingCleaner platform.""" - from pythinkingcleaner import Discovery discovery = Discovery() devices = discovery.discover() diff --git a/homeassistant/components/thomson/device_tracker.py b/homeassistant/components/thomson/device_tracker.py index 214c1b8cfb4..1f3fda6cc72 100644 --- a/homeassistant/components/thomson/device_tracker.py +++ b/homeassistant/components/thomson/device_tracker.py @@ -5,13 +5,13 @@ import telnetlib import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json index 11c5a676bf6..99c1335517e 100644 --- a/homeassistant/components/tibber/manifest.json +++ b/homeassistant/components/tibber/manifest.json @@ -3,7 +3,7 @@ "name": "Tibber", "documentation": "https://www.home-assistant.io/integrations/tibber", "requirements": [ - "pyTibber==0.11.7" + "pyTibber==0.12.0" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/tile/device_tracker.py b/homeassistant/components/tile/device_tracker.py index 1cb88f67c2f..8bc4fb11cdf 100644 --- a/homeassistant/components/tile/device_tracker.py +++ b/homeassistant/components/tile/device_tracker.py @@ -1,13 +1,13 @@ """Support for Tile® Bluetooth trackers.""" -import logging from datetime import timedelta +import logging from pytile import async_login from pytile.errors import SessionExpiredError, TileError import voluptuous as vol from homeassistant.components.device_tracker import PLATFORM_SCHEMA -from homeassistant.const import CONF_USERNAME, CONF_MONITORED_VARIABLES, CONF_PASSWORD +from homeassistant.const import CONF_MONITORED_VARIABLES, CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.event import async_track_time_interval from homeassistant.util import slugify diff --git a/homeassistant/components/time_date/sensor.py b/homeassistant/components/time_date/sensor.py index cbe4c85ace3..1deb564133e 100644 --- a/homeassistant/components/time_date/sensor.py +++ b/homeassistant/components/time_date/sensor.py @@ -4,13 +4,13 @@ import logging import voluptuous as vol -from homeassistant.core import callback from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_DISPLAY_OPTIONS -from homeassistant.helpers.entity import Entity +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt_util +from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_point_in_utc_time +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -20,7 +20,8 @@ OPTION_TYPES = { "time": "Time", "date": "Date", "date_time": "Date & Time", - "date_time_iso": "Date & Time ISO", + "date_time_utc": "Date & Time (UTC)", + "date_time_iso": "Date & Time (ISO)", "time_date": "Time & Date", "beat": "Internet Time", "time_utc": "Time (UTC)", @@ -102,6 +103,7 @@ class TimeDateSensor(Entity): time = dt_util.as_local(time_date).strftime(TIME_STR_FORMAT) time_utc = time_date.strftime(TIME_STR_FORMAT) date = dt_util.as_local(time_date).date().isoformat() + date_utc = time_date.date().isoformat() # Calculate Swatch Internet Time. time_bmt = time_date + timedelta(hours=1) @@ -119,6 +121,8 @@ class TimeDateSensor(Entity): self._state = date elif self.type == "date_time": self._state = f"{date}, {time}" + elif self.type == "date_time_utc": + self._state = f"{date_utc}, {time_utc}" elif self.type == "time_date": self._state = f"{time}, {date}" elif self.type == "time_utc": diff --git a/homeassistant/components/timer/__init__.py b/homeassistant/components/timer/__init__.py index 3ac41b84d82..0cc707f5a45 100644 --- a/homeassistant/components/timer/__init__.py +++ b/homeassistant/components/timer/__init__.py @@ -4,12 +4,12 @@ import logging import voluptuous as vol -from homeassistant.const import CONF_ICON, CONF_NAME +from homeassistant.const import CONF_ICON, CONF_NAME, SERVICE_RELOAD import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.restore_state import RestoreEntity +import homeassistant.helpers.service import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -37,10 +37,6 @@ SERVICE_PAUSE = "pause" SERVICE_CANCEL = "cancel" SERVICE_FINISH = "finish" -SERVICE_SCHEMA_DURATION = ENTITY_SERVICE_SCHEMA.extend( - {vol.Optional(ATTR_DURATION, default=timedelta(DEFAULT_DURATION)): cv.time_period} -) - CONFIG_SCHEMA = vol.Schema( { DOMAIN: cv.schema_with_slug_keys( @@ -59,11 +55,51 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) +RELOAD_SERVICE_SCHEMA = vol.Schema({}) + async def async_setup(hass, config): """Set up a timer.""" component = EntityComponent(_LOGGER, DOMAIN, hass) + entities = await _async_process_config(hass, config) + + async def reload_service_handler(service_call): + """Remove all input booleans and load new ones from config.""" + conf = await component.async_prepare_reload() + if conf is None: + return + new_entities = await _async_process_config(hass, conf) + if new_entities: + await component.async_add_entities(new_entities) + + homeassistant.helpers.service.async_register_admin_service( + hass, + DOMAIN, + SERVICE_RELOAD, + reload_service_handler, + schema=RELOAD_SERVICE_SCHEMA, + ) + component.async_register_entity_service( + SERVICE_START, + { + vol.Optional( + ATTR_DURATION, default=timedelta(DEFAULT_DURATION) + ): cv.time_period + }, + "async_start", + ) + component.async_register_entity_service(SERVICE_PAUSE, {}, "async_pause") + component.async_register_entity_service(SERVICE_CANCEL, {}, "async_cancel") + component.async_register_entity_service(SERVICE_FINISH, {}, "async_finish") + + if entities: + await component.async_add_entities(entities) + return True + + +async def _async_process_config(hass, config): + """Process config and create list of entities.""" entities = [] for object_id, cfg in config[DOMAIN].items(): @@ -76,24 +112,7 @@ async def async_setup(hass, config): entities.append(Timer(hass, object_id, name, icon, duration)) - if not entities: - return False - - component.async_register_entity_service( - SERVICE_START, SERVICE_SCHEMA_DURATION, "async_start" - ) - component.async_register_entity_service( - SERVICE_PAUSE, ENTITY_SERVICE_SCHEMA, "async_pause" - ) - component.async_register_entity_service( - SERVICE_CANCEL, ENTITY_SERVICE_SCHEMA, "async_cancel" - ) - component.async_register_entity_service( - SERVICE_FINISH, ENTITY_SERVICE_SCHEMA, "async_finish" - ) - - await component.async_add_entities(entities) - return True + return entities class Timer(RestoreEntity): @@ -162,7 +181,6 @@ class Timer(RestoreEntity): event = EVENT_TIMER_RESTARTED self._state = STATUS_ACTIVE - # pylint: disable=redefined-outer-name start = dt_util.utcnow() if self._remaining and newduration is None: self._end = start + self._remaining diff --git a/homeassistant/components/todoist/calendar.py b/homeassistant/components/todoist/calendar.py index 1179fd90868..ed6476af229 100644 --- a/homeassistant/components/todoist/calendar.py +++ b/homeassistant/components/todoist/calendar.py @@ -2,99 +2,51 @@ from datetime import datetime, timedelta import logging +from todoist.api import TodoistAPI import voluptuous as vol -from homeassistant.components.calendar import ( - DOMAIN, - PLATFORM_SCHEMA, - CalendarEventDevice, -) +from homeassistant.components.calendar import PLATFORM_SCHEMA, CalendarEventDevice from homeassistant.const import CONF_ID, CONF_NAME, CONF_TOKEN import homeassistant.helpers.config_validation as cv from homeassistant.helpers.template import DATE_STR_FORMAT from homeassistant.util import Throttle, dt +from .const import ( + ALL_DAY, + ALL_TASKS, + CHECKED, + COMPLETED, + CONF_EXTRA_PROJECTS, + CONF_PROJECT_DUE_DATE, + CONF_PROJECT_LABEL_WHITELIST, + CONF_PROJECT_WHITELIST, + CONTENT, + DATETIME, + DESCRIPTION, + DOMAIN, + DUE, + DUE_DATE, + DUE_DATE_LANG, + DUE_DATE_STRING, + DUE_DATE_VALID_LANGS, + DUE_TODAY, + END, + ID, + LABELS, + NAME, + OVERDUE, + PRIORITY, + PROJECT_ID, + PROJECT_NAME, + PROJECTS, + SERVICE_NEW_TASK, + START, + SUMMARY, + TASKS, +) + _LOGGER = logging.getLogger(__name__) -CONF_EXTRA_PROJECTS = "custom_projects" -CONF_PROJECT_DUE_DATE = "due_date_days" -CONF_PROJECT_LABEL_WHITELIST = "labels" -CONF_PROJECT_WHITELIST = "include_projects" - -# Calendar Platform: Does this calendar event last all day? -ALL_DAY = "all_day" -# Attribute: All tasks in this project -ALL_TASKS = "all_tasks" -# Todoist API: "Completed" flag -- 1 if complete, else 0 -CHECKED = "checked" -# Attribute: Is this task complete? -COMPLETED = "completed" -# Todoist API: What is this task about? -# Service Call: What is this task about? -CONTENT = "content" -# Calendar Platform: Get a calendar event's description -DESCRIPTION = "description" -# Calendar Platform: Used in the '_get_date()' method -DATETIME = "dateTime" -DUE = "due" -# Service Call: When is this task due (in natural language)? -DUE_DATE_STRING = "due_date_string" -# Service Call: The language of DUE_DATE_STRING -DUE_DATE_LANG = "due_date_lang" -# Service Call: The available options of DUE_DATE_LANG -DUE_DATE_VALID_LANGS = [ - "en", - "da", - "pl", - "zh", - "ko", - "de", - "pt", - "ja", - "it", - "fr", - "sv", - "ru", - "es", - "nl", -] -# Attribute: When is this task due? -# Service Call: When is this task due? -DUE_DATE = "due_date" -# Todoist API: Look up a task's due date -DUE_DATE_UTC = "due_date_utc" -# Attribute: Is this task due today? -DUE_TODAY = "due_today" -# Calendar Platform: When a calendar event ends -END = "end" -# Todoist API: Look up a Project/Label/Task ID -ID = "id" -# Todoist API: Fetch all labels -# Service Call: What are the labels attached to this task? -LABELS = "labels" -# Todoist API: "Name" value -NAME = "name" -# Attribute: Is this task overdue? -OVERDUE = "overdue" -# Attribute: What is this task's priority? -# Todoist API: Get a task's priority -# Service Call: What is this task's priority? -PRIORITY = "priority" -# Todoist API: Look up the Project ID a Task belongs to -PROJECT_ID = "project_id" -# Service Call: What Project do you want a Task added to? -PROJECT_NAME = "project" -# Todoist API: Fetch all Projects -PROJECTS = "projects" -# Calendar Platform: When does a calendar event start? -START = "start" -# Calendar Platform: What is the next calendar event about? -SUMMARY = "summary" -# Todoist API: Fetch all Tasks -TASKS = "items" - -SERVICE_NEW_TASK = "todoist_new_task" - NEW_TASK_SERVICE_SCHEMA = vol.Schema( { vol.Required(CONTENT): cv.string, @@ -143,8 +95,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): project_id_lookup = {} label_id_lookup = {} - from todoist.api import TodoistAPI - api = TodoistAPI(token) api.sync() diff --git a/homeassistant/components/todoist/const.py b/homeassistant/components/todoist/const.py new file mode 100644 index 00000000000..a1e37bf0292 --- /dev/null +++ b/homeassistant/components/todoist/const.py @@ -0,0 +1,79 @@ +"""Constants for the Todoist component.""" +CONF_EXTRA_PROJECTS = "custom_projects" +CONF_PROJECT_DUE_DATE = "due_date_days" +CONF_PROJECT_LABEL_WHITELIST = "labels" +CONF_PROJECT_WHITELIST = "include_projects" + +# Calendar Platform: Does this calendar event last all day? +ALL_DAY = "all_day" +# Attribute: All tasks in this project +ALL_TASKS = "all_tasks" +# Todoist API: "Completed" flag -- 1 if complete, else 0 +CHECKED = "checked" +# Attribute: Is this task complete? +COMPLETED = "completed" +# Todoist API: What is this task about? +# Service Call: What is this task about? +CONTENT = "content" +# Calendar Platform: Get a calendar event's description +DESCRIPTION = "description" +# Calendar Platform: Used in the '_get_date()' method +DATETIME = "dateTime" +DUE = "due" +# Service Call: When is this task due (in natural language)? +DUE_DATE_STRING = "due_date_string" +# Service Call: The language of DUE_DATE_STRING +DUE_DATE_LANG = "due_date_lang" +# Service Call: The available options of DUE_DATE_LANG +DUE_DATE_VALID_LANGS = [ + "en", + "da", + "pl", + "zh", + "ko", + "de", + "pt", + "ja", + "it", + "fr", + "sv", + "ru", + "es", + "nl", +] +# Attribute: When is this task due? +# Service Call: When is this task due? +DUE_DATE = "due_date" +# Attribute: Is this task due today? +DUE_TODAY = "due_today" +# Calendar Platform: When a calendar event ends +END = "end" +# Todoist API: Look up a Project/Label/Task ID +ID = "id" +# Todoist API: Fetch all labels +# Service Call: What are the labels attached to this task? +LABELS = "labels" +# Todoist API: "Name" value +NAME = "name" +# Attribute: Is this task overdue? +OVERDUE = "overdue" +# Attribute: What is this task's priority? +# Todoist API: Get a task's priority +# Service Call: What is this task's priority? +PRIORITY = "priority" +# Todoist API: Look up the Project ID a Task belongs to +PROJECT_ID = "project_id" +# Service Call: What Project do you want a Task added to? +PROJECT_NAME = "project" +# Todoist API: Fetch all Projects +PROJECTS = "projects" +# Calendar Platform: When does a calendar event start? +START = "start" +# Calendar Platform: What is the next calendar event about? +SUMMARY = "summary" +# Todoist API: Fetch all Tasks +TASKS = "items" + +DOMAIN = "todoist" + +SERVICE_NEW_TASK = "new_task" diff --git a/homeassistant/components/todoist/services.yaml b/homeassistant/components/todoist/services.yaml index c2d23cc4bec..3382e27693d 100644 --- a/homeassistant/components/todoist/services.yaml +++ b/homeassistant/components/todoist/services.yaml @@ -21,5 +21,5 @@ new_task: example: en due_date: description: The day this task is due, in format YYYY-MM-DD. - example: 2019-10-22 + example: "2019-10-22" diff --git a/homeassistant/components/tof/sensor.py b/homeassistant/components/tof/sensor.py index d9e85b1e22b..58f50f4899e 100644 --- a/homeassistant/components/tof/sensor.py +++ b/homeassistant/components/tof/sensor.py @@ -1,15 +1,16 @@ """Platform for Time of Flight sensor VL53L1X from STMicroelectronics.""" import asyncio -import logging from functools import partial +import logging +from VL53L1X2 import VL53L1X # pylint: disable=import-error import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA -import homeassistant.helpers.config_validation as cv from homeassistant.components import rpi_gpio +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 _LOGGER = logging.getLogger(__name__) @@ -51,7 +52,6 @@ def init_tof_1(xshut): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Reset and initialize the VL53L1X ToF Sensor from STMicroelectronics.""" - from VL53L1X2 import VL53L1X # pylint: disable=import-error name = config.get(CONF_NAME) bus_number = config.get(CONF_I2C_BUS) diff --git a/homeassistant/components/tomato/device_tracker.py b/homeassistant/components/tomato/device_tracker.py index 57348c9710a..5a5f1b1985b 100644 --- a/homeassistant/components/tomato/device_tracker.py +++ b/homeassistant/components/tomato/device_tracker.py @@ -6,7 +6,6 @@ import re import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, @@ -14,12 +13,13 @@ from homeassistant.components.device_tracker import ( ) from homeassistant.const import ( CONF_HOST, + CONF_PASSWORD, CONF_PORT, CONF_SSL, - CONF_VERIFY_SSL, - CONF_PASSWORD, CONF_USERNAME, + CONF_VERIFY_SSL, ) +import homeassistant.helpers.config_validation as cv CONF_HTTP_ID = "http_id" @@ -124,7 +124,7 @@ class TomatoDeviceScanner(DeviceScanner): # We get this if we could not connect to the router or # an invalid http_id was supplied. _LOGGER.exception( - "Failed to connect to the router or " "invalid http_id supplied" + "Failed to connect to the router or invalid http_id supplied" ) return False diff --git a/homeassistant/components/toon/.translations/da.json b/homeassistant/components/toon/.translations/da.json index 9200f80add0..e4f73bc7c6b 100644 --- a/homeassistant/components/toon/.translations/da.json +++ b/homeassistant/components/toon/.translations/da.json @@ -3,9 +3,9 @@ "abort": { "client_id": "Klient-id'et fra konfigurationen er ugyldigt.", "client_secret": "Klientens hemmelighed fra konfigurationen er ugyldig.", - "no_agreements": "Denne konto har ingen Toon sk\u00e6rme.", + "no_agreements": "Denne konto har ingen Toon-sk\u00e6rme.", "no_app": "Du skal konfigurere Toon f\u00f8r du kan godkende med det. [L\u00e6s venligst vejledningen](https://www.home-assistant.io/components/toon/).", - "unknown_auth_fail": "Der opstod en uventet fejl under autentificering." + "unknown_auth_fail": "Der opstod en uventet fejl under godkendelse." }, "error": { "credentials": "De angivne legitimationsoplysninger er ugyldige.", @@ -18,8 +18,8 @@ "tenant": "Tenant", "username": "Brugernavn" }, - "description": "Godkend med din Eneco Toon konto (ikke udviklerkontoen).", - "title": "Link din Toon konto" + "description": "Godkend med din Eneco Toon-konto (ikke udviklerkontoen).", + "title": "Forbind din Toon-konto" }, "display": { "data": { diff --git a/homeassistant/components/toon/.translations/ru.json b/homeassistant/components/toon/.translations/ru.json index 58e6f53986c..427f717e3ad 100644 --- a/homeassistant/components/toon/.translations/ru.json +++ b/homeassistant/components/toon/.translations/ru.json @@ -3,12 +3,12 @@ "abort": { "client_id": "Client ID \u0438\u0437 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d.", "client_secret": "Client secret \u0438\u0437 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d.", - "no_agreements": "\u0423 \u044d\u0442\u043e\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u043d\u0435\u0442 \u0434\u0438\u0441\u043f\u043b\u0435\u0435\u0432 Toon.", + "no_agreements": "\u0423 \u044d\u0442\u043e\u0439 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u043d\u0435\u0442 \u0434\u0438\u0441\u043f\u043b\u0435\u0435\u0432 Toon.", "no_app": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 Toon \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \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](https://www.home-assistant.io/components/toon/).", "unknown_auth_fail": "\u0412\u043e \u0432\u0440\u0435\u043c\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "error": { - "credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", + "credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", "display_exists": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0433\u043e \u0434\u0438\u0441\u043f\u043b\u0435\u044f \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "step": { @@ -18,8 +18,8 @@ "tenant": "\u0412\u043b\u0430\u0434\u0435\u043b\u0435\u0446", "username": "\u041b\u043e\u0433\u0438\u043d" }, - "description": "\u0412\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u0441\u0432\u043e\u0435\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Eneco Toon (\u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0430).", - "title": "\u041f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c Toon" + "description": "\u0412\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u0441\u0432\u043e\u0435\u0439 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Eneco Toon (\u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0443\u0447\u0451\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0430).", + "title": "\u041f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0443\u0447\u0451\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c Toon" }, "display": { "data": { diff --git a/homeassistant/components/toon/__init__.py b/homeassistant/components/toon/__init__.py index ed627fdc924..348826a1264 100644 --- a/homeassistant/components/toon/__init__.py +++ b/homeassistant/components/toon/__init__.py @@ -1,17 +1,18 @@ """Support for Toon van Eneco devices.""" +from functools import partial import logging from typing import Any, Dict -from functools import partial +from toonapilib import Toon import voluptuous as vol +from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME from homeassistant.core import callback -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, CONF_SCAN_INTERVAL from homeassistant.helpers import config_validation as cv, device_registry as dr +from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.dispatcher import dispatcher_send, async_dispatcher_connect +from homeassistant.helpers.typing import ConfigType, HomeAssistantType from . import config_flow # noqa: F401 from .const import ( @@ -19,10 +20,10 @@ from .const import ( CONF_CLIENT_SECRET, CONF_DISPLAY, CONF_TENANT, + DATA_TOON, DATA_TOON_CLIENT, DATA_TOON_CONFIG, DATA_TOON_UPDATED, - DATA_TOON, DEFAULT_SCAN_INTERVAL, DOMAIN, ) @@ -63,7 +64,6 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistantType, entry: ConfigType) -> bool: """Set up Toon from a config entry.""" - from toonapilib import Toon conf = hass.data.get(DATA_TOON_CONFIG) @@ -139,7 +139,7 @@ class ToonData: """Update all Toon data and notify entities.""" # Ignore the TTL meganism from client library # It causes a lots of issues, hence we take control over caching - self._toon._clear_cache() # pylint: disable=W0212 + self._toon._clear_cache() # pylint: disable=protected-access # Gather data from client library (single API call) self.gas = self._toon.gas diff --git a/homeassistant/components/toon/binary_sensor.py b/homeassistant/components/toon/binary_sensor.py index 9962e2c32d3..7cf52919efe 100644 --- a/homeassistant/components/toon/binary_sensor.py +++ b/homeassistant/components/toon/binary_sensor.py @@ -8,11 +8,11 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType from . import ( - ToonData, - ToonEntity, - ToonDisplayDeviceEntity, ToonBoilerDeviceEntity, ToonBoilerModuleDeviceEntity, + ToonData, + ToonDisplayDeviceEntity, + ToonEntity, ) from .const import DATA_TOON, DOMAIN diff --git a/homeassistant/components/toon/climate.py b/homeassistant/components/toon/climate.py index cfe07adfda3..9ce9991c371 100644 --- a/homeassistant/components/toon/climate.py +++ b/homeassistant/components/toon/climate.py @@ -5,6 +5,8 @@ from typing import Any, Dict, List, Optional from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, HVAC_MODE_HEAT, PRESET_AWAY, PRESET_COMFORT, @@ -12,8 +14,6 @@ from homeassistant.components.climate.const import ( PRESET_SLEEP, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, - CURRENT_HVAC_HEAT, - CURRENT_HVAC_IDLE, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS @@ -21,8 +21,8 @@ from homeassistant.helpers.typing import HomeAssistantType from . import ToonData, ToonDisplayDeviceEntity from .const import ( - DATA_TOON_CLIENT, DATA_TOON, + DATA_TOON_CLIENT, DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, DOMAIN, diff --git a/homeassistant/components/toon/config_flow.py b/homeassistant/components/toon/config_flow.py index 62c141b003a..ce4f347eaf2 100644 --- a/homeassistant/components/toon/config_flow.py +++ b/homeassistant/components/toon/config_flow.py @@ -1,8 +1,15 @@ """Config flow to configure the Toon component.""" from collections import OrderedDict -import logging from functools import partial +import logging +from toonapilib import Toon +from toonapilib.toonapilibexceptions import ( + AgreementsRetrievalError, + InvalidConsumerKey, + InvalidConsumerSecret, + InvalidCredentials, +) import voluptuous as vol from homeassistant import config_entries @@ -67,13 +74,6 @@ class ToonFlowHandler(config_entries.ConfigFlow): async def async_step_authenticate(self, user_input=None): """Attempt to authenticate with the Toon account.""" - from toonapilib import Toon - from toonapilib.toonapilibexceptions import ( - InvalidConsumerSecret, - InvalidConsumerKey, - InvalidCredentials, - AgreementsRetrievalError, - ) if user_input is None: return await self._show_authenticaticate_form() @@ -129,7 +129,6 @@ class ToonFlowHandler(config_entries.ConfigFlow): async def async_step_display(self, user_input=None): """Select Toon display to add.""" - from toonapilib import Toon if not self.displays: return self.async_abort(reason="no_displays") diff --git a/homeassistant/components/toon/sensor.py b/homeassistant/components/toon/sensor.py index f82bcb7ac1b..79a8fa28540 100644 --- a/homeassistant/components/toon/sensor.py +++ b/homeassistant/components/toon/sensor.py @@ -2,18 +2,18 @@ import logging from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.const import ENERGY_KILO_WATT_HOUR, POWER_WATT +from homeassistant.helpers.typing import HomeAssistantType from . import ( + ToonBoilerDeviceEntity, ToonData, - ToonEntity, ToonElectricityMeterDeviceEntity, + ToonEntity, ToonGasMeterDeviceEntity, ToonSolarDeviceEntity, - ToonBoilerDeviceEntity, ) -from .const import CURRENCY_EUR, DATA_TOON, DOMAIN, VOLUME_CM3, VOLUME_M3, RATIO_PERCENT +from .const import CURRENCY_EUR, DATA_TOON, DOMAIN, RATIO_PERCENT, VOLUME_CM3, VOLUME_M3 _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/torque/sensor.py b/homeassistant/components/torque/sensor.py index 10161856a47..f084c135e47 100644 --- a/homeassistant/components/torque/sensor.py +++ b/homeassistant/components/torque/sensor.py @@ -4,12 +4,12 @@ import re import voluptuous as vol -from homeassistant.core import callback from homeassistant.components.http import HomeAssistantView from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_EMAIL, CONF_NAME -from homeassistant.helpers.entity import Entity +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/totalconnect/__init__.py b/homeassistant/components/totalconnect/__init__.py index c14ac36057e..020f2d9c07f 100644 --- a/homeassistant/components/totalconnect/__init__.py +++ b/homeassistant/components/totalconnect/__init__.py @@ -1,13 +1,12 @@ """The totalconnect component.""" import logging -import voluptuous as vol from total_connect_client import TotalConnectClient +import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers import discovery from homeassistant.const import CONF_PASSWORD, CONF_USERNAME - +from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/totalconnect/alarm_control_panel.py b/homeassistant/components/totalconnect/alarm_control_panel.py index e5f0b0c8279..ed77fc4eea0 100644 --- a/homeassistant/components/totalconnect/alarm_control_panel.py +++ b/homeassistant/components/totalconnect/alarm_control_panel.py @@ -2,15 +2,20 @@ import logging import homeassistant.components.alarm_control_panel as alarm +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, +) from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, - STATE_ALARM_DISARMED, STATE_ALARM_ARMING, + STATE_ALARM_DISARMED, STATE_ALARM_DISARMING, STATE_ALARM_TRIGGERED, - STATE_ALARM_ARMED_CUSTOM_BYPASS, ) from . import DOMAIN as TOTALCONNECT_DOMAIN @@ -55,6 +60,11 @@ class TotalConnectAlarm(alarm.AlarmControlPanel): """Return the state of the device.""" return self._state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT + @property def device_state_attributes(self): """Return the state attributes of the device.""" @@ -109,7 +119,7 @@ class TotalConnectAlarm(alarm.AlarmControlPanel): attr["triggered_source"] = "Carbon Monoxide" else: logging.info( - "Total Connect Client returned unknown " "status code: %s", status + "Total Connect Client returned unknown status code: %s", status ) state = None diff --git a/homeassistant/components/touchline/climate.py b/homeassistant/components/touchline/climate.py index 46ac30d0d97..984e454ae02 100644 --- a/homeassistant/components/touchline/climate.py +++ b/homeassistant/components/touchline/climate.py @@ -2,14 +2,15 @@ import logging from typing import List +from pytouchline import PyTouchline import voluptuous as vol -from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice from homeassistant.components.climate.const import ( - SUPPORT_TARGET_TEMPERATURE, HVAC_MODE_HEAT, + SUPPORT_TARGET_TEMPERATURE, ) -from homeassistant.const import CONF_HOST, TEMP_CELSIUS, ATTR_TEMPERATURE +from homeassistant.const import ATTR_TEMPERATURE, CONF_HOST, TEMP_CELSIUS import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -21,7 +22,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_HOST): cv.string}) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Touchline devices.""" - from pytouchline import PyTouchline host = config[CONF_HOST] py_touchline = PyTouchline() diff --git a/homeassistant/components/tplink/.translations/da.json b/homeassistant/components/tplink/.translations/da.json index cdd953ff5c3..5225a89fb95 100644 --- a/homeassistant/components/tplink/.translations/da.json +++ b/homeassistant/components/tplink/.translations/da.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "Vil du konfigurere TP-Link smart devices?", + "description": "Vil du konfigurere TP-Link-smartenheder?", "title": "TP-Link Smart Home" } }, diff --git a/homeassistant/components/tplink/.translations/ko.json b/homeassistant/components/tplink/.translations/ko.json index 05bebdd1455..89255d78518 100644 --- a/homeassistant/components/tplink/.translations/ko.json +++ b/homeassistant/components/tplink/.translations/ko.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "TP-Link \uc2a4\ub9c8\ud2b8 \uae30\uae30\ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "TP-Link \uc2a4\ub9c8\ud2b8 \uae30\uae30\ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "TP-Link Smart Home" } }, diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index 7aa261564f3..764060135a2 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -13,8 +13,8 @@ from .common import ( CONF_DIMMER, CONF_DISCOVERY, CONF_LIGHT, - CONF_SWITCH, CONF_STRIP, + CONF_SWITCH, SmartDevices, async_discover_devices, get_static_devices, diff --git a/homeassistant/components/tplink/common.py b/homeassistant/components/tplink/common.py index 548edc6822c..0e06babbd52 100644 --- a/homeassistant/components/tplink/common.py +++ b/homeassistant/components/tplink/common.py @@ -32,7 +32,7 @@ class SmartDevices: def __init__( self, lights: List[SmartDevice] = None, switches: List[SmartDevice] = None ): - """Constructor.""" + """Initialize device holder.""" self._lights = lights or [] self._switches = switches or [] diff --git a/homeassistant/components/tplink/const.py b/homeassistant/components/tplink/const.py index 583c25e285c..8b85b8afd74 100644 --- a/homeassistant/components/tplink/const.py +++ b/homeassistant/components/tplink/const.py @@ -1,3 +1,5 @@ """Const for TP-Link.""" +import datetime DOMAIN = "tplink" +MIN_TIME_BETWEEN_UPDATES = datetime.timedelta(seconds=8) diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index 117ebf75025..ec3307fc87e 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -126,23 +126,27 @@ class TPLinkSmartBulb(Light): def turn_on(self, **kwargs): """Turn the light on.""" + self._state = True self.smartbulb.state = SmartBulb.BULB_STATE_ON if ATTR_COLOR_TEMP in kwargs: - self.smartbulb.color_temp = mired_to_kelvin(kwargs[ATTR_COLOR_TEMP]) + self._color_temp = kwargs.get(ATTR_COLOR_TEMP) + self.smartbulb.color_temp = mired_to_kelvin(self._color_temp) - brightness = brightness_to_percentage( - kwargs.get(ATTR_BRIGHTNESS, self.brightness or 255) - ) + brightness_value = kwargs.get(ATTR_BRIGHTNESS, self.brightness or 255) + brightness_pct = brightness_to_percentage(brightness_value) if ATTR_HS_COLOR in kwargs: - hue, sat = kwargs.get(ATTR_HS_COLOR) - hsv = (int(hue), int(sat), brightness) + self._hs = kwargs.get(ATTR_HS_COLOR) + hue, sat = self._hs + hsv = (int(hue), int(sat), brightness_pct) self.smartbulb.hsv = hsv elif ATTR_BRIGHTNESS in kwargs: - self.smartbulb.brightness = brightness + self._brightness = brightness_value + self.smartbulb.brightness = brightness_pct def turn_off(self, **kwargs): """Turn the light off.""" + self._state = False self.smartbulb.state = SmartBulb.BULB_STATE_OFF @property @@ -177,6 +181,15 @@ class TPLinkSmartBulb(Light): def update(self): """Update the TP-Link Bulb's state.""" + if self._supported_features is None: + # First run, update by blocking. + self.do_update() + else: + # Not first run, update in the background. + self.hass.add_job(self.do_update) + + def do_update(self): + """Update states.""" try: if self._supported_features is None: self.get_features() diff --git a/homeassistant/components/tplink/switch.py b/homeassistant/components/tplink/switch.py index 791d358c509..b6ca69f4ccd 100644 --- a/homeassistant/components/tplink/switch.py +++ b/homeassistant/components/tplink/switch.py @@ -115,6 +115,12 @@ class SmartPlugSwitch(SwitchDevice): """Return the state attributes of the device.""" return self._emeter_params + @property + def _plug_from_context(self): + """Return the plug from the context.""" + children = self.smartplug.sys_info["children"] + return next(c for c in children if c["id"] == self.smartplug.context) + def update(self): """Update the TP-Link switch's state.""" try: @@ -126,21 +132,13 @@ class SmartPlugSwitch(SwitchDevice): self._alias = self.smartplug.alias self._device_id = self._mac else: - self._alias = [ - child - for child in self.smartplug.sys_info["children"] - if child["id"] == self.smartplug.context - ][0]["alias"] + self._alias = self._plug_from_context["alias"] self._device_id = self.smartplug.context if self.smartplug.context is None: self._state = self.smartplug.state == self.smartplug.SWITCH_STATE_ON else: - self._state = [ - child - for child in self.smartplug.sys_info["children"] - if child["id"] == self.smartplug.context - ][0]["state"] == 1 + self._state = self._plug_from_context["state"] == 1 if self.smartplug.has_emeter: emeter_readings = self.smartplug.get_emeter_realtime() diff --git a/homeassistant/components/traccar/.translations/da.json b/homeassistant/components/traccar/.translations/da.json index af3963f8c0f..b1ab350c905 100644 --- a/homeassistant/components/traccar/.translations/da.json +++ b/homeassistant/components/traccar/.translations/da.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "not_internet_accessible": "Dit Home Assistant system skal v\u00e6re tilg\u00e6ngeligt fra internettet for at modtage Traccar meddelelser.", - "one_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning." + "not_internet_accessible": "Din Home Assistant-instans skal v\u00e6re tilg\u00e6ngelig fra internettet for at modtage Traccar-meddelelser.", + "one_instance_allowed": "Kun en enkelt instans er n\u00f8dvendig." }, "create_entry": { - "default": "For at sende begivenheder til Home Assistant skal du konfigurere webhook funktionen i Traccar.\n\n Brug f\u00f8lgende URL: `{webhook_url}`\n \n Se [dokumentationen]({docs_url}) for yderligere oplysninger." + "default": "For at sende h\u00e6ndelser til Home Assistant skal du konfigurere webhook-funktionen i Traccar.\n\nBrug f\u00f8lgende webadresse: `{webhook_url}`\n \nSe [dokumentationen]({docs_url}) for yderligere oplysninger." }, "step": { "user": { diff --git a/homeassistant/components/traccar/.translations/ko.json b/homeassistant/components/traccar/.translations/ko.json index d9f31967e68..40e1aaf4d6b 100644 --- a/homeassistant/components/traccar/.translations/ko.json +++ b/homeassistant/components/traccar/.translations/ko.json @@ -9,7 +9,7 @@ }, "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" } }, diff --git a/homeassistant/components/traccar/.translations/tr.json b/homeassistant/components/traccar/.translations/tr.json new file mode 100644 index 00000000000..22944e1c4cc --- /dev/null +++ b/homeassistant/components/traccar/.translations/tr.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "title": "Traccar'\u0131 kur" + } + }, + "title": "Traccar" + } +} \ No newline at end of file diff --git a/homeassistant/components/traccar/__init__.py b/homeassistant/components/traccar/__init__.py index 5eb87de0db2..7e94ab0a351 100644 --- a/homeassistant/components/traccar/__init__.py +++ b/homeassistant/components/traccar/__init__.py @@ -1,14 +1,15 @@ """Support for Traccar.""" import logging -import voluptuous as vol from aiohttp import web +import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY, HTTP_OK, CONF_WEBHOOK_ID -from homeassistant.helpers import config_entry_flow -from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER +from homeassistant.const import 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 + from .const import ( ATTR_ACCURACY, ATTR_ALTITUDE, diff --git a/homeassistant/components/traccar/config_flow.py b/homeassistant/components/traccar/config_flow.py index 4bd75910163..3702316ffb9 100644 --- a/homeassistant/components/traccar/config_flow.py +++ b/homeassistant/components/traccar/config_flow.py @@ -1,7 +1,7 @@ """Config flow for Traccar.""" from homeassistant.helpers import config_entry_flow -from .const import DOMAIN +from .const import DOMAIN config_entry_flow.register_webhook_flow( DOMAIN, diff --git a/homeassistant/components/traccar/device_tracker.py b/homeassistant/components/traccar/device_tracker.py index c7fdda013b0..7f23d6cf31e 100644 --- a/homeassistant/components/traccar/device_tracker.py +++ b/homeassistant/components/traccar/device_tracker.py @@ -2,29 +2,31 @@ from datetime import datetime, timedelta import logging +from pytraccar.api import API +from stringcase import camelcase import voluptuous as vol -from homeassistant.core import callback -from homeassistant.const import ( - CONF_HOST, - CONF_PORT, - CONF_SSL, - CONF_VERIFY_SSL, - CONF_PASSWORD, - CONF_USERNAME, - CONF_SCAN_INTERVAL, - CONF_MONITORED_CONDITIONS, - CONF_EVENT, -) from homeassistant.components.device_tracker import PLATFORM_SCHEMA, SOURCE_TYPE_GPS from homeassistant.components.device_tracker.config_entry import TrackerEntity +from homeassistant.const import ( + CONF_EVENT, + CONF_HOST, + CONF_MONITORED_CONDITIONS, + CONF_PASSWORD, + CONF_PORT, + CONF_SCAN_INTERVAL, + CONF_SSL, + CONF_USERNAME, + CONF_VERIFY_SSL, +) +from homeassistant.core import callback from homeassistant.helpers import device_registry +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import HomeAssistantType -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.event import async_track_time_interval from homeassistant.util import slugify from . import DOMAIN, TRACKER_UPDATE @@ -41,30 +43,29 @@ from .const import ( ATTR_MOTION, ATTR_SPEED, ATTR_STATUS, - ATTR_TRACKER, ATTR_TRACCAR_ID, - EVENT_DEVICE_MOVING, - EVENT_COMMAND_RESULT, - EVENT_DEVICE_FUEL_DROP, - EVENT_GEOFENCE_ENTER, - EVENT_DEVICE_OFFLINE, - EVENT_DRIVER_CHANGED, - EVENT_GEOFENCE_EXIT, - EVENT_DEVICE_OVERSPEED, - EVENT_DEVICE_ONLINE, - EVENT_DEVICE_STOPPED, - EVENT_MAINTENANCE, - EVENT_ALARM, - EVENT_TEXT_MESSAGE, - EVENT_DEVICE_UNKNOWN, - EVENT_IGNITION_OFF, - EVENT_IGNITION_ON, - EVENT_ALL_EVENTS, + ATTR_TRACKER, CONF_MAX_ACCURACY, CONF_SKIP_ACCURACY_ON, + EVENT_ALARM, + EVENT_ALL_EVENTS, + EVENT_COMMAND_RESULT, + EVENT_DEVICE_FUEL_DROP, + EVENT_DEVICE_MOVING, + EVENT_DEVICE_OFFLINE, + EVENT_DEVICE_ONLINE, + EVENT_DEVICE_OVERSPEED, + EVENT_DEVICE_STOPPED, + EVENT_DEVICE_UNKNOWN, + EVENT_DRIVER_CHANGED, + EVENT_GEOFENCE_ENTER, + EVENT_GEOFENCE_EXIT, + EVENT_IGNITION_OFF, + EVENT_IGNITION_ON, + EVENT_MAINTENANCE, + EVENT_TEXT_MESSAGE, ) - _LOGGER = logging.getLogger(__name__) DEFAULT_SCAN_INTERVAL = timedelta(seconds=30) @@ -156,7 +157,6 @@ async def async_setup_entry(hass: HomeAssistantType, entry, async_add_entities): async def async_setup_scanner(hass, config, async_see, discovery_info=None): """Validate the configuration and return a Traccar scanner.""" - from pytraccar.api import API session = async_get_clientsession(hass, config[CONF_VERIFY_SSL]) @@ -199,7 +199,6 @@ class TraccarScanner: event_types, ): """Initialize.""" - from stringcase import camelcase self._event_types = {camelcase(evt): evt for evt in event_types} self._custom_attributes = custom_attributes diff --git a/homeassistant/components/trackr/device_tracker.py b/homeassistant/components/trackr/device_tracker.py index 580f49b908f..07d3c60e256 100644 --- a/homeassistant/components/trackr/device_tracker.py +++ b/homeassistant/components/trackr/device_tracker.py @@ -1,10 +1,11 @@ """Support for the TrackR platform.""" import logging +from pytrackr.api import trackrApiInterface import voluptuous as vol from homeassistant.components.device_tracker import PLATFORM_SCHEMA -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import track_utc_time_change from homeassistant.util import slugify @@ -27,7 +28,6 @@ class TrackRDeviceScanner: def __init__(self, hass, config: dict, see) -> None: """Initialize the TrackR device scanner.""" - from pytrackr.api import trackrApiInterface self.hass = hass self.api = trackrApiInterface( diff --git a/homeassistant/components/tradfri/__init__.py b/homeassistant/components/tradfri/__init__.py index bb445647310..a797607e243 100644 --- a/homeassistant/components/tradfri/__init__.py +++ b/homeassistant/components/tradfri/__init__.py @@ -1,32 +1,33 @@ """Support for IKEA Tradfri.""" import logging -import voluptuous as vol from pytradfri import Gateway, RequestError from pytradfri.api.aiocoap_api import APIFactory +import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.exceptions import ConfigEntryNotReady +import homeassistant.helpers.config_validation as cv from homeassistant.util.json import load_json + from . import config_flow # noqa: F401 from .const import ( - DOMAIN, - CONFIG_FILE, - KEY_GATEWAY, - KEY_API, - CONF_ALLOW_TRADFRI_GROUPS, - DEFAULT_ALLOW_TRADFRI_GROUPS, - TRADFRI_DEVICE_TYPES, - ATTR_TRADFRI_MANUFACTURER, ATTR_TRADFRI_GATEWAY, ATTR_TRADFRI_GATEWAY_MODEL, - CONF_IMPORT_GROUPS, - CONF_IDENTITY, - CONF_HOST, - CONF_KEY, + ATTR_TRADFRI_MANUFACTURER, + CONF_ALLOW_TRADFRI_GROUPS, CONF_GATEWAY_ID, + CONF_HOST, + CONF_IDENTITY, + CONF_IMPORT_GROUPS, + CONF_KEY, + CONFIG_FILE, + DEFAULT_ALLOW_TRADFRI_GROUPS, + DOMAIN, + KEY_API, + KEY_GATEWAY, + TRADFRI_DEVICE_TYPES, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/tradfri/base_class.py b/homeassistant/components/tradfri/base_class.py index ba90fe05d1e..358056d7ef6 100644 --- a/homeassistant/components/tradfri/base_class.py +++ b/homeassistant/components/tradfri/base_class.py @@ -5,6 +5,7 @@ from pytradfri.error import PytradfriError from homeassistant.core import callback from homeassistant.helpers.entity import Entity + from .const import DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/tradfri/config_flow.py b/homeassistant/components/tradfri/config_flow.py index 24c3fbc1876..048541b5402 100644 --- a/homeassistant/components/tradfri/config_flow.py +++ b/homeassistant/components/tradfri/config_flow.py @@ -4,15 +4,18 @@ from collections import OrderedDict from uuid import uuid4 import async_timeout +from pytradfri import Gateway, RequestError +from pytradfri.api.aiocoap_api import APIFactory import voluptuous as vol from homeassistant import config_entries + from .const import ( - CONF_IMPORT_GROUPS, - CONF_IDENTITY, - CONF_HOST, - CONF_KEY, CONF_GATEWAY_ID, + CONF_HOST, + CONF_IDENTITY, + CONF_IMPORT_GROUPS, + CONF_KEY, KEY_SECURITY_CODE, ) @@ -153,8 +156,6 @@ class FlowHandler(config_entries.ConfigFlow): async def authenticate(hass, host, security_code): """Authenticate with a Tradfri hub.""" - from pytradfri.api.aiocoap_api import APIFactory - from pytradfri import RequestError identity = uuid4().hex @@ -173,8 +174,6 @@ async def authenticate(hass, host, security_code): async def get_gateway_info(hass, host, identity, key): """Return info for the gateway.""" - from pytradfri.api.aiocoap_api import APIFactory - from pytradfri import Gateway, RequestError try: factory = APIFactory(host, psk_id=identity, psk=key, loop=hass.loop) diff --git a/homeassistant/components/tradfri/const.py b/homeassistant/components/tradfri/const.py index 01d2f18501d..88225d3282a 100644 --- a/homeassistant/components/tradfri/const.py +++ b/homeassistant/components/tradfri/const.py @@ -1,5 +1,5 @@ """Consts used by Tradfri.""" -from homeassistant.components.light import SUPPORT_TRANSITION, SUPPORT_BRIGHTNESS +from homeassistant.components.light import SUPPORT_BRIGHTNESS, SUPPORT_TRANSITION from homeassistant.const import CONF_HOST # noqa: F401 pylint: disable=unused-import ATTR_DIMMER = "dimmer" diff --git a/homeassistant/components/tradfri/cover.py b/homeassistant/components/tradfri/cover.py index ae7d6a09ce3..d978e512920 100644 --- a/homeassistant/components/tradfri/cover.py +++ b/homeassistant/components/tradfri/cover.py @@ -1,8 +1,9 @@ """Support for IKEA Tradfri covers.""" -from homeassistant.components.cover import CoverDevice, ATTR_POSITION +from homeassistant.components.cover import ATTR_POSITION, CoverDevice + from .base_class import TradfriBaseDevice -from .const import KEY_GATEWAY, KEY_API, CONF_GATEWAY_ID +from .const import CONF_GATEWAY_ID, KEY_API, KEY_GATEWAY async def async_setup_entry(hass, config_entry, async_add_entities): diff --git a/homeassistant/components/tradfri/light.py b/homeassistant/components/tradfri/light.py index 9ee3c5d6a8c..0fe826be9af 100644 --- a/homeassistant/components/tradfri/light.py +++ b/homeassistant/components/tradfri/light.py @@ -1,29 +1,30 @@ """Support for IKEA Tradfri lights.""" import logging -import homeassistant.util.color as color_util from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION, - Light, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, + Light, ) -from .base_class import TradfriBaseDevice, TradfriBaseClass +import homeassistant.util.color as color_util + +from .base_class import TradfriBaseClass, TradfriBaseDevice from .const import ( ATTR_DIMMER, ATTR_HUE, ATTR_SAT, ATTR_TRANSITION_TIME, - SUPPORTED_LIGHT_FEATURES, - SUPPORTED_GROUP_FEATURES, CONF_GATEWAY_ID, CONF_IMPORT_GROUPS, - KEY_GATEWAY, KEY_API, + KEY_GATEWAY, + SUPPORTED_GROUP_FEATURES, + SUPPORTED_LIGHT_FEATURES, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/tradfri/sensor.py b/homeassistant/components/tradfri/sensor.py index cf797f34e3b..c3a08ab1675 100644 --- a/homeassistant/components/tradfri/sensor.py +++ b/homeassistant/components/tradfri/sensor.py @@ -1,8 +1,9 @@ """Support for IKEA Tradfri sensors.""" from homeassistant.const import DEVICE_CLASS_BATTERY + from .base_class import TradfriBaseDevice -from .const import KEY_GATEWAY, KEY_API, CONF_GATEWAY_ID +from .const import CONF_GATEWAY_ID, KEY_API, KEY_GATEWAY async def async_setup_entry(hass, config_entry, async_add_entities): diff --git a/homeassistant/components/tradfri/switch.py b/homeassistant/components/tradfri/switch.py index e1c549a1805..fffbf320c7e 100644 --- a/homeassistant/components/tradfri/switch.py +++ b/homeassistant/components/tradfri/switch.py @@ -1,7 +1,8 @@ """Support for IKEA Tradfri switches.""" from homeassistant.components.switch import SwitchDevice + from .base_class import TradfriBaseDevice -from .const import KEY_GATEWAY, KEY_API, CONF_GATEWAY_ID +from .const import CONF_GATEWAY_ID, KEY_API, KEY_GATEWAY async def async_setup_entry(hass, config_entry, async_add_entities): diff --git a/homeassistant/components/trafikverket_train/sensor.py b/homeassistant/components/trafikverket_train/sensor.py index e6789ca5aee..12f3cf73e50 100644 --- a/homeassistant/components/trafikverket_train/sensor.py +++ b/homeassistant/components/trafikverket_train/sensor.py @@ -11,8 +11,8 @@ from homeassistant.const import ( CONF_API_KEY, CONF_NAME, CONF_WEEKDAY, - WEEKDAYS, DEVICE_CLASS_TIMESTAMP, + WEEKDAYS, ) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/trafikverket_weatherstation/sensor.py b/homeassistant/components/trafikverket_weatherstation/sensor.py index cb80e8d441b..802bb897b96 100644 --- a/homeassistant/components/trafikverket_weatherstation/sensor.py +++ b/homeassistant/components/trafikverket_weatherstation/sensor.py @@ -5,6 +5,7 @@ from datetime import timedelta import logging import aiohttp +from pytrafikverket.trafikverket_weather import TrafikverketWeather import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -106,7 +107,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Trafikverket sensor platform.""" - from pytrafikverket.trafikverket_weather import TrafikverketWeather sensor_name = config[CONF_NAME] sensor_api = config[CONF_API_KEY] diff --git a/homeassistant/components/transmission/.translations/da.json b/homeassistant/components/transmission/.translations/da.json index b14fca00c2c..e84ec938ee2 100644 --- a/homeassistant/components/transmission/.translations/da.json +++ b/homeassistant/components/transmission/.translations/da.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "V\u00e6rten er allerede konfigureret.", - "one_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning." + "one_instance_allowed": "Kun en enkelt instans er n\u00f8dvendig." }, "error": { "cannot_connect": "Kunne ikke oprette forbindelse til v\u00e6rt", @@ -14,7 +14,7 @@ "data": { "scan_interval": "Opdateringsfrekvens" }, - "title": "Konfigurer indstillinger" + "title": "Konfigurationsmuligheder" }, "user": { "data": { @@ -24,7 +24,7 @@ "port": "Port", "username": "Brugernavn" }, - "title": "Konfigurer Transmission klient" + "title": "Konfigurer Transmission-klient" } }, "title": "Transmission" diff --git a/homeassistant/components/transmission/__init__.py b/homeassistant/components/transmission/__init__.py index be41ca85998..3e6f2407d17 100644 --- a/homeassistant/components/transmission/__init__.py +++ b/homeassistant/components/transmission/__init__.py @@ -19,10 +19,10 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.event import async_track_time_interval -from homeassistant.util import slugify from .const import ( ATTR_TORRENT, + DATA_UPDATED, DEFAULT_NAME, DEFAULT_PORT, DEFAULT_SCAN_INTERVAL, @@ -34,7 +34,9 @@ from .errors import AuthenticationError, CannotConnect, UnknownError _LOGGER = logging.getLogger(__name__) -SERVICE_ADD_TORRENT_SCHEMA = vol.Schema({vol.Required(ATTR_TORRENT): cv.string}) +SERVICE_ADD_TORRENT_SCHEMA = vol.Schema( + {vol.Required(ATTR_TORRENT): cv.string, vol.Required(CONF_NAME): cv.string} +) TRANS_SCHEMA = vol.All( vol.Schema( @@ -55,6 +57,8 @@ CONFIG_SCHEMA = vol.Schema( {DOMAIN: vol.All(cv.ensure_list, [TRANS_SCHEMA])}, extra=vol.ALLOW_EXTRA ) +PLATFORMS = ["sensor", "switch"] + async def async_setup(hass, config): """Import the Transmission Component from config.""" @@ -82,15 +86,15 @@ async def async_setup_entry(hass, config_entry): async def async_unload_entry(hass, config_entry): """Unload Transmission Entry from config_entry.""" - client = hass.data[DOMAIN][config_entry.entry_id] - hass.services.async_remove(DOMAIN, client.service_name) + client = hass.data[DOMAIN].pop(config_entry.entry_id) if client.unsub_timer: client.unsub_timer() - for component in "sensor", "switch": - 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) - hass.data[DOMAIN].pop(config_entry.entry_id) + if not hass.data[DOMAIN]: + hass.services.async_remove(DOMAIN, SERVICE_ADD_TORRENT) return True @@ -128,14 +132,10 @@ class TransmissionClient: """Initialize the Transmission RPC API.""" self.hass = hass self.config_entry = config_entry + self.tm_api = None self._tm_data = None self.unsub_timer = None - @property - def service_name(self): - """Return the service name.""" - return slugify(f"{SERVICE_ADD_TORRENT}_{self.config_entry.data[CONF_NAME]}") - @property def api(self): """Return the tm_data object.""" @@ -145,20 +145,20 @@ class TransmissionClient: """Set up the Transmission client.""" try: - api = await get_api(self.hass, self.config_entry.data) + self.tm_api = await get_api(self.hass, self.config_entry.data) except CannotConnect: raise ConfigEntryNotReady except (AuthenticationError, UnknownError): return False - self._tm_data = TransmissionData(self.hass, self.config_entry, api) + self._tm_data = TransmissionData(self.hass, self.config_entry, self.tm_api) await self.hass.async_add_executor_job(self._tm_data.init_torrent_list) await self.hass.async_add_executor_job(self._tm_data.update) self.add_options() self.set_scan_interval(self.config_entry.options[CONF_SCAN_INTERVAL]) - for platform in ["sensor", "switch"]: + for platform in PLATFORMS: self.hass.async_create_task( self.hass.config_entries.async_forward_entry_setup( self.config_entry, platform @@ -167,18 +167,26 @@ class TransmissionClient: def add_torrent(service): """Add new torrent to download.""" + tm_client = None + for entry in self.hass.config_entries.async_entries(DOMAIN): + if entry.data[CONF_NAME] == service.data[CONF_NAME]: + tm_client = self.hass.data[DOMAIN][entry.entry_id] + break + if tm_client is None: + _LOGGER.error("Transmission instance is not found") + return torrent = service.data[ATTR_TORRENT] if torrent.startswith( ("http", "ftp:", "magnet:") ) or self.hass.config.is_allowed_path(torrent): - api.add_torrent(torrent) + tm_client.tm_api.add_torrent(torrent) else: _LOGGER.warning( "Could not add torrent: unsupported type or no permission" ) self.hass.services.async_register( - DOMAIN, self.service_name, add_torrent, schema=SERVICE_ADD_TORRENT_SCHEMA + DOMAIN, SERVICE_ADD_TORRENT, add_torrent, schema=SERVICE_ADD_TORRENT_SCHEMA ) self.config_entry.add_update_listener(self.async_options_updated) @@ -200,7 +208,7 @@ class TransmissionClient: def set_scan_interval(self, scan_interval): """Update scan interval.""" - async def refresh(event_time): + def refresh(event_time): """Get the latest data from Transmission.""" self._tm_data.update() @@ -240,9 +248,9 @@ class TransmissionData: return self.config.data[CONF_HOST] @property - def signal_options_update(self): - """Option update signal per transmission entry.""" - return f"tm-options-{self.host}" + def signal_update(self): + """Update signal per transmission entry.""" + return f"{DATA_UPDATED}-{self.host}" def update(self): """Get the latest data from Transmission instance.""" @@ -260,7 +268,7 @@ class TransmissionData: except TransmissionError: self.available = False _LOGGER.error("Unable to connect to Transmission client %s", self.host) - dispatcher_send(self.hass, self.signal_options_update) + dispatcher_send(self.hass, self.signal_update) def init_torrent_list(self): """Initialize torrent lists.""" diff --git a/homeassistant/components/transmission/config_flow.py b/homeassistant/components/transmission/config_flow.py index d7b9efb15d8..193c152d7c1 100644 --- a/homeassistant/components/transmission/config_flow.py +++ b/homeassistant/components/transmission/config_flow.py @@ -16,9 +16,19 @@ from . import get_api from .const import DEFAULT_NAME, DEFAULT_PORT, DEFAULT_SCAN_INTERVAL, DOMAIN from .errors import AuthenticationError, CannotConnect, UnknownError +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_NAME, default=DEFAULT_NAME): str, + vol.Required(CONF_HOST): str, + vol.Optional(CONF_USERNAME): str, + vol.Optional(CONF_PASSWORD): str, + vol.Required(CONF_PORT, default=DEFAULT_PORT): int, + } +) + class TransmissionFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): - """Handle a UniFi config flow.""" + """Handle Tansmission config flow.""" VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL @@ -57,17 +67,7 @@ class TransmissionFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) return self.async_show_form( - step_id="user", - data_schema=vol.Schema( - { - vol.Required(CONF_NAME, default=DEFAULT_NAME): str, - vol.Required(CONF_HOST): str, - vol.Optional(CONF_USERNAME): str, - vol.Optional(CONF_PASSWORD): str, - vol.Required(CONF_PORT, default=DEFAULT_PORT): int, - } - ), - errors=errors, + step_id="user", data_schema=DATA_SCHEMA, errors=errors, ) async def async_step_import(self, import_config): diff --git a/homeassistant/components/transmission/const.py b/homeassistant/components/transmission/const.py index 5540f718ba1..9a9250dbed6 100644 --- a/homeassistant/components/transmission/const.py +++ b/homeassistant/components/transmission/const.py @@ -2,14 +2,14 @@ DOMAIN = "transmission" SENSOR_TYPES = { - "active_torrents": ["Active Torrents", None], + "active_torrents": ["Active Torrents", "Torrents"], "current_status": ["Status", None], "download_speed": ["Down Speed", "MB/s"], - "paused_torrents": ["Paused Torrents", None], - "total_torrents": ["Total Torrents", None], + "paused_torrents": ["Paused Torrents", "Torrents"], + "total_torrents": ["Total Torrents", "Torrents"], "upload_speed": ["Up Speed", "MB/s"], - "completed_torrents": ["Completed Torrents", None], - "started_torrents": ["Started Torrents", None], + "completed_torrents": ["Completed Torrents", "Torrents"], + "started_torrents": ["Started Torrents", "Torrents"], } SWITCH_TYPES = {"on_off": "Switch", "turtle_mode": "Turtle Mode"} diff --git a/homeassistant/components/transmission/sensor.py b/homeassistant/components/transmission/sensor.py index 489582de157..6bedc793ed9 100644 --- a/homeassistant/components/transmission/sensor.py +++ b/homeassistant/components/transmission/sensor.py @@ -8,7 +8,6 @@ from homeassistant.helpers.entity import Entity from .const import DOMAIN, SENSOR_TYPES, STATE_ATTR_TORRENT_INFO - _LOGGER = logging.getLogger(__name__) @@ -52,6 +51,7 @@ class TransmissionSensor(Entity): self._data = None self.client_name = client_name self.type = sensor_type + self.unsub_update = None @property def name(self): @@ -92,9 +92,9 @@ class TransmissionSensor(Entity): async def async_added_to_hass(self): """Handle entity which will be added.""" - async_dispatcher_connect( + self.unsub_update = async_dispatcher_connect( self.hass, - self._tm_client.api.signal_options_update, + self._tm_client.api.signal_update, self._schedule_immediate_update, ) @@ -102,6 +102,12 @@ class TransmissionSensor(Entity): def _schedule_immediate_update(self): self.async_schedule_update_ha_state(True) + async def will_remove_from_hass(self): + """Unsubscribe from update dispatcher.""" + if self.unsub_update: + self.unsub_update() + self.unsub_update = None + def update(self): """Get the latest data from Transmission and updates the state.""" self._data = self._tm_client.api.data diff --git a/homeassistant/components/transmission/services.yaml b/homeassistant/components/transmission/services.yaml index ab383584e83..de3314e20f6 100644 --- a/homeassistant/components/transmission/services.yaml +++ b/homeassistant/components/transmission/services.yaml @@ -1,6 +1,9 @@ add_torrent: description: Add a new torrent to download (URL, magnet link or Base64 encoded). fields: + name: + description: Instance name as entered during entry config + example: Transmission torrent: description: URL, magnet link or Base64 encoded file. example: http://releases.ubuntu.com/19.04/ubuntu-19.04-desktop-amd64.iso.torrent diff --git a/homeassistant/components/transmission/switch.py b/homeassistant/components/transmission/switch.py index 4b93b3f06e2..adf94c64fd6 100644 --- a/homeassistant/components/transmission/switch.py +++ b/homeassistant/components/transmission/switch.py @@ -40,6 +40,7 @@ class TransmissionSwitch(ToggleEntity): self._tm_client = tm_client self._state = STATE_OFF self._data = None + self.unsub_update = None @property def name(self): @@ -93,9 +94,9 @@ class TransmissionSwitch(ToggleEntity): async def async_added_to_hass(self): """Handle entity which will be added.""" - async_dispatcher_connect( + self.unsub_update = async_dispatcher_connect( self.hass, - self._tm_client.api.signal_options_update, + self._tm_client.api.signal_update, self._schedule_immediate_update, ) @@ -103,6 +104,12 @@ class TransmissionSwitch(ToggleEntity): def _schedule_immediate_update(self): self.async_schedule_update_ha_state(True) + async def will_remove_from_hass(self): + """Unsubscribe from update dispatcher.""" + if self.unsub_update: + self.unsub_update() + self.unsub_update = None + def update(self): """Get the latest data from Transmission and updates the state.""" active = None diff --git a/homeassistant/components/transport_nsw/sensor.py b/homeassistant/components/transport_nsw/sensor.py index 79df41ac489..7c6990de085 100644 --- a/homeassistant/components/transport_nsw/sensor.py +++ b/homeassistant/components/transport_nsw/sensor.py @@ -2,13 +2,13 @@ from datetime import timedelta import logging -import voluptuous as vol from TransportNSW import TransportNSW +import voluptuous as vol +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ATTR_ATTRIBUTION, ATTR_MODE, CONF_API_KEY, CONF_NAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ATTR_MODE, CONF_NAME, CONF_API_KEY, ATTR_ATTRIBUTION _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/travisci/sensor.py b/homeassistant/components/travisci/sensor.py index b86b62fc1e9..ba698c2b64d 100644 --- a/homeassistant/components/travisci/sensor.py +++ b/homeassistant/components/travisci/sensor.py @@ -1,17 +1,19 @@ """This component provides HA sensor support for Travis CI framework.""" -import logging from datetime import timedelta +import logging +from travispy import TravisPy +from travispy.errors import TravisError import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_API_KEY, - CONF_SCAN_INTERVAL, CONF_MONITORED_CONDITIONS, + CONF_SCAN_INTERVAL, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -53,8 +55,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Travis CI sensor.""" - from travispy import TravisPy - from travispy.errors import TravisError token = config.get(CONF_API_KEY) repositories = config.get(CONF_REPOSITORY) diff --git a/homeassistant/components/trend/manifest.json b/homeassistant/components/trend/manifest.json index cf9333be7c3..8842b03b594 100644 --- a/homeassistant/components/trend/manifest.json +++ b/homeassistant/components/trend/manifest.json @@ -2,9 +2,7 @@ "domain": "trend", "name": "Trend", "documentation": "https://www.home-assistant.io/integrations/trend", - "requirements": [ - "numpy==1.17.3" - ], + "requirements": ["numpy==1.17.4"], "dependencies": [], "codeowners": [] -} \ No newline at end of file +} diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index d17f64a3a3a..8ae06771618 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -1,4 +1,4 @@ -"""Provide functionality to TTS.""" +"""Provide functionality for TTS.""" import asyncio import ctypes import functools as ft @@ -353,7 +353,7 @@ class SpeechManager: raise HomeAssistantError(f"No TTS from {engine} for '{message}'") # Create file infos - filename = (f"{key}.{extension}").lower() + filename = f"{key}.{extension}".lower() data = self.write_tags(filename, data, provider, message, language, options) @@ -438,7 +438,7 @@ class SpeechManager: await self.async_file_to_mem(key) content, _ = mimetypes.guess_type(filename) - return (content, self.mem_cache[key][MEM_CACHE_VOICE]) + return content, self.mem_cache[key][MEM_CACHE_VOICE] @staticmethod def write_tags(filename, data, provider, message, language, options): diff --git a/homeassistant/components/tts/manifest.json b/homeassistant/components/tts/manifest.json index ca2059a4d19..b57d5c36112 100644 --- a/homeassistant/components/tts/manifest.json +++ b/homeassistant/components/tts/manifest.json @@ -2,13 +2,8 @@ "domain": "tts", "name": "Tts", "documentation": "https://www.home-assistant.io/integrations/tts", - "requirements": [ - "mutagen==1.42.0" - ], - "dependencies": [ - "http" - ], - "codeowners": [ - "@robbiet480" - ] + "requirements": ["mutagen==1.43.0"], + "dependencies": ["http"], + "after_dependencies": ["media_player"], + "codeowners": ["@robbiet480"] } diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index 4d6506d950d..dffd66265a6 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -1,13 +1,15 @@ """Support for Tuya Smart devices.""" from datetime import timedelta import logging + +from tuyaha import TuyaApi import voluptuous as vol +from homeassistant.const import CONF_PASSWORD, CONF_PLATFORM, CONF_USERNAME from homeassistant.core import callback -import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_PLATFORM from homeassistant.helpers import discovery -from homeassistant.helpers.dispatcher import dispatcher_send, async_dispatcher_connect +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import track_time_interval @@ -50,7 +52,6 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): """Set up Tuya Component.""" - from tuyaha import TuyaApi tuya = TuyaApi() username = config[DOMAIN][CONF_USERNAME] diff --git a/homeassistant/components/tuya/climate.py b/homeassistant/components/tuya/climate.py index 02e8a1bf821..eb0ef5eca2f 100644 --- a/homeassistant/components/tuya/climate.py +++ b/homeassistant/components/tuya/climate.py @@ -1,15 +1,17 @@ """Support for the Tuya climate devices.""" from homeassistant.components.climate import ENTITY_ID_FORMAT, ClimateDevice from homeassistant.components.climate.const import ( + FAN_HIGH, + FAN_LOW, + FAN_MEDIUM, HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_FAN_ONLY, HVAC_MODE_HEAT, + HVAC_MODE_OFF, SUPPORT_FAN_MODE, SUPPORT_TARGET_TEMPERATURE, - HVAC_MODE_OFF, ) -from homeassistant.components.fan import SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM from homeassistant.const import ( ATTR_TEMPERATURE, PRECISION_WHOLE, @@ -30,7 +32,7 @@ HA_STATE_TO_TUYA = { TUYA_STATE_TO_HA = {value: key for key, value in HA_STATE_TO_TUYA.items()} -FAN_MODES = {SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH} +FAN_MODES = {FAN_LOW, FAN_MEDIUM, FAN_HIGH} def setup_platform(hass, config, add_entities, discovery_info=None): diff --git a/homeassistant/components/tuya/manifest.json b/homeassistant/components/tuya/manifest.json index cf16d587e87..6479a26694d 100644 --- a/homeassistant/components/tuya/manifest.json +++ b/homeassistant/components/tuya/manifest.json @@ -3,7 +3,7 @@ "name": "Tuya", "documentation": "https://www.home-assistant.io/integrations/tuya", "requirements": [ - "tuyaha==0.0.4" + "tuyaha==0.0.5" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/twentemilieu/.translations/tr.json b/homeassistant/components/twentemilieu/.translations/tr.json new file mode 100644 index 00000000000..ebe13a37003 --- /dev/null +++ b/homeassistant/components/twentemilieu/.translations/tr.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "address_exists": "Adres zaten kurulmu\u015f." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twilio/.translations/da.json b/homeassistant/components/twilio/.translations/da.json index 3c1ab7c01b5..d5f40d56446 100644 --- a/homeassistant/components/twilio/.translations/da.json +++ b/homeassistant/components/twilio/.translations/da.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "not_internet_accessible": "Dit Home Assistant system skal v\u00e6re tilg\u00e6ngeligt fra internettet for at modtage Twilio meddelelser.", + "not_internet_accessible": "Din Home Assistant-instans skal v\u00e6re tilg\u00e6ngelig fra internettet for at modtage Twilio-meddelelser.", "one_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning." }, "create_entry": { - "default": "For at sende begivenheder til Home Assistant skal du konfigurere [Webhooks med Twilio]({twilio_url}).\n\n Udfyld f\u00f8lgende oplysninger: \n\n - URL: `{webhook_url}`\n - Metode: POST\n - Indholdstype: application/x-www-form-urlencoded\n\n Se [dokumentationen]({docs_url}) om hvordan du konfigurerer automatiseringer til at h\u00e5ndtere indg\u00e5ende data." + "default": "For at sende h\u00e6ndelser til Home Assistant skal du konfigurere [Webhooks med Twilio]({twilio_url}).\n\n Udfyld f\u00f8lgende oplysninger: \n\n - Webadresse: `{webhook_url}`\n - Metode: POST\n - Indholdstype: application/x-www-form-urlencoded\n\nSe [dokumentationen]({docs_url}) om hvordan du konfigurerer automatiseringer til at h\u00e5ndtere indg\u00e5ende data." }, "step": { "user": { diff --git a/homeassistant/components/twilio/.translations/ko.json b/homeassistant/components/twilio/.translations/ko.json index 4e4c80801d4..b8e88820590 100644 --- a/homeassistant/components/twilio/.translations/ko.json +++ b/homeassistant/components/twilio/.translations/ko.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "Twilio \ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "Twilio \ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "Twilio Webhook \uc124\uc815" } }, diff --git a/homeassistant/components/twilio_call/notify.py b/homeassistant/components/twilio_call/notify.py index 82705091814..83ca081b26e 100644 --- a/homeassistant/components/twilio_call/notify.py +++ b/homeassistant/components/twilio_call/notify.py @@ -2,6 +2,7 @@ import logging import urllib +from twilio.base.exceptions import TwilioRestException import voluptuous as vol from homeassistant.components.notify import ( @@ -42,7 +43,6 @@ class TwilioCallNotificationService(BaseNotificationService): def send_message(self, message="", **kwargs): """Call to specified target users.""" - from twilio.base.exceptions import TwilioRestException targets = kwargs.get(ATTR_TARGET) diff --git a/homeassistant/components/twitter/notify.py b/homeassistant/components/twitter/notify.py index 39faf987ae0..768e1ee7316 100644 --- a/homeassistant/components/twitter/notify.py +++ b/homeassistant/components/twitter/notify.py @@ -9,15 +9,14 @@ import os from TwitterAPI import TwitterAPI import voluptuous as vol -from homeassistant.const import CONF_ACCESS_TOKEN, CONF_USERNAME -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import async_track_point_in_time - from homeassistant.components.notify import ( ATTR_DATA, PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_USERNAME +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import async_track_point_in_time _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/ubee/device_tracker.py b/homeassistant/components/ubee/device_tracker.py index 86b2e3c09af..6fe7e90f4c7 100644 --- a/homeassistant/components/ubee/device_tracker.py +++ b/homeassistant/components/ubee/device_tracker.py @@ -1,6 +1,8 @@ """Support for Ubee router.""" import logging + +from pyubee import Ubee import voluptuous as vol from homeassistant.components.device_tracker import ( @@ -36,8 +38,6 @@ def get_scanner(hass, config): password = info[CONF_PASSWORD] model = info[CONF_MODEL] - from pyubee import Ubee - ubee = Ubee(host, username, password, model) if not ubee.login(): _LOGGER.error("Login failed") diff --git a/homeassistant/components/ue_smart_radio/media_player.py b/homeassistant/components/ue_smart_radio/media_player.py index ae54eb76d72..d25c52608e1 100644 --- a/homeassistant/components/ue_smart_radio/media_player.py +++ b/homeassistant/components/ue_smart_radio/media_player.py @@ -5,7 +5,7 @@ import logging import requests import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, diff --git a/homeassistant/components/uk_transport/sensor.py b/homeassistant/components/uk_transport/sensor.py index eb325d32212..e3c5440c450 100644 --- a/homeassistant/components/uk_transport/sensor.py +++ b/homeassistant/components/uk_transport/sensor.py @@ -3,19 +3,19 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.uk_transport/ """ +from datetime import datetime, timedelta import logging import re -from datetime import datetime, timedelta import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt_util from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_MODE +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 _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/unifi/.translations/da.json b/homeassistant/components/unifi/.translations/da.json index 0d0315e49c7..46a94cc4047 100644 --- a/homeassistant/components/unifi/.translations/da.json +++ b/homeassistant/components/unifi/.translations/da.json @@ -33,9 +33,15 @@ "track_wired_clients": "Inkluder kablede netv\u00e6rksklienter" } }, + "init": { + "data": { + "one": "EN", + "other": "ANDEN" + } + }, "statistics_sensors": { "data": { - "allow_bandwidth_sensors": "Opret b\u00e5ndbredde sensorer for netv\u00e6rksklienter" + "allow_bandwidth_sensors": "Opret b\u00e5ndbredde-forbrugssensorer for netv\u00e6rksklienter" } } } diff --git a/homeassistant/components/unifi/.translations/es.json b/homeassistant/components/unifi/.translations/es.json index 1db6712142d..677899c0958 100644 --- a/homeassistant/components/unifi/.translations/es.json +++ b/homeassistant/components/unifi/.translations/es.json @@ -35,8 +35,8 @@ }, "init": { "data": { - "one": "uno", - "other": "otro" + "one": "vac\u00edo", + "other": "vac\u00edo" } }, "statistics_sensors": { diff --git a/homeassistant/components/unifi/.translations/ru.json b/homeassistant/components/unifi/.translations/ru.json index dbb6efd8343..3a67d483c0c 100644 --- a/homeassistant/components/unifi/.translations/ru.json +++ b/homeassistant/components/unifi/.translations/ru.json @@ -5,7 +5,7 @@ "user_privilege": "\u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u043e\u043c." }, "error": { - "faulty_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", + "faulty_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", "service_unavailable": "\u0421\u043b\u0443\u0436\u0431\u0430 \u043d\u0435 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430." }, "step": { @@ -33,6 +33,14 @@ "track_wired_clients": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 \u043f\u0440\u043e\u0432\u043e\u0434\u043d\u043e\u0439 \u0441\u0435\u0442\u0438" } }, + "init": { + "data": { + "few": "\u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e", + "many": "\u043c\u043d\u043e\u0433\u043e", + "one": "\u043e\u0434\u043d\u0438", + "other": "\u0434\u0440\u0443\u0433\u0438\u0435" + } + }, "statistics_sensors": { "data": { "allow_bandwidth_sensors": "\u0421\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c \u0434\u0430\u0442\u0447\u0438\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u043f\u043e\u043b\u043e\u0441\u044b \u043f\u0440\u043e\u043f\u0443\u0441\u043a\u0430\u043d\u0438\u044f \u0434\u043b\u044f \u0441\u0435\u0442\u0435\u0432\u044b\u0445 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432" diff --git a/homeassistant/components/unifi/__init__.py b/homeassistant/components/unifi/__init__.py index 4f3edf9ce79..65015b357a7 100644 --- a/homeassistant/components/unifi/__init__.py +++ b/homeassistant/components/unifi/__init__.py @@ -3,9 +3,8 @@ import voluptuous as vol from homeassistant.const import CONF_HOST from homeassistant.core import callback -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC - import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from .config_flow import get_controller_id_from_config_entry from .const import ( diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index 01b97a78366..52ecab08856 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -2,7 +2,6 @@ import voluptuous as vol from homeassistant import config_entries -from homeassistant.core import callback from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -10,6 +9,7 @@ from homeassistant.const import ( CONF_USERNAME, CONF_VERIFY_SSL, ) +from homeassistant.core import callback from .const import ( CONF_ALLOW_BANDWIDTH_SENSORS, @@ -21,10 +21,10 @@ from .const import ( CONF_TRACK_WIRED_CLIENTS, CONTROLLER_ID, DEFAULT_ALLOW_BANDWIDTH_SENSORS, + DEFAULT_DETECTION_TIME, DEFAULT_TRACK_CLIENTS, DEFAULT_TRACK_DEVICES, DEFAULT_TRACK_WIRED_CLIENTS, - DEFAULT_DETECTION_TIME, DOMAIN, LOGGER, ) diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index 3deb2e9040a..826491f6ba6 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -1,16 +1,14 @@ """UniFi Controller abstraction.""" -from datetime import timedelta - import asyncio +from datetime import timedelta import ssl -import async_timeout from aiohttp import CookieJar - import aiounifi +import async_timeout -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.const import CONF_HOST +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -22,19 +20,19 @@ from .const import ( CONF_DONT_TRACK_CLIENTS, CONF_DONT_TRACK_DEVICES, CONF_DONT_TRACK_WIRED_CLIENTS, + CONF_SITE_ID, + CONF_SSID_FILTER, CONF_TRACK_CLIENTS, CONF_TRACK_DEVICES, CONF_TRACK_WIRED_CLIENTS, - CONF_SITE_ID, - CONF_SSID_FILTER, CONTROLLER_ID, DEFAULT_ALLOW_BANDWIDTH_SENSORS, DEFAULT_BLOCK_CLIENTS, + DEFAULT_DETECTION_TIME, + DEFAULT_SSID_FILTER, DEFAULT_TRACK_CLIENTS, DEFAULT_TRACK_DEVICES, DEFAULT_TRACK_WIRED_CLIENTS, - DEFAULT_DETECTION_TIME, - DEFAULT_SSID_FILTER, DOMAIN, LOGGER, UNIFI_CONFIG, diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index b92211c4eae..8b45a0f227b 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -1,16 +1,16 @@ """Track devices using UniFi controllers.""" import logging +from pprint import pformat -from homeassistant.components.unifi.config_flow import get_controller_from_config_entry from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.components.device_tracker.const import SOURCE_TYPE_ROUTER +from homeassistant.components.unifi.config_flow import get_controller_from_config_entry from homeassistant.core import callback from homeassistant.helpers import entity_registry from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_registry import DISABLED_CONFIG_ENTRY - import homeassistant.util.dt as dt_util from .const import ATTR_MANUFACTURER @@ -76,6 +76,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Update the values of the controller.""" for entity in tracked.values(): + if entity.entity_registry_enabled_default == entity.enabled: + continue + disabled_by = None if not entity.entity_registry_enabled_default and entity.enabled: disabled_by = DISABLED_CONFIG_ENTRY @@ -125,6 +128,10 @@ class UniFiClientTracker(ScannerEntity): self.client = client self.controller = controller self.is_wired = self.client.mac not in controller.wireless_clients + self.wired_bug = None + + if self.is_wired != self.client.is_wired: + self.wired_bug = dt_util.utcnow() - self.controller.option_detection_time @property def entity_registry_enabled_default(self): @@ -153,27 +160,35 @@ class UniFiClientTracker(ScannerEntity): Make sure to update self.is_wired if client is wireless, there is an issue when clients go offline that they get marked as wired. """ - LOGGER.debug( - "Updating UniFi tracked client %s (%s)", self.entity_id, self.client.mac - ) await self.controller.request_update() if self.is_wired and self.client.mac in self.controller.wireless_clients: self.is_wired = False + LOGGER.debug( + "Updating UniFi tracked client %s\n%s", + self.entity_id, + pformat(self.client.raw), + ) + @property def is_connected(self): """Return true if the client is connected to the network. If is_wired and client.is_wired differ it means that the device is offline and UniFi bug shows device as wired. """ - if self.is_wired == self.client.is_wired and ( - ( - dt_util.utcnow() - - dt_util.utc_from_timestamp(float(self.client.last_seen)) + if self.is_wired != self.client.is_wired: + if not self.wired_bug: + self.wired_bug = dt_util.utcnow() + since_last_seen = dt_util.utcnow() - self.wired_bug + + else: + self.wired_bug = None + since_last_seen = dt_util.utcnow() - dt_util.utc_from_timestamp( + float(self.client.last_seen) ) - < self.controller.option_detection_time - ): + + if since_last_seen < self.controller.option_detection_time: return True return False @@ -228,10 +243,9 @@ class UniFiDeviceTracker(ScannerEntity): @property def entity_registry_enabled_default(self): """Return if the entity should be enabled when first added to the entity registry.""" - if not self.controller.option_track_devices: - return False - - return True + if self.controller.option_track_devices: + return True + return False async def async_added_to_hass(self): """Subscribe to device events.""" @@ -239,11 +253,14 @@ class UniFiDeviceTracker(ScannerEntity): async def async_update(self): """Synchronize state with controller.""" - LOGGER.debug( - "Updating UniFi tracked device %s (%s)", self.entity_id, self.device.mac - ) await self.controller.request_update() + LOGGER.debug( + "Updating UniFi tracked device %s\n%s", + self.entity_id, + pformat(self.device.raw), + ) + @property def is_connected(self): """Return true if the device is connected to the network.""" diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index e4f9b0df6c9..9145fd8e00f 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -40,6 +40,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Update the values of the controller.""" for entity in sensors.values(): + if entity.entity_registry_enabled_default == entity.enabled: + continue + disabled_by = None if not entity.entity_registry_enabled_default and entity.enabled: disabled_by = DISABLED_CONFIG_ENTRY diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index 82aa6f0384d..5b64f573ccd 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -1,8 +1,9 @@ """Support for devices connected to UniFi POE.""" import logging +from pprint import pformat -from homeassistant.components.unifi.config_flow import get_controller_from_config_entry from homeassistant.components.switch import SwitchDevice +from homeassistant.components.unifi.config_flow import get_controller_from_config_entry from homeassistant.core import callback from homeassistant.helpers import entity_registry from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC @@ -73,12 +74,13 @@ def update_items(controller, async_add_entities, switches, switches_off): block_client_id = f"block-{client_id}" if block_client_id in switches: - LOGGER.debug( - "Updating UniFi block switch %s (%s)", - switches[block_client_id].entity_id, - switches[block_client_id].client.mac, - ) - switches[block_client_id].async_schedule_update_ha_state() + if switches[block_client_id].enabled: + LOGGER.debug( + "Updating UniFi block switch %s (%s)", + switches[block_client_id].entity_id, + switches[block_client_id].client.mac, + ) + switches[block_client_id].async_schedule_update_ha_state() continue if client_id not in controller.api.clients_all: @@ -87,7 +89,6 @@ def update_items(controller, async_add_entities, switches, switches_off): client = controller.api.clients_all[client_id] switches[block_client_id] = UniFiBlockClientSwitch(client, controller) new_switches.append(switches[block_client_id]) - LOGGER.debug("New UniFi Block switch %s (%s)", client.hostname, client.mac) # control POE for client_id in controller.api.clients: @@ -95,12 +96,13 @@ def update_items(controller, async_add_entities, switches, switches_off): poe_client_id = f"poe-{client_id}" if poe_client_id in switches: - LOGGER.debug( - "Updating UniFi POE switch %s (%s)", - switches[poe_client_id].entity_id, - switches[poe_client_id].client.mac, - ) - switches[poe_client_id].async_schedule_update_ha_state() + if switches[poe_client_id].enabled: + LOGGER.debug( + "Updating UniFi POE switch %s (%s)", + switches[poe_client_id].entity_id, + switches[poe_client_id].client.mac, + ) + switches[poe_client_id].async_schedule_update_ha_state() continue client = controller.api.clients[client_id] @@ -138,7 +140,6 @@ def update_items(controller, async_add_entities, switches, switches_off): switches[poe_client_id] = UniFiPOEClientSwitch(client, controller) new_switches.append(switches[poe_client_id]) - LOGGER.debug("New UniFi POE switch %s (%s)", client.hostname, client.mac) if new_switches: async_add_entities(new_switches) @@ -179,6 +180,7 @@ class UniFiPOEClientSwitch(UniFiClient, SwitchDevice, RestoreEntity): async def async_added_to_hass(self): """Call when entity about to be added to Home Assistant.""" + LOGGER.debug("New UniFi POE switch %s (%s)", self.name, self.client.mac) state = await self.async_get_last_state() if state is None: @@ -193,6 +195,16 @@ class UniFiPOEClientSwitch(UniFiClient, SwitchDevice, RestoreEntity): if not self.client.sw_port: self.client.raw["sw_port"] = state.attributes["port"] + async def async_update(self): + """Log client information after update.""" + await super().async_update() + + LOGGER.debug( + "Updating UniFi POE controlled client %s\n%s", + self.entity_id, + pformat(self.client.raw), + ) + @property def unique_id(self): """Return a unique identifier for this switch.""" @@ -252,6 +264,10 @@ class UniFiPOEClientSwitch(UniFiClient, SwitchDevice, RestoreEntity): class UniFiBlockClientSwitch(UniFiClient, SwitchDevice): """Representation of a blockable client.""" + async def async_added_to_hass(self): + """Call when entity about to be added to Home Assistant.""" + LOGGER.debug("New UniFi Block switch %s (%s)", self.name, self.client.mac) + @property def unique_id(self): """Return a unique identifier for this switch.""" diff --git a/homeassistant/components/unifi_direct/device_tracker.py b/homeassistant/components/unifi_direct/device_tracker.py index a526cc926d3..558a9981171 100644 --- a/homeassistant/components/unifi_direct/device_tracker.py +++ b/homeassistant/components/unifi_direct/device_tracker.py @@ -1,16 +1,17 @@ """Support for Unifi AP direct access.""" -import logging import json +import logging +from pexpect import exceptions, pxssh import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner, ) -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -74,7 +75,6 @@ class UnifiDeviceScanner(DeviceScanner): def _connect(self): """Connect to the Unifi AP SSH server.""" - from pexpect import pxssh, exceptions self.ssh = pxssh.pxssh() try: @@ -98,7 +98,6 @@ class UnifiDeviceScanner(DeviceScanner): self.connected = False def _get_update(self): - from pexpect import pxssh, exceptions try: if not self.connected: diff --git a/homeassistant/components/universal/media_player.py b/homeassistant/components/universal/media_player.py index 09771b551ac..37d4cf138f2 100644 --- a/homeassistant/components/universal/media_player.py +++ b/homeassistant/components/universal/media_player.py @@ -4,7 +4,7 @@ import logging import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( ATTR_APP_ID, ATTR_APP_NAME, @@ -255,7 +255,10 @@ class UniversalMediaPlayer(MediaPlayerDevice): @property def volume_level(self): """Volume level of entity specified in attributes or active child.""" - return self._override_or_child_attr(ATTR_MEDIA_VOLUME_LEVEL) + try: + return float(self._override_or_child_attr(ATTR_MEDIA_VOLUME_LEVEL)) + except (TypeError, ValueError): + return None @property def is_volume_muted(self): diff --git a/homeassistant/components/upcloud/__init__.py b/homeassistant/components/upcloud/__init__.py index c77b0fe3cdd..cd2cf5d02c0 100644 --- a/homeassistant/components/upcloud/__init__.py +++ b/homeassistant/components/upcloud/__init__.py @@ -18,6 +18,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import track_time_interval +from homeassistant.util import dt _LOGGER = logging.getLogger(__name__) @@ -82,6 +83,7 @@ def setup(hass, config): dispatcher_send(hass, SIGNAL_UPDATE_UPCLOUD) # Call the UpCloud API to refresh data + upcloud_update(dt.utcnow()) track_time_interval(hass, upcloud_update, scan_interval) return True @@ -108,6 +110,7 @@ class UpCloudServerEntity(Entity): self._upcloud = upcloud self.uuid = uuid self.data = None + self._unsub_handlers = [] @property def unique_id(self) -> str: @@ -124,10 +127,18 @@ class UpCloudServerEntity(Entity): async def async_added_to_hass(self): """Register callbacks.""" - async_dispatcher_connect( - self.hass, SIGNAL_UPDATE_UPCLOUD, self._update_callback + self._unsub_handlers.append( + async_dispatcher_connect( + self.hass, SIGNAL_UPDATE_UPCLOUD, self._update_callback + ) ) + async def async_will_remove_from_hass(self) -> None: + """Invoke unsubscription handlers.""" + for unsub in self._unsub_handlers: + unsub() + self._unsub_handlers.clear() + @callback def _update_callback(self): """Call update method.""" diff --git a/homeassistant/components/upcloud/manifest.json b/homeassistant/components/upcloud/manifest.json index 62ce608a911..0499ce1e9ad 100644 --- a/homeassistant/components/upcloud/manifest.json +++ b/homeassistant/components/upcloud/manifest.json @@ -1,9 +1,9 @@ { "domain": "upcloud", - "name": "Upcloud", + "name": "UpCloud", "documentation": "https://www.home-assistant.io/integrations/upcloud", "requirements": [ - "upcloud-api==0.4.3" + "upcloud-api==0.4.5" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/upcloud/switch.py b/homeassistant/components/upcloud/switch.py index 66f3d9f42b1..5cb1d86671e 100644 --- a/homeassistant/components/upcloud/switch.py +++ b/homeassistant/components/upcloud/switch.py @@ -6,8 +6,9 @@ import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import STATE_OFF import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import dispatcher_send -from . import CONF_SERVERS, DATA_UPCLOUD, UpCloudServerEntity +from . import CONF_SERVERS, DATA_UPCLOUD, SIGNAL_UPDATE_UPCLOUD, UpCloudServerEntity _LOGGER = logging.getLogger(__name__) @@ -34,6 +35,7 @@ class UpCloudSwitch(UpCloudServerEntity, SwitchDevice): """Start the server.""" if self.state == STATE_OFF: self.data.start() + dispatcher_send(self.hass, SIGNAL_UPDATE_UPCLOUD) def turn_off(self, **kwargs): """Stop the server.""" diff --git a/homeassistant/components/updater/__init__.py b/homeassistant/components/updater/__init__.py index 22c11d0c38e..1d4e441f12e 100644 --- a/homeassistant/components/updater/__init__.py +++ b/homeassistant/components/updater/__init__.py @@ -1,8 +1,6 @@ """Support to check for available updates.""" import asyncio from datetime import timedelta - -# pylint: disable=import-error,no-name-in-module from distutils.version import StrictVersion import json import logging @@ -10,15 +8,14 @@ import uuid import aiohttp import async_timeout -from distro import linux_distribution +from distro import linux_distribution # pylint: disable=import-error import voluptuous as vol from homeassistant.const import __version__ as current_version -from homeassistant.helpers import event +from homeassistant.helpers import discovery, event from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers import discovery -from homeassistant.helpers.dispatcher import async_dispatcher_send import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_send import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/updater/binary_sensor.py b/homeassistant/components/updater/binary_sensor.py index cae3ae32e3c..3e026a87d4d 100644 --- a/homeassistant/components/updater/binary_sensor.py +++ b/homeassistant/components/updater/binary_sensor.py @@ -1,7 +1,7 @@ """Support for Home Assistant Updater binary sensors.""" -from homeassistant.core import callback from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import ATTR_NEWEST_VERSION, ATTR_RELEASE_NOTES, DISPATCHER_REMOTE_UPDATE, Updater diff --git a/homeassistant/components/upnp/.translations/da.json b/homeassistant/components/upnp/.translations/da.json index 1d0097c2f1f..c41741b8635 100644 --- a/homeassistant/components/upnp/.translations/da.json +++ b/homeassistant/components/upnp/.translations/da.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "UPnP/IGD er allerede konfigureret", "incomplete_device": "Ignorerer ufuldst\u00e6ndig UPnP-enhed", - "no_devices_discovered": "Ingen UPnP/IGD enheder fundet.", + "no_devices_discovered": "Ingen UPnP/IGD-enheder fundet.", "no_devices_found": "Ingen UPnP/IGD enheder kunne findes p\u00e5 netv\u00e6rket.", "no_sensors_or_port_mapping": "Aktiv\u00e9r enten sensorer eller porttilknytning", "single_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning af UPnP/IGD." @@ -23,7 +23,7 @@ "user": { "data": { "enable_port_mapping": "Aktiv\u00e9r porttilknytning til Home Assistent", - "enable_sensors": "Tilf\u00f8j trafik sensorer", + "enable_sensors": "Tilf\u00f8j trafiksensorer", "igd": "UPnP/IGD" }, "title": "Konfigurationsindstillinger for UPnP/IGD" diff --git a/homeassistant/components/upnp/.translations/ko.json b/homeassistant/components/upnp/.translations/ko.json index d846a5e38ce..bd6aaeef4e2 100644 --- a/homeassistant/components/upnp/.translations/ko.json +++ b/homeassistant/components/upnp/.translations/ko.json @@ -10,7 +10,7 @@ }, "step": { "confirm": { - "description": "UPnP/IGD \ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "UPnP/IGD \ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "UPnP/IGD" }, "init": { diff --git a/homeassistant/components/upnp/.translations/ru.json b/homeassistant/components/upnp/.translations/ru.json index 9599832799f..6dce1b3d76c 100644 --- a/homeassistant/components/upnp/.translations/ru.json +++ b/homeassistant/components/upnp/.translations/ru.json @@ -8,6 +8,12 @@ "no_sensors_or_port_mapping": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u0438 \u0438\u043b\u0438 \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u043f\u043e\u0440\u0442\u043e\u0432.", "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, + "error": { + "few": "\u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e", + "many": "\u043c\u043d\u043e\u0433\u043e", + "one": "\u043e\u0434\u0438\u043d", + "other": "\u0434\u0440\u0443\u0433\u0438\u0435" + }, "step": { "confirm": { "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c UPnP / IGD?", diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index bbb49ebd1d4..9a7e06738db 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -7,11 +7,12 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers import device_registry as dr -from homeassistant.helpers import dispatcher -from homeassistant.helpers.typing import ConfigType -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.helpers import ( + config_validation as cv, + device_registry as dr, + dispatcher, +) +from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.util import get_local_ip from .const import ( @@ -20,10 +21,10 @@ from .const import ( CONF_HASS, CONF_LOCAL_IP, CONF_PORTS, + DOMAIN, + LOGGER as _LOGGER, SIGNAL_REMOVE_SENSOR, ) -from .const import DOMAIN -from .const import LOGGER as _LOGGER from .device import Device NOTIFICATION_ID = "upnp_notification" diff --git a/homeassistant/components/upnp/config_flow.py b/homeassistant/components/upnp/config_flow.py index cdcce76dbc3..1601595b6a9 100644 --- a/homeassistant/components/upnp/config_flow.py +++ b/homeassistant/components/upnp/config_flow.py @@ -1,11 +1,10 @@ """Config flow for UPNP.""" -from homeassistant.helpers import config_entry_flow from homeassistant import config_entries +from homeassistant.helpers import config_entry_flow from .const import DOMAIN from .device import Device - config_entry_flow.register_discovery_flow( DOMAIN, "UPnP/IGD", Device.async_discover, config_entries.CONN_CLASS_LOCAL_POLL ) diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py index de3c93a82ed..1f34e63bcdf 100644 --- a/homeassistant/components/upnp/device.py +++ b/homeassistant/components/upnp/device.py @@ -3,26 +3,27 @@ import asyncio from ipaddress import IPv4Address import aiohttp +from async_upnp_client import UpnpError, UpnpFactory +from async_upnp_client.aiohttp import AiohttpSessionRequester from async_upnp_client.profiles.igd import IgdDevice from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import HomeAssistantType -from .const import LOGGER as _LOGGER -from .const import DOMAIN, CONF_LOCAL_IP +from .const import CONF_LOCAL_IP, DOMAIN, LOGGER as _LOGGER class Device: """Hass representation of an UPnP/IGD.""" def __init__(self, igd_device): - """Initializer.""" + """Initialize UPnP/IGD device.""" self._igd_device = igd_device self._mapped_ports = [] @classmethod async def async_discover(cls, hass: HomeAssistantType): - """Discovery UPNP/IGD devices.""" + """Discover UPnP/IGD devices.""" _LOGGER.debug("Discovering UPnP/IGD devices") local_ip = None if DOMAIN in hass.data and "config" in hass.data[DOMAIN]: @@ -48,14 +49,10 @@ class Device: async def async_create_device(cls, hass: HomeAssistantType, ssdp_description: str): """Create UPnP/IGD device.""" # build async_upnp_client requester - from async_upnp_client.aiohttp import AiohttpSessionRequester - session = async_get_clientsession(hass) requester = AiohttpSessionRequester(session, True) # create async_upnp_client device - from async_upnp_client import UpnpFactory - factory = UpnpFactory(requester, disable_state_variable_validation=True) upnp_device = await factory.async_create_device(ssdp_description) @@ -99,8 +96,6 @@ class Device: async def _async_add_port_mapping(self, external_port, local_ip, internal_port): """Add a port mapping.""" # create port mapping - from async_upnp_client import UpnpError - _LOGGER.info( "Creating port mapping %s:%s:%s (TCP)", external_port, @@ -135,8 +130,6 @@ class Device: async def _async_delete_port_mapping(self, external_port): """Remove a port mapping.""" - from async_upnp_client import UpnpError - _LOGGER.info("Deleting port mapping %s (TCP)", external_port) try: await self._igd_device.async_delete_port_mapping( @@ -157,7 +150,6 @@ class Device: async def async_get_total_packets_received(self): """Get total packets received.""" - # pylint: disable=invalid-name return await self._igd_device.async_get_total_packets_received() async def async_get_total_packets_sent(self): diff --git a/homeassistant/components/upnp/sensor.py b/homeassistant/components/upnp/sensor.py index 4c85e904b1d..06e4a86401f 100644 --- a/homeassistant/components/upnp/sensor.py +++ b/homeassistant/components/upnp/sensor.py @@ -154,7 +154,7 @@ class PerSecondUPnPIGDSensor(UpnpSensor): """Abstract representation of a X Sent/Received per second sensor.""" def __init__(self, device, direction): - """Initializer.""" + """Initialize sensor.""" super().__init__(device) self._direction = direction @@ -189,7 +189,7 @@ class PerSecondUPnPIGDSensor(UpnpSensor): @property def unit_of_measurement(self) -> str: """Return the unit of measurement of this entity, if any.""" - return f"{self.unit}/sec" + return f"{self.unit}/s" def _is_overflowed(self, new_value) -> bool: """Check if value has overflowed.""" @@ -222,7 +222,7 @@ class KBytePerSecondUPnPIGDSensor(PerSecondUPnPIGDSensor): @property def unit(self) -> str: """Get unit we are measuring in.""" - return "kbyte" + return "kB" async def _async_fetch_value(self) -> float: """Fetch value from device.""" diff --git a/homeassistant/components/uptimerobot/binary_sensor.py b/homeassistant/components/uptimerobot/binary_sensor.py index 2075d930494..401da496d2f 100644 --- a/homeassistant/components/uptimerobot/binary_sensor.py +++ b/homeassistant/components/uptimerobot/binary_sensor.py @@ -1,6 +1,7 @@ """A platform that to monitor Uptime Robot monitors.""" import logging +from pyuptimerobot import UptimeRobot import voluptuous as vol from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice @@ -18,7 +19,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_API_KEY): cv.string} def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Uptime Robot binary_sensors.""" - from pyuptimerobot import UptimeRobot up_robot = UptimeRobot() api_key = config.get(CONF_API_KEY) diff --git a/homeassistant/components/uscis/sensor.py b/homeassistant/components/uscis/sensor.py index 3f5175ad09d..12e84a9dbf8 100644 --- a/homeassistant/components/uscis/sensor.py +++ b/homeassistant/components/uscis/sensor.py @@ -1,16 +1,15 @@ """Support for USCIS Case Status.""" -import logging from datetime import timedelta +import logging import uscisstatus import voluptuous as vol +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_FRIENDLY_NAME +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.helpers import config_validation as cv -from homeassistant.const import CONF_FRIENDLY_NAME - _LOGGER = logging.getLogger(__name__) @@ -31,7 +30,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if uscis.valid_case_id: add_entities([uscis]) else: - _LOGGER.error("Setup USCIS Sensor Fail" " check if your Case ID is Valid") + _LOGGER.error("Setup USCIS Sensor Fail check if your Case ID is Valid") class UscisSensor(Entity): diff --git a/homeassistant/components/usgs_earthquakes_feed/geo_location.py b/homeassistant/components/usgs_earthquakes_feed/geo_location.py index 7890243c1e0..37934db3052 100644 --- a/homeassistant/components/usgs_earthquakes_feed/geo_location.py +++ b/homeassistant/components/usgs_earthquakes_feed/geo_location.py @@ -3,6 +3,9 @@ from datetime import timedelta import logging from typing import Optional +from geojson_client.usgs_earthquake_hazards_program_feed import ( + UsgsEarthquakeHazardsProgramFeedManager, +) import voluptuous as vol from homeassistant.components.geo_location import PLATFORM_SCHEMA, GeolocationEvent @@ -122,9 +125,6 @@ class UsgsEarthquakesFeedEntityManager: minimum_magnitude, ): """Initialize the Feed Entity Manager.""" - from geojson_client.usgs_earthquake_hazards_program_feed import ( - UsgsEarthquakeHazardsProgramFeedManager, - ) self._hass = hass self._feed_manager = UsgsEarthquakeHazardsProgramFeedManager( diff --git a/homeassistant/components/utility_meter/__init__.py b/homeassistant/components/utility_meter/__init__.py index 17eacc326d3..ef9d9b1ddce 100644 --- a/homeassistant/components/utility_meter/__init__.py +++ b/homeassistant/components/utility_meter/__init__.py @@ -1,34 +1,34 @@ """Support for tracking consumption over given periods of time.""" -import logging from datetime import timedelta +import logging import voluptuous as vol +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import CONF_NAME -import homeassistant.helpers.config_validation as cv from homeassistant.helpers import discovery -from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN + from .const import ( - DOMAIN, - SIGNAL_RESET_METER, - METER_TYPES, - CONF_METER_TYPE, - CONF_METER_OFFSET, - CONF_METER_NET_CONSUMPTION, - CONF_SOURCE_SENSOR, - CONF_TARIFF_ENTITY, - CONF_TARIFF, - CONF_TARIFFS, - CONF_METER, - DATA_UTILITY, - SERVICE_RESET, - SERVICE_SELECT_TARIFF, - SERVICE_SELECT_NEXT_TARIFF, ATTR_TARIFF, + CONF_METER, + CONF_METER_NET_CONSUMPTION, + CONF_METER_OFFSET, + CONF_METER_TYPE, + CONF_SOURCE_SENSOR, + CONF_TARIFF, + CONF_TARIFF_ENTITY, + CONF_TARIFFS, + DATA_UTILITY, + DOMAIN, + METER_TYPES, + SERVICE_RESET, + SERVICE_SELECT_NEXT_TARIFF, + SERVICE_SELECT_TARIFF, + SIGNAL_RESET_METER, ) _LOGGER = logging.getLogger(__name__) @@ -39,11 +39,8 @@ ATTR_TARIFFS = "tariffs" DEFAULT_OFFSET = timedelta(hours=0) -SERVICE_SELECT_TARIFF_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_TARIFF): cv.string} -) -METER_CONFIG_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( +METER_CONFIG_SCHEMA = vol.Schema( { vol.Required(CONF_SOURCE_SENSOR): cv.entity_id, vol.Optional(CONF_NAME): cv.string, @@ -110,16 +107,16 @@ async def async_setup(hass, config): register_services = True if register_services: + component.async_register_entity_service(SERVICE_RESET, {}, "async_reset_meters") + component.async_register_entity_service( - SERVICE_RESET, ENTITY_SERVICE_SCHEMA, "async_reset_meters" + SERVICE_SELECT_TARIFF, + {vol.Required(ATTR_TARIFF): cv.string}, + "async_select_tariff", ) component.async_register_entity_service( - SERVICE_SELECT_TARIFF, SERVICE_SELECT_TARIFF_SCHEMA, "async_select_tariff" - ) - - component.async_register_entity_service( - SERVICE_SELECT_NEXT_TARIFF, ENTITY_SERVICE_SCHEMA, "async_next_tariff" + SERVICE_SELECT_NEXT_TARIFF, {}, "async_next_tariff" ) return True diff --git a/homeassistant/components/utility_meter/const.py b/homeassistant/components/utility_meter/const.py index 87721dfbf81..23d39204f9c 100644 --- a/homeassistant/components/utility_meter/const.py +++ b/homeassistant/components/utility_meter/const.py @@ -5,9 +5,10 @@ HOURLY = "hourly" DAILY = "daily" WEEKLY = "weekly" MONTHLY = "monthly" +QUARTERLY = "quarterly" YEARLY = "yearly" -METER_TYPES = [HOURLY, DAILY, WEEKLY, MONTHLY, YEARLY] +METER_TYPES = [HOURLY, DAILY, WEEKLY, MONTHLY, QUARTERLY, YEARLY] DATA_UTILITY = "utility_meter_data" diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index 1ad4300b28b..3dab92b89f8 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -1,38 +1,40 @@ """Utility meter from sensors providing raw data.""" -import logging from datetime import date, timedelta from decimal import Decimal, DecimalException +import logging -import homeassistant.util.dt as dt_util from homeassistant.const import ( - CONF_NAME, ATTR_UNIT_OF_MEASUREMENT, + CONF_NAME, EVENT_HOMEASSISTANT_START, - STATE_UNKNOWN, STATE_UNAVAILABLE, + STATE_UNKNOWN, ) from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.event import ( async_track_state_change, async_track_time_change, ) -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.restore_state import RestoreEntity +import homeassistant.util.dt as dt_util + from .const import ( - DATA_UTILITY, - SIGNAL_RESET_METER, - HOURLY, - DAILY, - WEEKLY, - MONTHLY, - YEARLY, - CONF_SOURCE_SENSOR, - CONF_METER_TYPE, - CONF_METER_OFFSET, + CONF_METER, CONF_METER_NET_CONSUMPTION, + CONF_METER_OFFSET, + CONF_METER_TYPE, + CONF_SOURCE_SENSOR, CONF_TARIFF, CONF_TARIFF_ENTITY, - CONF_METER, + DAILY, + DATA_UTILITY, + HOURLY, + MONTHLY, + QUARTERLY, + SIGNAL_RESET_METER, + WEEKLY, + YEARLY, ) _LOGGER = logging.getLogger(__name__) @@ -184,6 +186,12 @@ class UtilityMeterSensor(RestoreEntity): and now != date(now.year, now.month, 1) + self._period_offset ): return + if ( + self._period == QUARTERLY + and now + != date(now.year, (((now.month - 1) // 3) * 3 + 1), 1) + self._period_offset + ): + return if self._period == YEARLY and now != date(now.year, 1, 1) + self._period_offset: return await self.async_reset_meter(self._tariff_entity) @@ -209,7 +217,7 @@ class UtilityMeterSensor(RestoreEntity): minute=self._period_offset.seconds // 60, second=self._period_offset.seconds % 60, ) - elif self._period in [DAILY, WEEKLY, MONTHLY, YEARLY]: + elif self._period in [DAILY, WEEKLY, MONTHLY, QUARTERLY, YEARLY]: async_track_time_change( self.hass, self._async_reset_meter, diff --git a/homeassistant/components/uvc/camera.py b/homeassistant/components/uvc/camera.py index 20aae3849ab..b9a6262cd4f 100644 --- a/homeassistant/components/uvc/camera.py +++ b/homeassistant/components/uvc/camera.py @@ -3,12 +3,13 @@ import logging import socket import requests +from uvcclient import camera as uvc_camera, nvr import voluptuous as vol +from homeassistant.components.camera import PLATFORM_SCHEMA, Camera from homeassistant.const import CONF_PORT, CONF_SSL -from homeassistant.components.camera import Camera, PLATFORM_SCHEMA -import homeassistant.helpers.config_validation as cv from homeassistant.exceptions import PlatformNotReady +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -39,8 +40,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): port = config[CONF_PORT] ssl = config[CONF_SSL] - from uvcclient import nvr - try: # Exceptions may be raised in all method calls to the nvr library. nvrconn = nvr.UVCRemote(addr, port, key, ssl=ssl) @@ -76,10 +75,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class UnifiVideoCamera(Camera): """A Ubiquiti Unifi Video Camera.""" - def __init__(self, nvr, uuid, name, password): + def __init__(self, camera, uuid, name, password): """Initialize an Unifi camera.""" super().__init__() - self._nvr = nvr + self._nvr = camera self._uuid = uuid self._name = name self._password = password @@ -118,7 +117,6 @@ class UnifiVideoCamera(Camera): def _login(self): """Login to the camera.""" - from uvcclient import camera as uvc_camera caminfo = self._nvr.get_camera(self._uuid) if self._connect_addr: @@ -160,7 +158,6 @@ class UnifiVideoCamera(Camera): def camera_image(self): """Return the image of this camera.""" - from uvcclient import camera as uvc_camera if not self._camera: if not self._login(): @@ -182,7 +179,6 @@ class UnifiVideoCamera(Camera): def set_motion_detection(self, mode): """Set motion detection on or off.""" - from uvcclient.nvr import NvrError if mode is True: set_mode = "motion" @@ -192,7 +188,7 @@ class UnifiVideoCamera(Camera): try: self._nvr.set_recordmode(self._uuid, set_mode) self._motion_status = mode - except NvrError as err: + except nvr.NvrError as err: _LOGGER.error("Unable to set recordmode to %s", set_mode) _LOGGER.debug(err) diff --git a/homeassistant/components/vacuum/.translations/bg.json b/homeassistant/components/vacuum/.translations/bg.json new file mode 100644 index 00000000000..1ab7fce7abe --- /dev/null +++ b/homeassistant/components/vacuum/.translations/bg.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "clean": "\u041d\u0435\u043a\u0430 {entity_name} \u043f\u043e\u0447\u0438\u0441\u0442\u0438", + "dock": "\u041d\u0435\u043a\u0430 {entity_name} \u0434\u0430 \u0441\u0435 \u0432\u044a\u0440\u043d\u0435 \u0432 \u0431\u0430\u0437\u043e\u0432\u0430\u0442\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f" + }, + "condition_type": { + "is_cleaning": "{entity_name} \u043f\u043e\u0447\u0438\u0441\u0442\u0432\u0430", + "is_docked": "{entity_name} \u0435 \u0432 \u0431\u0430\u0437\u043e\u0432\u0430\u0442\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f" + }, + "trigger_type": { + "cleaning": "{entity_name} \u0437\u0430\u043f\u043e\u0447\u043d\u0430 \u043f\u043e\u0447\u0438\u0441\u0442\u0432\u0430\u043d\u0435", + "docked": "{entity_name} \u0432 \u0431\u0430\u0437\u043e\u0432\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vacuum/.translations/ca.json b/homeassistant/components/vacuum/.translations/ca.json index ee69152ed5c..b3cdbb2f6c7 100644 --- a/homeassistant/components/vacuum/.translations/ca.json +++ b/homeassistant/components/vacuum/.translations/ca.json @@ -4,7 +4,7 @@ "clean": "Fes que {entity_name} netegi", "dock": "Fes que {entity_name} torni a la base" }, - "condtion_type": { + "condition_type": { "is_cleaning": "{entity_name} est\u00e0 netejant", "is_docked": "{entity_name} est\u00e0 acoblada" }, diff --git a/homeassistant/components/vacuum/.translations/da.json b/homeassistant/components/vacuum/.translations/da.json new file mode 100644 index 00000000000..fac748ca464 --- /dev/null +++ b/homeassistant/components/vacuum/.translations/da.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "clean": "Lad {entity_name} g\u00f8re rent", + "dock": "Lad {entity_name} vende tilbage til dock" + }, + "condition_type": { + "is_cleaning": "{entity_name} g\u00f8r rent", + "is_docked": "{entity_name} er i dock" + }, + "trigger_type": { + "cleaning": "{entity_name} begyndte at reng\u00f8re", + "docked": "{entity_name} er i dock" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vacuum/.translations/de.json b/homeassistant/components/vacuum/.translations/de.json index 060358a0a7a..3fe2d57eb01 100644 --- a/homeassistant/components/vacuum/.translations/de.json +++ b/homeassistant/components/vacuum/.translations/de.json @@ -1,7 +1,16 @@ { "device_automation": { - "condtion_type": { - "is_cleaning": "{entity_name} reinigt" + "action_type": { + "clean": "Lass {entity_name} reinigen", + "dock": "Lass {entity_name} zum Dock zur\u00fcckkehren" + }, + "condition_type": { + "is_cleaning": "{entity_name} reinigt", + "is_docked": "{entity_name} ist angedockt" + }, + "trigger_type": { + "cleaning": "{entity_name} hat mit der Reinigung begonnen", + "docked": "{entity_name} angedockt" } } } \ No newline at end of file diff --git a/homeassistant/components/vacuum/.translations/en.json b/homeassistant/components/vacuum/.translations/en.json index 396c6a83be9..3feb8eada72 100644 --- a/homeassistant/components/vacuum/.translations/en.json +++ b/homeassistant/components/vacuum/.translations/en.json @@ -4,7 +4,7 @@ "clean": "Let {entity_name} clean", "dock": "Let {entity_name} return to the dock" }, - "condtion_type": { + "condition_type": { "is_cleaning": "{entity_name} is cleaning", "is_docked": "{entity_name} is docked" }, diff --git a/homeassistant/components/vacuum/.translations/es.json b/homeassistant/components/vacuum/.translations/es.json index 9ecf3ade99c..376058faafa 100644 --- a/homeassistant/components/vacuum/.translations/es.json +++ b/homeassistant/components/vacuum/.translations/es.json @@ -4,9 +4,9 @@ "clean": "Deje que {entity_name} limpie", "dock": "Deje que {entity_name} regrese a la base" }, - "condtion_type": { + "condition_type": { "is_cleaning": "{entity_name} est\u00e1 limpiando", - "is_docked": "{entity_name} est\u00e1 acoplado" + "is_docked": "{entity_name} en la base" }, "trigger_type": { "cleaning": "{entity_name} empez\u00f3 a limpiar", diff --git a/homeassistant/components/vacuum/.translations/fr.json b/homeassistant/components/vacuum/.translations/fr.json index 44e7b2887e2..84d5e17bda1 100644 --- a/homeassistant/components/vacuum/.translations/fr.json +++ b/homeassistant/components/vacuum/.translations/fr.json @@ -4,13 +4,13 @@ "clean": "Laisser {entity_name} vide", "dock": "Laisser {entity_name} retourner \u00e0 la base" }, - "condtion_type": { + "condition_type": { "is_cleaning": "{entity_name} nettoie", - "is_docked": "{entity_name} est sur la base" + "is_docked": "{entity_name} est connect\u00e9" }, "trigger_type": { "cleaning": "{entity_name} commence \u00e0 nettoyer", - "docked": "{entity_name} est sur la base" + "docked": "{entity_name} connect\u00e9" } } } \ No newline at end of file diff --git a/homeassistant/components/vacuum/.translations/hu.json b/homeassistant/components/vacuum/.translations/hu.json new file mode 100644 index 00000000000..81a39802c55 --- /dev/null +++ b/homeassistant/components/vacuum/.translations/hu.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "clean": "{entity_name} takar\u00edt\u00e1s ind\u00edt\u00e1sa", + "dock": "{entity_name} visszak\u00fcld\u00e9se a dokkol\u00f3ra" + }, + "condition_type": { + "is_cleaning": "{entity_name} takar\u00edt", + "is_docked": "{entity_name} dokkolva van" + }, + "trigger_type": { + "cleaning": "{entity_name} elkezdett takar\u00edtani", + "docked": "{entity_name} dokkolt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vacuum/.translations/it.json b/homeassistant/components/vacuum/.translations/it.json index 0b879f154fa..32ecd1e0377 100644 --- a/homeassistant/components/vacuum/.translations/it.json +++ b/homeassistant/components/vacuum/.translations/it.json @@ -4,7 +4,7 @@ "clean": "Lascia pulire {entity_name}", "dock": "Lascia che {entity_name} ritorni alla base" }, - "condtion_type": { + "condition_type": { "is_cleaning": "{entity_name} sta pulendo", "is_docked": "{entity_name} \u00e8 agganciato alla base" }, diff --git a/homeassistant/components/vacuum/.translations/ko.json b/homeassistant/components/vacuum/.translations/ko.json new file mode 100644 index 00000000000..0197329abda --- /dev/null +++ b/homeassistant/components/vacuum/.translations/ko.json @@ -0,0 +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" + }, + "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" + }, + "trigger_type": { + "cleaning": "{entity_name} \uc774(\uac00) \uccad\uc18c\ub97c \uc2dc\uc791\ud560 \ub54c", + "docked": "{entity_name} \uc774(\uac00) \ub3c4\ud0b9\ub420 \ub54c" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vacuum/.translations/lb.json b/homeassistant/components/vacuum/.translations/lb.json index 6d984b997fa..d6776ccd619 100644 --- a/homeassistant/components/vacuum/.translations/lb.json +++ b/homeassistant/components/vacuum/.translations/lb.json @@ -4,7 +4,7 @@ "clean": "Looss {entity_name} botzen", "dock": "Sch\u00e9ck {entity_name} z\u00e9reck zur Statioun" }, - "condtion_type": { + "condition_type": { "is_cleaning": "{entity_name} botzt", "is_docked": "{entity_name} ass an der Statioun" }, diff --git a/homeassistant/components/vacuum/.translations/nl.json b/homeassistant/components/vacuum/.translations/nl.json new file mode 100644 index 00000000000..8ef0588796c --- /dev/null +++ b/homeassistant/components/vacuum/.translations/nl.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "clean": "Laat {entity_name} schoonmaken", + "dock": "Laat {entity_name} terugkeren naar het basisstation" + }, + "condition_type": { + "is_cleaning": "{entity_name} is aan het schoonmaken", + "is_docked": "{entity_name} is bij basisstation" + }, + "trigger_type": { + "cleaning": "{entity_name} begon met schoonmaken", + "docked": "{entity_name} is bij basisstation" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vacuum/.translations/no.json b/homeassistant/components/vacuum/.translations/no.json index 7d6475f8cef..0c34081cb2f 100644 --- a/homeassistant/components/vacuum/.translations/no.json +++ b/homeassistant/components/vacuum/.translations/no.json @@ -4,7 +4,7 @@ "clean": "La {entity_name} rengj\u00f8res", "dock": "La {entity_name} tilbake til dock" }, - "condtion_type": { + "condition_type": { "is_cleaning": "{entity_name} rengj\u00f8res", "is_docked": "{entity_name} er docked" }, diff --git a/homeassistant/components/vacuum/.translations/pl.json b/homeassistant/components/vacuum/.translations/pl.json index e637c26b3ed..09eef23ac9a 100644 --- a/homeassistant/components/vacuum/.translations/pl.json +++ b/homeassistant/components/vacuum/.translations/pl.json @@ -4,7 +4,7 @@ "clean": "niech {entity_name} sprz\u0105ta", "dock": "niech {entity_name} wr\u00f3ci do bazy" }, - "condtion_type": { + "condition_type": { "is_cleaning": "{entity_name} sprz\u0105ta", "is_docked": "{entity_name} jest w bazie" }, diff --git a/homeassistant/components/vacuum/.translations/pt.json b/homeassistant/components/vacuum/.translations/pt.json index 42b8bdabc0f..15b8ac3fd19 100644 --- a/homeassistant/components/vacuum/.translations/pt.json +++ b/homeassistant/components/vacuum/.translations/pt.json @@ -3,7 +3,7 @@ "action_type": { "clean": "Deixar {entity_name} limpar" }, - "condtion_type": { + "condition_type": { "is_cleaning": "{entity_name} est\u00e1 a limpar" } } diff --git a/homeassistant/components/vacuum/.translations/ru.json b/homeassistant/components/vacuum/.translations/ru.json index c727e8f6ea3..c42f0310fae 100644 --- a/homeassistant/components/vacuum/.translations/ru.json +++ b/homeassistant/components/vacuum/.translations/ru.json @@ -4,7 +4,7 @@ "clean": "\u041e\u0442\u043f\u0440\u0430\u0432\u0438\u0442\u044c {entity_name} \u0434\u0435\u043b\u0430\u0442\u044c \u0443\u0431\u043e\u0440\u043a\u0443", "dock": "\u0412\u0435\u0440\u043d\u0443\u0442\u044c {entity_name} \u043d\u0430 \u0434\u043e\u043a-\u0441\u0442\u0430\u043d\u0446\u0438\u044e" }, - "condtion_type": { + "condition_type": { "is_cleaning": "{entity_name} \u0434\u0435\u043b\u0430\u0435\u0442 \u0443\u0431\u043e\u0440\u043a\u0443", "is_docked": "{entity_name} \u0443 \u0434\u043e\u043a-\u0441\u0442\u0430\u043d\u0446\u0438\u0438" }, diff --git a/homeassistant/components/vacuum/.translations/sl.json b/homeassistant/components/vacuum/.translations/sl.json index 25de303b157..c594c4f1bdd 100644 --- a/homeassistant/components/vacuum/.translations/sl.json +++ b/homeassistant/components/vacuum/.translations/sl.json @@ -4,7 +4,7 @@ "clean": "Naj {entity_name} \u010disti", "dock": "Pustite, da se {entity_name} vrne na dok" }, - "condtion_type": { + "condition_type": { "is_cleaning": "{entity_name} \u010disti", "is_docked": "{entity_name} je priklju\u010den" }, diff --git a/homeassistant/components/vacuum/.translations/zh-Hant.json b/homeassistant/components/vacuum/.translations/zh-Hant.json index f0ad431afc9..b406e1baede 100644 --- a/homeassistant/components/vacuum/.translations/zh-Hant.json +++ b/homeassistant/components/vacuum/.translations/zh-Hant.json @@ -4,7 +4,7 @@ "clean": "\u555f\u52d5 {entity_name} \u6e05\u9664", "dock": "\u555f\u52d5 {entity_name} \u56de\u5230\u5145\u96fb\u7ad9" }, - "condtion_type": { + "condition_type": { "is_cleaning": "{entity_name} \u6b63\u5728\u6e05\u6383", "is_docked": "{entity_name} \u65bc\u5145\u96fb\u7ad9" }, diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index c1f1131a52f..62eac6e39f5 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -12,21 +12,20 @@ from homeassistant.const import ( # noqa: F401 # STATE_PAUSED/IDLE are API SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, + STATE_IDLE, STATE_ON, STATE_PAUSED, - STATE_IDLE, ) -from homeassistant.loader import bind_hass import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa: F401 - ENTITY_SERVICE_SCHEMA, PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, + make_entity_service_schema, ) +from homeassistant.helpers.entity import Entity, ToggleEntity from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.entity import ToggleEntity, Entity from homeassistant.helpers.icon import icon_for_battery_level - +from homeassistant.loader import bind_hass # mypy: allow-untyped-defs, no-check-untyped-defs @@ -55,16 +54,6 @@ SERVICE_START = "start" SERVICE_PAUSE = "pause" SERVICE_STOP = "stop" -VACUUM_SET_FAN_SPEED_SERVICE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_FAN_SPEED): cv.string} -) - -VACUUM_SEND_COMMAND_SERVICE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - { - vol.Required(ATTR_COMMAND): cv.string, - vol.Optional(ATTR_PARAMS): vol.Any(dict, cv.ensure_list), - } -) STATE_CLEANING = "cleaning" STATE_DOCKED = "docked" @@ -106,43 +95,32 @@ async def async_setup(hass, config): await component.async_setup(config) + component.async_register_entity_service(SERVICE_TURN_ON, {}, "async_turn_on") + component.async_register_entity_service(SERVICE_TURN_OFF, {}, "async_turn_off") + component.async_register_entity_service(SERVICE_TOGGLE, {}, "async_toggle") component.async_register_entity_service( - SERVICE_TURN_ON, ENTITY_SERVICE_SCHEMA, "async_turn_on" + SERVICE_START_PAUSE, {}, "async_start_pause" ) + component.async_register_entity_service(SERVICE_START, {}, "async_start") + component.async_register_entity_service(SERVICE_PAUSE, {}, "async_pause") component.async_register_entity_service( - SERVICE_TURN_OFF, ENTITY_SERVICE_SCHEMA, "async_turn_off" - ) - component.async_register_entity_service( - SERVICE_TOGGLE, ENTITY_SERVICE_SCHEMA, "async_toggle" - ) - component.async_register_entity_service( - SERVICE_START_PAUSE, ENTITY_SERVICE_SCHEMA, "async_start_pause" - ) - component.async_register_entity_service( - SERVICE_START, ENTITY_SERVICE_SCHEMA, "async_start" - ) - component.async_register_entity_service( - SERVICE_PAUSE, ENTITY_SERVICE_SCHEMA, "async_pause" - ) - component.async_register_entity_service( - SERVICE_RETURN_TO_BASE, ENTITY_SERVICE_SCHEMA, "async_return_to_base" - ) - component.async_register_entity_service( - SERVICE_CLEAN_SPOT, ENTITY_SERVICE_SCHEMA, "async_clean_spot" - ) - component.async_register_entity_service( - SERVICE_LOCATE, ENTITY_SERVICE_SCHEMA, "async_locate" - ) - component.async_register_entity_service( - SERVICE_STOP, ENTITY_SERVICE_SCHEMA, "async_stop" + SERVICE_RETURN_TO_BASE, {}, "async_return_to_base" ) + component.async_register_entity_service(SERVICE_CLEAN_SPOT, {}, "async_clean_spot") + component.async_register_entity_service(SERVICE_LOCATE, {}, "async_locate") + component.async_register_entity_service(SERVICE_STOP, {}, "async_stop") component.async_register_entity_service( SERVICE_SET_FAN_SPEED, - VACUUM_SET_FAN_SPEED_SERVICE_SCHEMA, + {vol.Required(ATTR_FAN_SPEED): cv.string}, "async_set_fan_speed", ) component.async_register_entity_service( - SERVICE_SEND_COMMAND, VACUUM_SEND_COMMAND_SERVICE_SCHEMA, "async_send_command" + SERVICE_SEND_COMMAND, + { + vol.Required(ATTR_COMMAND): cv.string, + vol.Optional(ATTR_PARAMS): vol.Any(dict, cv.ensure_list), + }, + "async_send_command", ) return True diff --git a/homeassistant/components/vacuum/device_action.py b/homeassistant/components/vacuum/device_action.py index e5f8c162fbd..ed25289da10 100644 --- a/homeassistant/components/vacuum/device_action.py +++ b/homeassistant/components/vacuum/device_action.py @@ -1,18 +1,20 @@ """Provides device automations for Vacuum.""" -from typing import Optional, List +from typing import List, Optional + import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, - CONF_DOMAIN, - CONF_TYPE, CONF_DEVICE_ID, + CONF_DOMAIN, CONF_ENTITY_ID, + CONF_TYPE, ) -from homeassistant.core import HomeAssistant, Context +from homeassistant.core import Context, HomeAssistant from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv -from . import DOMAIN, SERVICE_START, SERVICE_RETURN_TO_BASE + +from . import DOMAIN, SERVICE_RETURN_TO_BASE, SERVICE_START ACTION_TYPES = {"clean", "dock"} diff --git a/homeassistant/components/vacuum/device_condition.py b/homeassistant/components/vacuum/device_condition.py index 6a41fe0490e..5a2eefd94f2 100644 --- a/homeassistant/components/vacuum/device_condition.py +++ b/homeassistant/components/vacuum/device_condition.py @@ -1,20 +1,22 @@ """Provide the device automations for Vacuum.""" from typing import Dict, List + import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, CONF_CONDITION, - CONF_DOMAIN, - CONF_TYPE, CONF_DEVICE_ID, + CONF_DOMAIN, CONF_ENTITY_ID, + CONF_TYPE, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import condition, config_validation as cv, entity_registry -from homeassistant.helpers.typing import ConfigType, TemplateVarsType from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA -from . import DOMAIN, STATE_DOCKED, STATE_CLEANING, STATE_RETURNING +from homeassistant.helpers.typing import ConfigType, TemplateVarsType + +from . import DOMAIN, STATE_CLEANING, STATE_DOCKED, STATE_RETURNING CONDITION_TYPES = {"is_cleaning", "is_docked"} diff --git a/homeassistant/components/vacuum/device_trigger.py b/homeassistant/components/vacuum/device_trigger.py index 328db54b1b9..ee225ab3caa 100644 --- a/homeassistant/components/vacuum/device_trigger.py +++ b/homeassistant/components/vacuum/device_trigger.py @@ -1,19 +1,21 @@ """Provides device automations for Vacuum.""" from typing import List + import voluptuous as vol +from homeassistant.components.automation import AutomationActionType, state +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA from homeassistant.const import ( - CONF_DOMAIN, - CONF_TYPE, - CONF_PLATFORM, CONF_DEVICE_ID, + CONF_DOMAIN, CONF_ENTITY_ID, + CONF_PLATFORM, + CONF_TYPE, ) -from homeassistant.core import HomeAssistant, CALLBACK_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 homeassistant.components.automation import state, AutomationActionType -from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA + from . import DOMAIN, STATE_CLEANING, STATE_DOCKED, STATES TRIGGER_TYPES = {"cleaning", "docked"} diff --git a/homeassistant/components/vacuum/services.yaml b/homeassistant/components/vacuum/services.yaml index fe5bb77cefe..7db70c5cd51 100644 --- a/homeassistant/components/vacuum/services.yaml +++ b/homeassistant/components/vacuum/services.yaml @@ -85,81 +85,3 @@ set_fan_speed: fan_speed: description: Platform dependent vacuum cleaner fan speed, with speed steps, like 'medium' or by percentage, between 0 and 100. example: 'low' - -xiaomi_remote_control_start: - description: Start remote control of the vacuum cleaner. You can then move it with `remote_control_move`, when done call `remote_control_stop`. - fields: - entity_id: - description: Name of the vacuum entity. - example: 'vacuum.xiaomi_vacuum_cleaner' - -xiaomi_remote_control_stop: - description: Stop remote control mode of the vacuum cleaner. - fields: - entity_id: - description: Name of the vacuum entity. - example: 'vacuum.xiaomi_vacuum_cleaner' - -xiaomi_remote_control_move: - description: Remote control the vacuum cleaner, make sure you first set it in remote control mode with `remote_control_start`. - fields: - entity_id: - description: Name of the vacuum entity. - example: 'vacuum.xiaomi_vacuum_cleaner' - velocity: - description: Speed, between -0.29 and 0.29. - example: '0.2' - rotation: - description: Rotation, between -179 degrees and 179 degrees. - example: '90' - duration: - description: Duration of the movement. - example: '1500' - -xiaomi_remote_control_move_step: - description: Remote control the vacuum cleaner, only makes one move and then stops. - fields: - entity_id: - description: Name of the vacuum entity. - example: 'vacuum.xiaomi_vacuum_cleaner' - velocity: - description: Speed, between -0.29 and 0.29. - example: '0.2' - rotation: - description: Rotation, between -179 degrees and 179 degrees. - example: '90' - duration: - description: Duration of the movement. - example: '1500' - -xiaomi_clean_zone: - description: Start the cleaning operation in the selected areas for the number of repeats indicated. - fields: - entity_id: - description: Name of the vacuum entity. - example: 'vacuum.xiaomi_vacuum_cleaner' - zone: - description: Array of zones. Each zone is an array of 4 integer values. - example: '[[23510,25311,25110,26362]]' - repeats: - description: Number of cleaning repeats for each zone between 1 and 3. - example: '1' - -neato_custom_cleaning: - description: Zone Cleaning service call specific to Neato Botvacs. - fields: - entity_id: - description: Name of the vacuum entity. [Required] - example: 'vacuum.neato' - mode: - description: "Set the cleaning mode: 1 for eco and 2 for turbo. Defaults to turbo if not set." - example: 2 - navigation: - description: "Set the navigation mode: 1 for normal, 2 for extra care, 3 for deep. Defaults to normal if not set." - example: 1 - category: - description: "Whether to use a persistent map or not for cleaning (i.e. No go lines): 2 for no map, 4 for map. Default to using map if not set (and fallback to no map if no map is found)." - example: 2 - zone: - description: Only supported on the Botvac D7. Name of the zone to clean. Defaults to no zone i.e. complete house cleanup. - example: "Kitchen" diff --git a/homeassistant/components/vacuum/strings.json b/homeassistant/components/vacuum/strings.json index 0300242a506..4eee3f359b5 100644 --- a/homeassistant/components/vacuum/strings.json +++ b/homeassistant/components/vacuum/strings.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_docked": "{entity_name} is docked", "is_cleaning": "{entity_name} is cleaning" }, diff --git a/homeassistant/components/vasttrafik/sensor.py b/homeassistant/components/vasttrafik/sensor.py index d13383a0832..54fd0a5503e 100644 --- a/homeassistant/components/vasttrafik/sensor.py +++ b/homeassistant/components/vasttrafik/sensor.py @@ -5,9 +5,9 @@ import logging import vasttrafik import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION +from homeassistant.const import ATTR_ATTRIBUTION, 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 diff --git a/homeassistant/components/velbus/__init__.py b/homeassistant/components/velbus/__init__.py index 9946f06446f..de48f846540 100644 --- a/homeassistant/components/velbus/__init__.py +++ b/homeassistant/components/velbus/__init__.py @@ -1,13 +1,14 @@ """Support for Velbus devices.""" import asyncio import logging + import velbus import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_PORT, CONF_NAME +from homeassistant.const import CONF_NAME, CONF_PORT from homeassistant.exceptions import ConfigEntryNotReady +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType @@ -21,7 +22,7 @@ CONFIG_SCHEMA = vol.Schema( {DOMAIN: vol.Schema({vol.Required(CONF_PORT): cv.string})}, extra=vol.ALLOW_EXTRA ) -COMPONENT_TYPES = ["switch", "sensor", "binary_sensor", "cover", "climate"] +COMPONENT_TYPES = ["switch", "sensor", "binary_sensor", "cover", "climate", "light"] async def async_setup(hass, config): diff --git a/homeassistant/components/velbus/binary_sensor.py b/homeassistant/components/velbus/binary_sensor.py index 9230632e442..505303ded24 100644 --- a/homeassistant/components/velbus/binary_sensor.py +++ b/homeassistant/components/velbus/binary_sensor.py @@ -3,8 +3,8 @@ import logging from homeassistant.components.binary_sensor import BinarySensorDevice -from .const import DOMAIN from . import VelbusEntity +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/velbus/climate.py b/homeassistant/components/velbus/climate.py index eb5ed00c395..812e4605d95 100644 --- a/homeassistant/components/velbus/climate.py +++ b/homeassistant/components/velbus/climate.py @@ -10,8 +10,8 @@ from homeassistant.components.climate.const import ( ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT -from .const import DOMAIN from . import VelbusEntity +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/velbus/config_flow.py b/homeassistant/components/velbus/config_flow.py index e9cbe14ce25..9325acf0608 100644 --- a/homeassistant/components/velbus/config_flow.py +++ b/homeassistant/components/velbus/config_flow.py @@ -3,7 +3,7 @@ import velbus import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_PORT, CONF_NAME +from homeassistant.const import CONF_NAME, CONF_PORT from homeassistant.core import HomeAssistant, callback from homeassistant.util import slugify diff --git a/homeassistant/components/velbus/cover.py b/homeassistant/components/velbus/cover.py index cf73af593b8..aea02331ead 100644 --- a/homeassistant/components/velbus/cover.py +++ b/homeassistant/components/velbus/cover.py @@ -4,14 +4,14 @@ import logging from velbus.util import VelbusException from homeassistant.components.cover import ( - CoverDevice, SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_STOP, + CoverDevice, ) -from .const import DOMAIN from . import VelbusEntity +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/velbus/light.py b/homeassistant/components/velbus/light.py new file mode 100644 index 00000000000..6b34182e559 --- /dev/null +++ b/homeassistant/components/velbus/light.py @@ -0,0 +1,77 @@ +"""Support for Velbus light.""" +import logging + +from velbus.util import VelbusException + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_TRANSITION, + SUPPORT_BRIGHTNESS, + SUPPORT_TRANSITION, + Light, +) + +from . import VelbusEntity +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Old way.""" + pass + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up Velbus light based on config_entry.""" + cntrl = hass.data[DOMAIN][entry.entry_id]["cntrl"] + modules_data = hass.data[DOMAIN][entry.entry_id]["light"] + entities = [] + for address, channel in modules_data: + module = cntrl.get_module(address) + entities.append(VelbusLight(module, channel)) + async_add_entities(entities) + + +class VelbusLight(VelbusEntity, Light): + """Representation of a Velbus light.""" + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION + + @property + def is_on(self): + """Return true if the light is on.""" + return self._module.is_on(self._channel) + + @property + def brightness(self): + """Return the brightness of the light.""" + return self._module.get_dimmer_state(self._channel) + + def turn_on(self, **kwargs): + """Instruct the Velbus light to turn on.""" + try: + if ATTR_BRIGHTNESS in kwargs: + self._module.set_dimmer_state( + self._channel, + kwargs[ATTR_BRIGHTNESS], + kwargs.get(ATTR_TRANSITION, 0), + ) + else: + self._module.restore_dimmer_state( + self._channel, kwargs.get(ATTR_TRANSITION, 0), + ) + except VelbusException as err: + _LOGGER.error("A Velbus error occurred: %s", err) + + def turn_off(self, **kwargs): + """Instruct the velbus light to turn off.""" + try: + self._module.set_dimmer_state( + self._channel, 0, kwargs.get(ATTR_TRANSITION, 0), + ) + except VelbusException as err: + _LOGGER.error("A Velbus error occurred: %s", err) diff --git a/homeassistant/components/velbus/manifest.json b/homeassistant/components/velbus/manifest.json index 1d9401f6cfe..007ca421276 100644 --- a/homeassistant/components/velbus/manifest.json +++ b/homeassistant/components/velbus/manifest.json @@ -3,7 +3,7 @@ "name": "Velbus", "documentation": "https://www.home-assistant.io/integrations/velbus", "requirements": [ - "python-velbus==2.0.27" + "python-velbus==2.0.32" ], "config_flow": true, "dependencies": [], diff --git a/homeassistant/components/velbus/sensor.py b/homeassistant/components/velbus/sensor.py index 3b7f2b6f5bc..8af5df9e165 100644 --- a/homeassistant/components/velbus/sensor.py +++ b/homeassistant/components/velbus/sensor.py @@ -1,8 +1,8 @@ """Support for Velbus sensors.""" import logging -from .const import DOMAIN from . import VelbusEntity +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/velux/__init__.py b/homeassistant/components/velux/__init__.py index 51f615e68aa..bac65c969cf 100644 --- a/homeassistant/components/velux/__init__.py +++ b/homeassistant/components/velux/__init__.py @@ -1,12 +1,12 @@ """Support for VELUX KLF 200 devices.""" import logging -import voluptuous as vol -from pyvlx import PyVLX -from pyvlx import PyVLXException +from pyvlx import PyVLX, PyVLXException +import voluptuous as vol + +from homeassistant.const import CONF_HOST, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_HOST, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP DOMAIN = "velux" DATA_VELUX = "data_velux" diff --git a/homeassistant/components/velux/cover.py b/homeassistant/components/velux/cover.py index a471960b048..c9b4aa53fe5 100644 --- a/homeassistant/components/velux/cover.py +++ b/homeassistant/components/velux/cover.py @@ -1,4 +1,7 @@ """Support for Velux covers.""" +from pyvlx import OpeningDevice, Position +from pyvlx.opening_device import Awning, Blind, GarageDoor, RollerShutter, Window + from homeassistant.components.cover import ( ATTR_POSITION, SUPPORT_CLOSE, @@ -16,7 +19,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= """Set up cover(s) for Velux platform.""" entities = [] for node in hass.data[DATA_VELUX].pyvlx.nodes: - from pyvlx import OpeningDevice if isinstance(node, OpeningDevice): entities.append(VeluxCover(node)) @@ -67,8 +69,6 @@ class VeluxCover(CoverDevice): @property def device_class(self): """Define this cover as either window/blind/awning/shutter.""" - from pyvlx.opening_device import Blind, RollerShutter, Window, Awning - if isinstance(self.node, Window): return "window" if isinstance(self.node, Blind): @@ -77,6 +77,8 @@ class VeluxCover(CoverDevice): return "shutter" if isinstance(self.node, Awning): return "awning" + if isinstance(self.node, GarageDoor): + return "garage" return "window" @property @@ -96,7 +98,6 @@ class VeluxCover(CoverDevice): """Move the cover to a specific position.""" if ATTR_POSITION in kwargs: position_percent = 100 - kwargs[ATTR_POSITION] - from pyvlx import Position await self.node.set_position( Position(position_percent=position_percent), wait_for_completion=False diff --git a/homeassistant/components/velux/manifest.json b/homeassistant/components/velux/manifest.json index 783e23a8171..d2fbb3b728a 100644 --- a/homeassistant/components/velux/manifest.json +++ b/homeassistant/components/velux/manifest.json @@ -3,7 +3,7 @@ "name": "Velux", "documentation": "https://www.home-assistant.io/integrations/velux", "requirements": [ - "pyvlx==0.2.11" + "pyvlx==0.2.12" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/venstar/climate.py b/homeassistant/components/venstar/climate.py index de26d236649..4ffe75acb9e 100644 --- a/homeassistant/components/venstar/climate.py +++ b/homeassistant/components/venstar/climate.py @@ -4,27 +4,27 @@ import logging from venstarcolortouch import VenstarColorTouch import voluptuous as vol -from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice from homeassistant.components.climate.const import ( ATTR_HVAC_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, + CURRENT_HVAC_COOL, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + CURRENT_HVAC_OFF, + FAN_AUTO, + FAN_ON, HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_OFF, - CURRENT_HVAC_HEAT, - CURRENT_HVAC_COOL, - CURRENT_HVAC_IDLE, - CURRENT_HVAC_OFF, - SUPPORT_FAN_MODE, - FAN_ON, - FAN_AUTO, - SUPPORT_TARGET_HUMIDITY, - SUPPORT_PRESET_MODE, - SUPPORT_TARGET_TEMPERATURE, PRESET_AWAY, PRESET_NONE, + SUPPORT_FAN_MODE, + SUPPORT_PRESET_MODE, + SUPPORT_TARGET_HUMIDITY, + SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, ) from homeassistant.const import ( @@ -34,7 +34,7 @@ from homeassistant.const import ( CONF_SSL, CONF_TIMEOUT, CONF_USERNAME, - PRECISION_WHOLE, + PRECISION_HALVES, STATE_ON, TEMP_CELSIUS, TEMP_FAHRENHEIT, @@ -134,9 +134,9 @@ class VenstarThermostat(ClimateDevice): """Return the precision of the system. Venstar temperature values are passed back and forth in the - API as whole degrees C or F. + API in C or F, with half-degree accuracy. """ - return PRECISION_WHOLE + return PRECISION_HALVES @property def temperature_unit(self): diff --git a/homeassistant/components/venstar/manifest.json b/homeassistant/components/venstar/manifest.json index e8e36d04467..78243844a93 100644 --- a/homeassistant/components/venstar/manifest.json +++ b/homeassistant/components/venstar/manifest.json @@ -3,7 +3,7 @@ "name": "Venstar", "documentation": "https://www.home-assistant.io/integrations/venstar", "requirements": [ - "venstarcolortouch==0.9" + "venstarcolortouch==0.12" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/vera/__init__.py b/homeassistant/components/vera/__init__.py index 8fcc8a4a2fe..1c9d412d974 100644 --- a/homeassistant/components/vera/__init__.py +++ b/homeassistant/components/vera/__init__.py @@ -1,25 +1,24 @@ """Support for Vera devices.""" -import logging from collections import defaultdict +import logging import pyvera as veraApi -import voluptuous as vol from requests.exceptions import RequestException +import voluptuous as vol -from homeassistant.util.dt import utc_from_timestamp -from homeassistant.util import convert, slugify -from homeassistant.helpers import discovery -from homeassistant.helpers import config_validation as cv from homeassistant.const import ( ATTR_ARMED, ATTR_BATTERY_LEVEL, ATTR_LAST_TRIP_TIME, ATTR_TRIPPED, - EVENT_HOMEASSISTANT_STOP, - CONF_LIGHTS, CONF_EXCLUDE, + CONF_LIGHTS, + EVENT_HOMEASSISTANT_STOP, ) +from homeassistant.helpers import config_validation as cv, discovery from homeassistant.helpers.entity import Entity +from homeassistant.util import convert, slugify +from homeassistant.util.dt import utc_from_timestamp _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/vera/climate.py b/homeassistant/components/vera/climate.py index 50c897d0fc1..60e73d48978 100644 --- a/homeassistant/components/vera/climate.py +++ b/homeassistant/components/vera/climate.py @@ -92,6 +92,8 @@ class VeraThermostat(VeraDevice, ClimateDevice): else: self.vera_device.fan_auto() + self.schedule_update_ha_state() + @property def current_power_w(self): """Return the current power usage in W.""" @@ -129,6 +131,8 @@ class VeraThermostat(VeraDevice, ClimateDevice): if kwargs.get(ATTR_TEMPERATURE) is not None: self.vera_device.set_temperature(kwargs.get(ATTR_TEMPERATURE)) + self.schedule_update_ha_state() + def set_hvac_mode(self, hvac_mode): """Set new target hvac mode.""" if hvac_mode == HVAC_MODE_OFF: @@ -139,3 +143,5 @@ class VeraThermostat(VeraDevice, ClimateDevice): self.vera_device.turn_cool_on() elif hvac_mode == HVAC_MODE_HEAT: self.vera_device.turn_heat_on() + + self.schedule_update_ha_state() diff --git a/homeassistant/components/vera/manifest.json b/homeassistant/components/vera/manifest.json index 120ec241d60..70abc098431 100644 --- a/homeassistant/components/vera/manifest.json +++ b/homeassistant/components/vera/manifest.json @@ -3,7 +3,7 @@ "name": "Vera", "documentation": "https://www.home-assistant.io/integrations/vera", "requirements": [ - "pyvera==0.3.6" + "pyvera==0.3.7" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/verisure/__init__.py b/homeassistant/components/verisure/__init__.py index d5cc7f31efb..32735bf06c1 100644 --- a/homeassistant/components/verisure/__init__.py +++ b/homeassistant/components/verisure/__init__.py @@ -1,10 +1,10 @@ """Support for Verisure devices.""" +from datetime import timedelta import logging import threading -from datetime import timedelta + from jsonpath import jsonpath import verisure - import voluptuous as vol from homeassistant.const import ( @@ -14,8 +14,8 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, ) from homeassistant.helpers import discovery -from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv +from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/verisure/alarm_control_panel.py b/homeassistant/components/verisure/alarm_control_panel.py index 02f64b6fa9c..78a09e439d7 100644 --- a/homeassistant/components/verisure/alarm_control_panel.py +++ b/homeassistant/components/verisure/alarm_control_panel.py @@ -3,6 +3,10 @@ import logging from time import sleep import homeassistant.components.alarm_control_panel as alarm +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, @@ -33,7 +37,6 @@ def set_arm_state(state, code=None): while "result" not in transaction: sleep(0.5) transaction = hub.session.get_arm_state_transaction(transaction_id) - # pylint: disable=unexpected-keyword-arg hub.update_overview(no_throttle=True) @@ -64,6 +67,11 @@ class VerisureAlarm(alarm.AlarmControlPanel): """Return the state of the device.""" return self._state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY + @property def code_format(self): """Return one or more digits/characters.""" diff --git a/homeassistant/components/verisure/binary_sensor.py b/homeassistant/components/verisure/binary_sensor.py index 47ec3c536b3..bbdd9f54e83 100644 --- a/homeassistant/components/verisure/binary_sensor.py +++ b/homeassistant/components/verisure/binary_sensor.py @@ -2,8 +2,8 @@ import logging from homeassistant.components.binary_sensor import ( - BinarySensorDevice, DEVICE_CLASS_CONNECTIVITY, + BinarySensorDevice, ) from . import CONF_DOOR_WINDOW, HUB as hub diff --git a/homeassistant/components/verisure/manifest.json b/homeassistant/components/verisure/manifest.json index 38ea8c73147..13962e81b7b 100644 --- a/homeassistant/components/verisure/manifest.json +++ b/homeassistant/components/verisure/manifest.json @@ -3,8 +3,8 @@ "name": "Verisure", "documentation": "https://www.home-assistant.io/integrations/verisure", "requirements": [ - "jsonpath==0.75", - "vsure==1.5.2" + "jsonpath==0.82", + "vsure==1.5.4" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/versasense/__init__.py b/homeassistant/components/versasense/__init__.py new file mode 100644 index 00000000000..d2081d715d5 --- /dev/null +++ b/homeassistant/components/versasense/__init__.py @@ -0,0 +1,97 @@ +"""Support for VersaSense MicroPnP devices.""" +import logging + +import pyversasense as pyv +import voluptuous as vol + +from homeassistant.const import CONF_HOST +from homeassistant.helpers import aiohttp_client +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.discovery import async_load_platform + +from .const import ( + KEY_CONSUMER, + KEY_IDENTIFIER, + KEY_MEASUREMENT, + KEY_PARENT_MAC, + KEY_PARENT_NAME, + KEY_UNIT, + PERIPHERAL_CLASS_SENSOR, + PERIPHERAL_CLASS_SENSOR_ACTUATOR, +) + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "versasense" + +# Validation of the user's configuration +CONFIG_SCHEMA = vol.Schema( + {DOMAIN: vol.Schema({vol.Required(CONF_HOST): cv.string})}, extra=vol.ALLOW_EXTRA +) + + +async def async_setup(hass, config): + """Set up the versasense component.""" + session = aiohttp_client.async_get_clientsession(hass) + consumer = pyv.Consumer(config[DOMAIN]["host"], session) + + hass.data[DOMAIN] = {KEY_CONSUMER: consumer} + + await _configure_entities(hass, config, consumer) + + # Return boolean to indicate that initialization was successful. + return True + + +async def _configure_entities(hass, config, consumer): + """Fetch all devices with their peripherals for representation.""" + devices = await consumer.fetchDevices() + _LOGGER.debug(devices) + + sensor_info_list = [] + switch_info_list = [] + + for mac, device in devices.items(): + _LOGGER.info("Device connected: %s %s", device.name, mac) + hass.data[DOMAIN][mac] = {} + + for peripheral_id, peripheral in device.peripherals.items(): + hass.data[DOMAIN][mac][peripheral_id] = peripheral + + if peripheral.classification == PERIPHERAL_CLASS_SENSOR: + sensor_info_list = _add_entity_info_to_list( + peripheral, device, sensor_info_list + ) + elif peripheral.classification == PERIPHERAL_CLASS_SENSOR_ACTUATOR: + switch_info_list = _add_entity_info_to_list( + peripheral, device, switch_info_list + ) + + if sensor_info_list: + _load_platform(hass, config, "sensor", sensor_info_list) + + if switch_info_list: + _load_platform(hass, config, "switch", switch_info_list) + + +def _add_entity_info_to_list(peripheral, device, entity_info_list): + """Add info from a peripheral to specified list.""" + for measurement in peripheral.measurements: + entity_info = { + KEY_IDENTIFIER: peripheral.identifier, + KEY_UNIT: measurement.unit, + KEY_MEASUREMENT: measurement.name, + KEY_PARENT_NAME: device.name, + KEY_PARENT_MAC: device.mac, + } + + entity_info_list.append(entity_info) + + return entity_info_list + + +def _load_platform(hass, config, entity_type, entity_info_list): + """Load platform with list of entity info.""" + hass.async_create_task( + async_load_platform(hass, entity_type, DOMAIN, entity_info_list, config) + ) diff --git a/homeassistant/components/versasense/const.py b/homeassistant/components/versasense/const.py new file mode 100644 index 00000000000..5283f61ac26 --- /dev/null +++ b/homeassistant/components/versasense/const.py @@ -0,0 +1,11 @@ +"""Constants for versasense.""" +KEY_CONSUMER = "consumer" +KEY_IDENTIFIER = "identifier" +KEY_MEASUREMENT = "measurement" +KEY_PARENT_MAC = "parent_mac" +KEY_PARENT_NAME = "parent_name" +KEY_UNIT = "unit" +PERIPHERAL_CLASS_SENSOR = "sensor" +PERIPHERAL_CLASS_SENSOR_ACTUATOR = "sensor-actuator" +PERIPHERAL_STATE_OFF = "OFF" +PERIPHERAL_STATE_ON = "ON" diff --git a/homeassistant/components/versasense/manifest.json b/homeassistant/components/versasense/manifest.json new file mode 100644 index 00000000000..3e2be6131d1 --- /dev/null +++ b/homeassistant/components/versasense/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "versasense", + "name": "VersaSense", + "documentation": "https://www.home-assistant.io/components/versasense", + "dependencies": [], + "codeowners": ["@flamm3blemuff1n"], + "requirements": ["pyversasense==0.0.6"] +} diff --git a/homeassistant/components/versasense/sensor.py b/homeassistant/components/versasense/sensor.py new file mode 100644 index 00000000000..e598093cd37 --- /dev/null +++ b/homeassistant/components/versasense/sensor.py @@ -0,0 +1,97 @@ +"""Support for VersaSense sensor peripheral.""" +import logging + +from homeassistant.helpers.entity import Entity + +from . import DOMAIN +from .const import ( + KEY_CONSUMER, + KEY_IDENTIFIER, + KEY_MEASUREMENT, + KEY_PARENT_MAC, + KEY_PARENT_NAME, + KEY_UNIT, +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the sensor platform.""" + if discovery_info is None: + return None + + consumer = hass.data[DOMAIN][KEY_CONSUMER] + + sensor_list = [] + + for entity_info in discovery_info: + peripheral = hass.data[DOMAIN][entity_info[KEY_PARENT_MAC]][ + entity_info[KEY_IDENTIFIER] + ] + parent_name = entity_info[KEY_PARENT_NAME] + unit = entity_info[KEY_UNIT] + measurement = entity_info[KEY_MEASUREMENT] + + sensor_list.append( + VSensor(peripheral, parent_name, unit, measurement, consumer) + ) + + async_add_entities(sensor_list) + + +class VSensor(Entity): + """Representation of a Sensor.""" + + def __init__(self, peripheral, parent_name, unit, measurement, consumer): + """Initialize the sensor.""" + self._state = None + self._available = True + self._name = f"{parent_name} {measurement}" + self._parent_mac = peripheral.parentMac + self._identifier = peripheral.identifier + self._unit = unit + self._measurement = measurement + self.consumer = consumer + + @property + def unique_id(self): + """Return the unique id of the sensor.""" + return f"{self._parent_mac}/{self._identifier}/{self._measurement}" + + @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 unit_of_measurement(self): + """Return the unit of measurement.""" + return self._unit + + @property + def available(self): + """Return if the sensor is available.""" + return self._available + + async def async_update(self): + """Fetch new state data for the sensor.""" + samples = await self.consumer.fetchPeripheralSample( + None, self._identifier, self._parent_mac + ) + + if samples is not None: + for sample in samples: + if sample.measurement == self._measurement: + self._available = True + self._state = sample.value + break + else: + _LOGGER.error("Sample unavailable") + self._available = False + self._state = None diff --git a/homeassistant/components/versasense/switch.py b/homeassistant/components/versasense/switch.py new file mode 100644 index 00000000000..4b44cb7aa2a --- /dev/null +++ b/homeassistant/components/versasense/switch.py @@ -0,0 +1,113 @@ +"""Support for VersaSense actuator peripheral.""" +import logging + +from homeassistant.components.switch import SwitchDevice + +from . import DOMAIN +from .const import ( + KEY_CONSUMER, + KEY_IDENTIFIER, + KEY_MEASUREMENT, + KEY_PARENT_MAC, + KEY_PARENT_NAME, + KEY_UNIT, + PERIPHERAL_STATE_OFF, + PERIPHERAL_STATE_ON, +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up actuator platform.""" + if discovery_info is None: + return None + + consumer = hass.data[DOMAIN][KEY_CONSUMER] + + actuator_list = [] + + for entity_info in discovery_info: + peripheral = hass.data[DOMAIN][entity_info[KEY_PARENT_MAC]][ + entity_info[KEY_IDENTIFIER] + ] + parent_name = entity_info[KEY_PARENT_NAME] + unit = entity_info[KEY_UNIT] + measurement = entity_info[KEY_MEASUREMENT] + + actuator_list.append( + VActuator(peripheral, parent_name, unit, measurement, consumer) + ) + + async_add_entities(actuator_list) + + +class VActuator(SwitchDevice): + """Representation of an Actuator.""" + + def __init__(self, peripheral, parent_name, unit, measurement, consumer): + """Initialize the sensor.""" + self._is_on = False + self._available = True + self._name = f"{parent_name} {measurement}" + self._parent_mac = peripheral.parentMac + self._identifier = peripheral.identifier + self._unit = unit + self._measurement = measurement + self.consumer = consumer + + @property + def unique_id(self): + """Return the unique id of the actuator.""" + return f"{self._parent_mac}/{self._identifier}/{self._measurement}" + + @property + def name(self): + """Return the name of the actuator.""" + return self._name + + @property + def is_on(self): + """Return the state of the actuator.""" + return self._is_on + + @property + def available(self): + """Return if the actuator is available.""" + return self._available + + async def async_turn_off(self, **kwargs): + """Turn off the actuator.""" + await self.update_state(0) + + async def async_turn_on(self, **kwargs): + """Turn on the actuator.""" + await self.update_state(1) + + async def update_state(self, state): + """Update the state of the actuator.""" + payload = {"id": "state-num", "value": state} + + await self.consumer.actuatePeripheral( + None, self._identifier, self._parent_mac, payload + ) + + async def async_update(self): + """Fetch state data from the actuator.""" + samples = await self.consumer.fetchPeripheralSample( + None, self._identifier, self._parent_mac + ) + + if samples is not None: + for sample in samples: + if sample.measurement == self._measurement: + self._available = True + if sample.value == PERIPHERAL_STATE_OFF: + self._is_on = False + elif sample.value == PERIPHERAL_STATE_ON: + self._is_on = True + break + else: + _LOGGER.error("Sample unavailable") + self._available = False + self._is_on = None diff --git a/homeassistant/components/version/sensor.py b/homeassistant/components/version/sensor.py index 3e00b87e984..636e564b816 100644 --- a/homeassistant/components/version/sensor.py +++ b/homeassistant/components/version/sensor.py @@ -1,13 +1,20 @@ """Sensor that can display the current Home Assistant versions.""" -import logging from datetime import timedelta +import logging +from pyhaversion import ( + DockerVersion, + HaIoVersion, + HassioVersion, + LocalVersion, + PyPiVersion, +) import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.components.sensor import PLATFORM_SCHEMA 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 @@ -24,6 +31,8 @@ ALL_IMAGES = [ "raspberrypi2", "raspberrypi3", "raspberrypi3-64", + "raspberrypi4", + "raspberrypi4-64", "tinker", "odroid-c2", "odroid-xu", @@ -54,13 +63,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Version sensor platform.""" - from pyhaversion import ( - LocalVersion, - DockerVersion, - HassioVersion, - PyPiVersion, - HaIoVersion, - ) beta = config.get(CONF_BETA) image = config.get(CONF_IMAGE) diff --git a/homeassistant/components/vesync/.translations/da.json b/homeassistant/components/vesync/.translations/da.json index 43e56328f99..f2be5792f33 100644 --- a/homeassistant/components/vesync/.translations/da.json +++ b/homeassistant/components/vesync/.translations/da.json @@ -10,7 +10,7 @@ "user": { "data": { "password": "Adgangskode", - "username": "Email adresse" + "username": "Emailadresse" }, "title": "Indtast brugernavn og adgangskode" } diff --git a/homeassistant/components/vesync/.translations/lb.json b/homeassistant/components/vesync/.translations/lb.json index cfccd8b1dbb..0825bd0805d 100644 --- a/homeassistant/components/vesync/.translations/lb.json +++ b/homeassistant/components/vesync/.translations/lb.json @@ -12,7 +12,7 @@ "password": "Passwuert", "username": "E-Mail Adresse" }, - "title": "Benotznumm a Passwuert aginn" + "title": "Benotzernumm a Passwuert aginn" } }, "title": "VeSync" diff --git a/homeassistant/components/vesync/__init__.py b/homeassistant/components/vesync/__init__.py index 9ed71dbc5ee..0f905b8d7ef 100644 --- a/homeassistant/components/vesync/__init__.py +++ b/homeassistant/components/vesync/__init__.py @@ -1,20 +1,23 @@ """Etekcity VeSync integration.""" import logging -import voluptuous as vol + from pyvesync import VeSync -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD +import voluptuous as vol + +from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.config_entries import SOURCE_IMPORT + from .common import async_process_devices from .config_flow import configured_instances from .const import ( DOMAIN, - VS_DISPATCHERS, - VS_DISCOVERY, - VS_SWITCHES, SERVICE_UPDATE_DEVS, + VS_DISCOVERY, + VS_DISPATCHERS, VS_MANAGER, + VS_SWITCHES, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/vesync/common.py b/homeassistant/components/vesync/common.py index 361b3913283..d2ffa5281e9 100644 --- a/homeassistant/components/vesync/common.py +++ b/homeassistant/components/vesync/common.py @@ -1,6 +1,8 @@ """Common utilities for VeSync Component.""" import logging + from homeassistant.helpers.entity import ToggleEntity + from .const import VS_SWITCHES _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/vesync/config_flow.py b/homeassistant/components/vesync/config_flow.py index 168a3568392..8b0e8ae6781 100644 --- a/homeassistant/components/vesync/config_flow.py +++ b/homeassistant/components/vesync/config_flow.py @@ -1,11 +1,14 @@ """Config flow utilities.""" -import logging from collections import OrderedDict -import voluptuous as vol +import logging + from pyvesync import VeSync +import voluptuous as vol + from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD + from .const import DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/vesync/switch.py b/homeassistant/components/vesync/switch.py index 5ca76a77254..6ab5c0c4368 100644 --- a/homeassistant/components/vesync/switch.py +++ b/homeassistant/components/vesync/switch.py @@ -1,10 +1,12 @@ """Support for Etekcity VeSync switches.""" import logging -from homeassistant.core import callback + from homeassistant.components.switch import SwitchDevice +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .const import VS_DISCOVERY, VS_DISPATCHERS, VS_SWITCHES, DOMAIN + from .common import VeSyncDevice +from .const import DOMAIN, VS_DISCOVERY, VS_DISPATCHERS, VS_SWITCHES _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/vicare/__init__.py b/homeassistant/components/vicare/__init__.py index e091ff99970..282e234811a 100644 --- a/homeassistant/components/vicare/__init__.py +++ b/homeassistant/components/vicare/__init__.py @@ -2,15 +2,14 @@ import enum import logging -import voluptuous as vol - from PyViCare.PyViCareDevice import Device from PyViCare.PyViCareGazBoiler import GazBoiler from PyViCare.PyViCareHeatPump import HeatPump +import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_NAME +from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/vicare/climate.py b/homeassistant/components/vicare/climate.py index 7e330383b30..1b101cc7612 100644 --- a/homeassistant/components/vicare/climate.py +++ b/homeassistant/components/vicare/climate.py @@ -1,27 +1,29 @@ """Viessmann ViCare climate device.""" import logging + import requests -import simplejson from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( - SUPPORT_PRESET_MODE, - SUPPORT_TARGET_TEMPERATURE, - PRESET_ECO, - PRESET_COMFORT, - HVAC_MODE_OFF, - HVAC_MODE_HEAT, - HVAC_MODE_AUTO, CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, + HVAC_MODE_AUTO, + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + PRESET_COMFORT, + PRESET_ECO, + SUPPORT_PRESET_MODE, + SUPPORT_TARGET_TEMPERATURE, ) -from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE, PRECISION_WHOLE +from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, TEMP_CELSIUS -from . import DOMAIN as VICARE_DOMAIN -from . import VICARE_API -from . import VICARE_NAME -from . import VICARE_HEATING_TYPE -from . import HeatingType +from . import ( + DOMAIN as VICARE_DOMAIN, + VICARE_API, + VICARE_HEATING_TYPE, + VICARE_NAME, + HeatingType, +) _LOGGER = logging.getLogger(__name__) @@ -169,7 +171,7 @@ class ViCareClimate(ClimateDevice): ] = self._api.getReturnTemperature() except requests.exceptions.ConnectionError: _LOGGER.error("Unable to retrieve data from ViCare server") - except simplejson.errors.JSONDecodeError: + except ValueError: _LOGGER.error("Unable to decode data from ViCare server") @property diff --git a/homeassistant/components/vicare/water_heater.py b/homeassistant/components/vicare/water_heater.py index 1f56c46dc1c..f31e4f65170 100644 --- a/homeassistant/components/vicare/water_heater.py +++ b/homeassistant/components/vicare/water_heater.py @@ -1,18 +1,15 @@ """Viessmann ViCare water_heater device.""" import logging + import requests -import simplejson from homeassistant.components.water_heater import ( SUPPORT_TARGET_TEMPERATURE, WaterHeaterDevice, ) -from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE, PRECISION_WHOLE +from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, TEMP_CELSIUS -from . import DOMAIN as VICARE_DOMAIN -from . import VICARE_API -from . import VICARE_NAME -from . import VICARE_HEATING_TYPE +from . import DOMAIN as VICARE_DOMAIN, VICARE_API, VICARE_HEATING_TYPE, VICARE_NAME _LOGGER = logging.getLogger(__name__) @@ -93,7 +90,7 @@ class ViCareWater(WaterHeaterDevice): self._current_mode = self._api.getActiveMode() except requests.exceptions.ConnectionError: _LOGGER.error("Unable to retrieve data from ViCare server") - except simplejson.errors.JSONDecodeError: + except ValueError: _LOGGER.error("Unable to decode data from ViCare server") @property diff --git a/homeassistant/components/vivotek/camera.py b/homeassistant/components/vivotek/camera.py index c39a9b495bd..f4a195f5b0c 100644 --- a/homeassistant/components/vivotek/camera.py +++ b/homeassistant/components/vivotek/camera.py @@ -2,29 +2,33 @@ import logging -import voluptuous as vol from libpyvivotek import VivotekCamera +import voluptuous as vol +from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_STREAM, Camera from homeassistant.const import ( + CONF_AUTHENTICATION, CONF_IP_ADDRESS, CONF_NAME, CONF_PASSWORD, CONF_SSL, CONF_USERNAME, CONF_VERIFY_SSL, + HTTP_BASIC_AUTHENTICATION, + HTTP_DIGEST_AUTHENTICATION, ) -from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_STREAM, Camera from homeassistant.helpers import config_validation as cv _LOGGER = logging.getLogger(__name__) CONF_FRAMERATE = "framerate" - +CONF_SECURITY_LEVEL = "security_level" CONF_STREAM_PATH = "stream_path" DEFAULT_CAMERA_BRAND = "Vivotek" DEFAULT_NAME = "Vivotek Camera" DEFAULT_EVENT_0_KEY = "event_i0_enable" +DEFAULT_SECURITY_LEVEL = "admin" DEFAULT_STREAM_SOURCE = "live.sdp" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -33,9 +37,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_USERNAME): cv.string, + vol.Optional(CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION): vol.In( + [HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION] + ), vol.Optional(CONF_SSL, default=False): cv.boolean, vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, vol.Optional(CONF_FRAMERATE, default=2): cv.positive_int, + vol.Optional(CONF_SECURITY_LEVEL, default=DEFAULT_SECURITY_LEVEL): cv.string, vol.Optional(CONF_STREAM_PATH, default=DEFAULT_STREAM_SOURCE): cv.string, } ) @@ -52,6 +60,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): verify_ssl=config[CONF_VERIFY_SSL], usr=config[CONF_USERNAME], pwd=config[CONF_PASSWORD], + digest_auth=config[CONF_AUTHENTICATION] == HTTP_DIGEST_AUTHENTICATION, + sec_lvl=config[CONF_SECURITY_LEVEL], ), stream_source=f"rtsp://{creds}@{config[CONF_IP_ADDRESS]}:554/{config[CONF_STREAM_PATH]}", ) diff --git a/homeassistant/components/vivotek/manifest.json b/homeassistant/components/vivotek/manifest.json index ff498991127..afd7535aa0f 100644 --- a/homeassistant/components/vivotek/manifest.json +++ b/homeassistant/components/vivotek/manifest.json @@ -3,7 +3,7 @@ "name": "Vivotek", "documentation": "https://www.home-assistant.io/integrations/vivotek", "requirements": [ - "libpyvivotek==0.2.2" + "libpyvivotek==0.4.0" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py index f64fd2ca531..94601216f23 100644 --- a/homeassistant/components/vizio/media_player.py +++ b/homeassistant/components/vizio/media_player.py @@ -2,11 +2,12 @@ from datetime import timedelta import logging -import voluptuous as vol from pyvizio import Vizio +from requests.packages import urllib3 +import voluptuous as vol from homeassistant import util -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, @@ -110,8 +111,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return if config[CONF_SUPPRESS_WARNING]: - from requests.packages import urllib3 - _LOGGER.warning( "InsecureRequestWarning is disabled " "because of Vizio platform configuration" diff --git a/homeassistant/components/vlc/media_player.py b/homeassistant/components/vlc/media_player.py index 30b316cb4e8..c7a3d49fabc 100644 --- a/homeassistant/components/vlc/media_player.py +++ b/homeassistant/components/vlc/media_player.py @@ -1,10 +1,10 @@ """Provide functionality to interact with vlc devices on the network.""" import logging -import voluptuous as vol import vlc +import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, SUPPORT_PAUSE, @@ -18,7 +18,6 @@ from homeassistant.const import CONF_NAME, STATE_IDLE, STATE_PAUSED, STATE_PLAYI import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util - _LOGGER = logging.getLogger(__name__) CONF_ARGUMENTS = "arguments" diff --git a/homeassistant/components/vlc_telnet/media_player.py b/homeassistant/components/vlc_telnet/media_player.py index 1adb4eaebb3..45b0971ad9f 100644 --- a/homeassistant/components/vlc_telnet/media_player.py +++ b/homeassistant/components/vlc_telnet/media_player.py @@ -1,33 +1,33 @@ """Provide functionality to interact with the vlc telnet interface.""" import logging + +from python_telnet_vlc import ConnectionError as ConnErr, VLCTelnet import voluptuous as vol -from python_telnet_vlc import VLCTelnet, ConnectionError as ConnErr - -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, + SUPPORT_CLEAR_PLAYLIST, + SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, + SUPPORT_PREVIOUS_TRACK, + SUPPORT_SEEK, + SUPPORT_SHUFFLE_SET, SUPPORT_STOP, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - SUPPORT_PREVIOUS_TRACK, - SUPPORT_SEEK, - SUPPORT_NEXT_TRACK, - SUPPORT_CLEAR_PLAYLIST, - SUPPORT_SHUFFLE_SET, ) from homeassistant.const import ( + CONF_HOST, CONF_NAME, + CONF_PASSWORD, + CONF_PORT, STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_UNAVAILABLE, - CONF_HOST, - CONF_PORT, - CONF_PASSWORD, ) import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/volkszaehler/sensor.py b/homeassistant/components/volkszaehler/sensor.py index 6a0815a9b14..a418780c165 100644 --- a/homeassistant/components/volkszaehler/sensor.py +++ b/homeassistant/components/volkszaehler/sensor.py @@ -2,16 +2,18 @@ from datetime import timedelta import logging +from volkszaehler import Volkszaehler +from volkszaehler.exceptions import VolkszaehlerApiConnectionError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_HOST, + CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_PORT, - CONF_MONITORED_CONDITIONS, - POWER_WATT, ENERGY_WATT_HOUR, + POWER_WATT, ) from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -51,7 +53,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Volkszaehler sensors.""" - from volkszaehler import Volkszaehler host = config[CONF_HOST] name = config[CONF_NAME] @@ -130,7 +131,6 @@ class VolkszaehlerData: @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self): """Get the latest data from the Volkszaehler REST API.""" - from volkszaehler.exceptions import VolkszaehlerApiConnectionError try: await self.api.get_data() diff --git a/homeassistant/components/volumio/media_player.py b/homeassistant/components/volumio/media_player.py index 7c13488c3f5..f62a74345b1 100644 --- a/homeassistant/components/volumio/media_player.py +++ b/homeassistant/components/volumio/media_player.py @@ -14,22 +14,21 @@ import socket import aiohttp import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, SUPPORT_CLEAR_PLAYLIST, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, - SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_SELECT_SOURCE, + SUPPORT_SHUFFLE_SET, SUPPORT_STOP, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, - SUPPORT_SHUFFLE_SET, ) from homeassistant.const import ( CONF_HOST, @@ -61,7 +60,6 @@ SUPPORT_VOLUMIO = ( | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK - | SUPPORT_PLAY_MEDIA | SUPPORT_STOP | SUPPORT_PLAY | SUPPORT_VOLUME_STEP diff --git a/homeassistant/components/volvooncall/__init__.py b/homeassistant/components/volvooncall/__init__.py index c41c72020c4..c621a12943b 100644 --- a/homeassistant/components/volvooncall/__init__.py +++ b/homeassistant/components/volvooncall/__init__.py @@ -1,10 +1,10 @@ """Support for Volvo On Call.""" -import logging from datetime import timedelta +import logging import voluptuous as vol +from volvooncall import Connection -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( CONF_NAME, CONF_PASSWORD, @@ -14,6 +14,7 @@ from homeassistant.const import ( ) from homeassistant.helpers import discovery from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -115,8 +116,6 @@ async def async_setup(hass, config): """Set up the Volvo On Call component.""" session = async_get_clientsession(hass) - from volvooncall import Connection - connection = Connection( session=session, username=config[DOMAIN].get(CONF_USERNAME), diff --git a/homeassistant/components/vultr/__init__.py b/homeassistant/components/vultr/__init__.py index 50e77c01c43..9b26c4a75b3 100644 --- a/homeassistant/components/vultr/__init__.py +++ b/homeassistant/components/vultr/__init__.py @@ -1,12 +1,13 @@ """Support for Vultr.""" -import logging from datetime import timedelta +import logging import voluptuous as vol +from vultr import Vultr as VultrAPI from homeassistant.const import CONF_API_KEY -from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv +from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -69,7 +70,6 @@ class Vultr: def __init__(self, api_key): """Initialize the Vultr connection.""" - from vultr import Vultr as VultrAPI self._api_key = api_key self.data = None diff --git a/homeassistant/components/w800rf32/__init__.py b/homeassistant/components/w800rf32/__init__.py index 805cca47023..bbd3fdac23d 100644 --- a/homeassistant/components/w800rf32/__init__.py +++ b/homeassistant/components/w800rf32/__init__.py @@ -1,15 +1,14 @@ """Support for w800rf32 devices.""" import logging -import voluptuous as vol import W800rf32 as w800 +import voluptuous as vol from homeassistant.const import ( CONF_DEVICE, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, ) - import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import dispatcher_send diff --git a/homeassistant/components/w800rf32/binary_sensor.py b/homeassistant/components/w800rf32/binary_sensor.py index e08111da8ba..9c83dbce804 100644 --- a/homeassistant/components/w800rf32/binary_sensor.py +++ b/homeassistant/components/w800rf32/binary_sensor.py @@ -1,8 +1,8 @@ """Support for w800rf32 binary sensors.""" import logging -import voluptuous as vol import W800rf32 as w800 +import voluptuous as vol from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA, diff --git a/homeassistant/components/wake_on_lan/__init__.py b/homeassistant/components/wake_on_lan/__init__.py index b4aad4925b9..d5b8f92a9bc 100644 --- a/homeassistant/components/wake_on_lan/__init__.py +++ b/homeassistant/components/wake_on_lan/__init__.py @@ -5,15 +5,13 @@ import logging import voluptuous as vol import wakeonlan -from homeassistant.const import CONF_MAC +from homeassistant.const import CONF_BROADCAST_ADDRESS, CONF_MAC import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) DOMAIN = "wake_on_lan" -CONF_BROADCAST_ADDRESS = "broadcast_address" - SERVICE_SEND_MAGIC_PACKET = "send_magic_packet" WAKE_ON_LAN_SEND_MAGIC_PACKET_SCHEMA = vol.Schema( diff --git a/homeassistant/components/wake_on_lan/switch.py b/homeassistant/components/wake_on_lan/switch.py index 01f69679829..8200a0309fa 100644 --- a/homeassistant/components/wake_on_lan/switch.py +++ b/homeassistant/components/wake_on_lan/switch.py @@ -7,14 +7,12 @@ import voluptuous as vol import wakeonlan from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice -from homeassistant.const import CONF_HOST, CONF_NAME +from homeassistant.const import CONF_BROADCAST_ADDRESS, CONF_HOST, CONF_MAC, CONF_NAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.script import Script _LOGGER = logging.getLogger(__name__) -CONF_BROADCAST_ADDRESS = "broadcast_address" -CONF_MAC_ADDRESS = "mac_address" CONF_OFF_ACTION = "turn_off" DEFAULT_NAME = "Wake on LAN" @@ -22,7 +20,7 @@ DEFAULT_PING_TIMEOUT = 1 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { - vol.Required(CONF_MAC_ADDRESS): cv.string, + vol.Required(CONF_MAC): cv.string, vol.Optional(CONF_BROADCAST_ADDRESS): cv.string, vol.Optional(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, @@ -35,16 +33,16 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up a wake on lan switch.""" broadcast_address = config.get(CONF_BROADCAST_ADDRESS) host = config.get(CONF_HOST) - mac_address = config.get(CONF_MAC_ADDRESS) - name = config.get(CONF_NAME) + mac_address = config[CONF_MAC] + name = config[CONF_NAME] off_action = config.get(CONF_OFF_ACTION) add_entities( - [WOLSwitch(hass, name, host, mac_address, off_action, broadcast_address)], True + [WolSwitch(hass, name, host, mac_address, off_action, broadcast_address)], True ) -class WOLSwitch(SwitchDevice): +class WolSwitch(SwitchDevice): """Representation of a wake on lan switch.""" def __init__(self, hass, name, host, mac_address, off_action, broadcast_address): diff --git a/homeassistant/components/waqi/sensor.py b/homeassistant/components/waqi/sensor.py index b53723a29b6..8f16c216b37 100644 --- a/homeassistant/components/waqi/sensor.py +++ b/homeassistant/components/waqi/sensor.py @@ -1,21 +1,21 @@ """Support for the World Air Quality Index service.""" import asyncio -import logging from datetime import timedelta +import logging import aiohttp import voluptuous as vol from waqiasync import WaqiClient -from homeassistant.exceptions import PlatformNotReady -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( ATTR_ATTRIBUTION, - ATTR_TIME, ATTR_TEMPERATURE, + ATTR_TIME, CONF_TOKEN, ) +from homeassistant.exceptions import PlatformNotReady 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 diff --git a/homeassistant/components/water_heater/__init__.py b/homeassistant/components/water_heater/__init__.py index 6e7b918c289..c5ba009717c 100644 --- a/homeassistant/components/water_heater/__init__.py +++ b/homeassistant/components/water_heater/__init__.py @@ -1,32 +1,31 @@ """Support for water heater devices.""" from datetime import timedelta -import logging import functools as ft +import logging import voluptuous as vol -from homeassistant.helpers.temperature import display_temp as show_temp -from homeassistant.util.temperature import convert as convert_temperature -from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.entity import Entity +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_TEMPERATURE, + PRECISION_TENTHS, + PRECISION_WHOLE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) -import homeassistant.helpers.config_validation as cv -from homeassistant.const import ( - ATTR_ENTITY_ID, - ATTR_TEMPERATURE, - SERVICE_TURN_ON, - SERVICE_TURN_OFF, - STATE_ON, - STATE_OFF, - TEMP_CELSIUS, - PRECISION_WHOLE, - PRECISION_TENTHS, - TEMP_FAHRENHEIT, -) - +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.util.temperature import convert as convert_temperature # mypy: allow-untyped-defs, no-check-untyped-defs diff --git a/homeassistant/components/water_heater/services.yaml b/homeassistant/components/water_heater/services.yaml index 72a3f909fbb..7a26e5bc0d4 100644 --- a/homeassistant/components/water_heater/services.yaml +++ b/homeassistant/components/water_heater/services.yaml @@ -29,23 +29,3 @@ set_operation_mode: operation_mode: description: New value of operation mode. example: eco - -econet_add_vacation: - description: Add a vacation to your water heater. - fields: - entity_id: - description: Name(s) of entities to change. - example: 'water_heater.econet' - start_date: - description: The timestamp of when the vacation should start. (Optional, defaults to now) - example: 1513186320 - end_date: - description: The timestamp of when the vacation should end. - example: 1513445520 - -econet_delete_vacation: - description: Delete your existing vacation from your water heater. - fields: - entity_id: - description: Name(s) of entities to change. - example: 'water_heater.econet' \ No newline at end of file diff --git a/homeassistant/components/waterfurnace/__init__.py b/homeassistant/components/waterfurnace/__init__.py index b6eb22c89ae..942ab8a14ac 100644 --- a/homeassistant/components/waterfurnace/__init__.py +++ b/homeassistant/components/waterfurnace/__init__.py @@ -1,16 +1,15 @@ """Support for Waterfurnaces.""" from datetime import timedelta import logging -import time import threading +import time import voluptuous as vol from waterfurnace.waterfurnace import WaterFurnace, WFCredentialError, WFException -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers import discovery +from homeassistant.helpers import config_validation as cv, discovery _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/watson_tts/tts.py b/homeassistant/components/watson_tts/tts.py index 40ebd768a31..74803464484 100644 --- a/homeassistant/components/watson_tts/tts.py +++ b/homeassistant/components/watson_tts/tts.py @@ -1,6 +1,8 @@ """Support for IBM Watson TTS integration.""" import logging +from ibm_cloud_sdk_core.authenticators import IAMAuthenticator +from ibm_watson import TextToSpeechV1 import voluptuous as vol from homeassistant.components.tts import PLATFORM_SCHEMA, Provider @@ -92,8 +94,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def get_engine(hass, config, discovery_info=None): """Set up IBM Watson TTS component.""" - from ibm_watson import TextToSpeechV1 - from ibm_cloud_sdk_core.authenticators import IAMAuthenticator authenticator = IAMAuthenticator(config[CONF_APIKEY]) service = TextToSpeechV1(authenticator) diff --git a/homeassistant/components/waze_travel_time/manifest.json b/homeassistant/components/waze_travel_time/manifest.json index 85bcc19032e..32083ca8ca8 100644 --- a/homeassistant/components/waze_travel_time/manifest.json +++ b/homeassistant/components/waze_travel_time/manifest.json @@ -3,7 +3,7 @@ "name": "Waze travel time", "documentation": "https://www.home-assistant.io/integrations/waze_travel_time", "requirements": [ - "WazeRouteCalculator==0.10" + "WazeRouteCalculator==0.12" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/waze_travel_time/sensor.py b/homeassistant/components/waze_travel_time/sensor.py index 4392a20d801..b9ca64c0970 100644 --- a/homeassistant/components/waze_travel_time/sensor.py +++ b/homeassistant/components/waze_travel_time/sensor.py @@ -38,10 +38,16 @@ 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" @@ -65,6 +71,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( VEHICLE_TYPES ), vol.Optional(CONF_UNITS): vol.In(UNITS), + vol.Optional( + CONF_AVOID_TOLL_ROADS, default=DEFAULT_AVOID_TOLL_ROADS + ): cv.boolean, + vol.Optional( + CONF_AVOID_SUBSCRIPTION_ROADS, default=DEFAULT_AVOID_SUBSCRIPTION_ROADS + ): cv.boolean, + vol.Optional(CONF_AVOID_FERRIES, default=DEFAULT_AVOID_FERRIES): cv.boolean, } ) @@ -79,10 +92,23 @@ def setup_platform(hass, config, add_entities, discovery_info=None): excl_filter = config.get(CONF_EXCL_FILTER) realtime = config.get(CONF_REALTIME) vehicle_type = config.get(CONF_VEHICLE_TYPE) + avoid_toll_roads = config.get(CONF_AVOID_TOLL_ROADS) + avoid_subscription_roads = config.get(CONF_AVOID_SUBSCRIPTION_ROADS) + avoid_ferries = config.get(CONF_AVOID_FERRIES) units = config.get(CONF_UNITS, hass.config.units.name) data = WazeTravelTimeData( - None, None, region, incl_filter, excl_filter, realtime, units, vehicle_type + None, + None, + region, + incl_filter, + excl_filter, + realtime, + units, + vehicle_type, + avoid_toll_roads, + avoid_subscription_roads, + avoid_ferries, ) sensor = WazeTravelTime(name, origin, destination, data) @@ -236,6 +262,9 @@ class WazeTravelTimeData: realtime, units, vehicle_type, + avoid_toll_roads, + avoid_subscription_roads, + avoid_ferries, ): """Set up WazeRouteCalculator.""" @@ -251,6 +280,9 @@ class WazeTravelTimeData: 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": @@ -268,7 +300,9 @@ class WazeTravelTimeData: self.destination, self.region, self.vehicle_type, - log_lvl=logging.DEBUG, + self.avoid_toll_roads, + self.avoid_subscription_roads, + self.avoid_ferries, ) routes = params.calc_all_routes_info(real_time=self.realtime) @@ -286,7 +320,7 @@ class WazeTravelTimeData: if self.exclude.lower() not in k.lower() } - route = sorted(routes, key=(lambda key: routes[key][0]))[0] + route = list(routes)[0] self.duration, distance = routes[route] diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index bdeedd4cd6b..01ca8ed6790 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -11,7 +11,6 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.temperature import display_temp as show_temp - # mypy: allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/webhook/__init__.py b/homeassistant/components/webhook/__init__.py index 5a41bfa9851..217baf42f3a 100644 --- a/homeassistant/components/webhook/__init__.py +++ b/homeassistant/components/webhook/__init__.py @@ -1,14 +1,14 @@ """Webhooks for Home Assistant.""" import logging +import secrets -from aiohttp.web import Response, Request +from aiohttp.web import Request, Response import voluptuous as vol -from homeassistant.core import callback -from homeassistant.loader import bind_hass -from homeassistant.auth.util import generate_secret from homeassistant.components import websocket_api from homeassistant.components.http.view import HomeAssistantView +from homeassistant.core import callback +from homeassistant.loader import bind_hass _LOGGER = logging.getLogger(__name__) @@ -46,7 +46,7 @@ def async_unregister(hass, webhook_id): @callback def async_generate_id(): """Generate a webhook_id.""" - return generate_secret(entropy=32) + return secrets.token_hex(32) @callback diff --git a/homeassistant/components/weblink/__init__.py b/homeassistant/components/weblink/__init__.py index a6ee72fa147..be6814da30c 100644 --- a/homeassistant/components/weblink/__init__.py +++ b/homeassistant/components/weblink/__init__.py @@ -3,10 +3,10 @@ import logging import voluptuous as vol -from homeassistant.const import CONF_NAME, CONF_ICON, CONF_URL +from homeassistant.const import CONF_ICON, CONF_NAME, CONF_URL +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import slugify -import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/webostv/__init__.py b/homeassistant/components/webostv/__init__.py index f0b3c2c5f7e..b34dba3ad94 100644 --- a/homeassistant/components/webostv/__init__.py +++ b/homeassistant/components/webostv/__init__.py @@ -1 +1,152 @@ """Support for WebOS TV.""" +import asyncio +import logging + +from aiopylgtv import PyLGTVCmdException, PyLGTVPairException, WebOsClient +import voluptuous as vol +from websockets.exceptions import ConnectionClosed + +from homeassistant.const import ( + CONF_CUSTOMIZE, + CONF_HOST, + CONF_ICON, + CONF_NAME, + EVENT_HOMEASSISTANT_STOP, +) +import homeassistant.helpers.config_validation as cv + +DOMAIN = "webostv" + +CONF_SOURCES = "sources" +CONF_ON_ACTION = "turn_on_action" +CONF_STANDBY_CONNECTION = "standby_connection" +DEFAULT_NAME = "LG webOS Smart TV" +WEBOSTV_CONFIG_FILE = "webostv.conf" + +CUSTOMIZE_SCHEMA = vol.Schema( + {vol.Optional(CONF_SOURCES, default=[]): vol.All(cv.ensure_list, [cv.string])} +) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.All( + cv.ensure_list, + [ + vol.Schema( + { + vol.Optional(CONF_CUSTOMIZE, default={}): CUSTOMIZE_SCHEMA, + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional( + CONF_STANDBY_CONNECTION, default=False + ): cv.boolean, + vol.Optional(CONF_ICON): cv.string, + } + ) + ], + ) + }, + extra=vol.ALLOW_EXTRA, +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass, config): + """Set up the LG WebOS TV platform.""" + hass.data[DOMAIN] = {} + + tasks = [async_setup_tv(hass, config, conf) for conf in config[DOMAIN]] + if tasks: + await asyncio.gather(*tasks) + + return True + + +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) + standby_connection = conf[CONF_STANDBY_CONNECTION] + + client = WebOsClient(host, config_file, standby_connection=standby_connection) + hass.data[DOMAIN][host] = {"client": client} + + if client.is_registered(): + await async_setup_tv_finalize(hass, config, conf, client) + else: + _LOGGER.warning("LG webOS TV %s needs to be paired", host) + await async_request_configuration(hass, config, conf, client) + + +async def async_connect(client): + """Attempt a connection, but fail gracefully if tv is off for example.""" + try: + await client.connect() + except ( + OSError, + ConnectionClosed, + ConnectionRefusedError, + asyncio.TimeoutError, + asyncio.CancelledError, + PyLGTVPairException, + PyLGTVCmdException, + ): + pass + + +async def async_setup_tv_finalize(hass, config, conf, client): + """Make initial connection attempt and call platform setup.""" + + async def async_on_stop(event): + """Unregister callbacks and disconnect.""" + client.clear_state_update_callbacks() + await client.disconnect() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_on_stop) + + await async_connect(client) + hass.async_create_task( + hass.helpers.discovery.async_load_platform("media_player", DOMAIN, conf, config) + ) + hass.async_create_task( + hass.helpers.discovery.async_load_platform("notify", DOMAIN, conf, config) + ) + + +async def async_request_configuration(hass, config, conf, client): + """Request configuration steps from the user.""" + host = conf.get(CONF_HOST) + name = conf.get(CONF_NAME) + configurator = hass.components.configurator + + async def lgtv_configuration_callback(data): + """Handle actions when configuration callback is called.""" + try: + await client.connect() + except PyLGTVPairException: + _LOGGER.warning("Connected to LG webOS TV %s but not paired", host) + return + except ( + OSError, + ConnectionClosed, + ConnectionRefusedError, + asyncio.TimeoutError, + asyncio.CancelledError, + PyLGTVCmdException, + ): + _LOGGER.error("Unable to connect to host %s", host) + return + + await async_setup_tv_finalize(hass, config, conf, client) + configurator.async_request_done(request_id) + + request_id = configurator.async_request_config( + name, + lgtv_configuration_callback, + description="Click start and accept the pairing request on your TV.", + description_image="/static/images/config_webos.png", + submit_caption="Start pairing request", + ) diff --git a/homeassistant/components/webostv/manifest.json b/homeassistant/components/webostv/manifest.json index dcf908cd603..016f14f0f94 100644 --- a/homeassistant/components/webostv/manifest.json +++ b/homeassistant/components/webostv/manifest.json @@ -3,8 +3,7 @@ "name": "Webostv", "documentation": "https://www.home-assistant.io/integrations/webostv", "requirements": [ - "pylgtv==0.1.9", - "websockets==6.0" + "aiopylgtv==0.2.4" ], "dependencies": ["configurator"], "codeowners": [] diff --git a/homeassistant/components/webostv/media_player.py b/homeassistant/components/webostv/media_player.py index 913d193845f..fd47cf0a114 100644 --- a/homeassistant/components/webostv/media_player.py +++ b/homeassistant/components/webostv/media_player.py @@ -1,14 +1,14 @@ """Support for interface with an LG webOS Smart TV.""" import asyncio from datetime import timedelta +from functools import wraps import logging -from urllib.parse import urlparse -from typing import Dict -import voluptuous as vol +from aiopylgtv import PyLGTVCmdException, PyLGTVPairException +from websockets.exceptions import ConnectionClosed from homeassistant import util -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, SUPPORT_NEXT_TRACK, @@ -25,27 +25,21 @@ from homeassistant.components.media_player.const import ( ) from homeassistant.const import ( CONF_CUSTOMIZE, - CONF_FILENAME, CONF_HOST, CONF_NAME, - CONF_TIMEOUT, STATE_OFF, STATE_PAUSED, STATE_PLAYING, ) -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.script import Script -_CONFIGURING: Dict[str, str] = {} +from . import CONF_ON_ACTION, CONF_SOURCES, DOMAIN + _LOGGER = logging.getLogger(__name__) -CONF_SOURCES = "sources" -CONF_ON_ACTION = "turn_on_action" -DEFAULT_NAME = "LG webOS Smart TV" LIVETV_APP_ID = "com.webos.app.livetv" -WEBOSTV_CONFIG_FILE = "webostv.conf" SUPPORT_WEBOSTV = ( SUPPORT_TURN_OFF @@ -63,135 +57,65 @@ SUPPORT_WEBOSTV = ( MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1) -CUSTOMIZE_SCHEMA = vol.Schema( - {vol.Optional(CONF_SOURCES): vol.All(cv.ensure_list, [cv.string])} -) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_CUSTOMIZE, default={}): CUSTOMIZE_SCHEMA, - vol.Optional(CONF_FILENAME, default=WEBOSTV_CONFIG_FILE): cv.string, - vol.Optional(CONF_HOST): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, - vol.Optional(CONF_TIMEOUT, default=8): cv.positive_int, - } -) - - -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 LG WebOS TV platform.""" - if discovery_info is not None: - host = urlparse(discovery_info[1]).hostname - else: - host = config.get(CONF_HOST) - if host is None: - _LOGGER.error("No TV found in configuration file or with discovery") - return False - - # Only act if we are not already configuring this host - if host in _CONFIGURING: + if discovery_info is None: return - name = config.get(CONF_NAME) - customize = config.get(CONF_CUSTOMIZE) - timeout = config.get(CONF_TIMEOUT) - turn_on_action = config.get(CONF_ON_ACTION) + host = discovery_info[CONF_HOST] + name = discovery_info[CONF_NAME] + customize = discovery_info[CONF_CUSTOMIZE] + turn_on_action = discovery_info.get(CONF_ON_ACTION) - config = hass.config.path(config.get(CONF_FILENAME)) + client = hass.data[DOMAIN][host]["client"] + on_script = Script(hass, turn_on_action) if turn_on_action else None - setup_tv(host, name, customize, config, timeout, hass, add_entities, turn_on_action) + entity = LgWebOSMediaPlayerEntity(client, name, customize, on_script) + + async_add_entities([entity], update_before_add=False) -def setup_tv( - host, name, customize, config, timeout, hass, add_entities, turn_on_action -): - """Set up a LG WebOS TV based on host parameter.""" - from pylgtv import WebOsClient - from pylgtv import PyLGTVPairException - from websockets.exceptions import ConnectionClosed +def cmd(func): + """Catch command exceptions.""" - client = WebOsClient(host, config, timeout) - - if not client.is_registered(): - if host in _CONFIGURING: - # Try to pair. - try: - client.register() - except PyLGTVPairException: - _LOGGER.warning("Connected to LG webOS TV %s but not paired", host) - return - except (OSError, ConnectionClosed, asyncio.TimeoutError): - _LOGGER.error("Unable to connect to host %s", host) - return - else: - # Not registered, request configuration. - _LOGGER.warning("LG webOS TV %s needs to be paired", host) - request_configuration( - host, - name, - customize, - config, - timeout, - hass, - add_entities, - turn_on_action, + @wraps(func) + async def wrapper(obj, *args, **kwargs): + """Wrap all command methods.""" + try: + await func(obj, *args, **kwargs) + except ( + asyncio.TimeoutError, + asyncio.CancelledError, + PyLGTVCmdException, + ) as exc: + # If TV is off, we expect calls to fail. + if obj.state == STATE_OFF: + level = logging.INFO + else: + level = logging.ERROR + _LOGGER.log( + level, + "Error calling %s on entity %s: %r", + func.__name__, + obj.entity_id, + exc, ) - return - # If we came here and configuring this host, mark as done. - if client.is_registered() and host in _CONFIGURING: - request_id = _CONFIGURING.pop(host) - configurator = hass.components.configurator - configurator.request_done(request_id) - - add_entities( - [LgWebOSDevice(host, name, customize, config, timeout, hass, turn_on_action)], - True, - ) + return wrapper -def request_configuration( - host, name, customize, config, timeout, hass, add_entities, turn_on_action -): - """Request configuration steps from the user.""" - configurator = hass.components.configurator - - # We got an error if this method is called while we are configuring - if host in _CONFIGURING: - configurator.notify_errors( - _CONFIGURING[host], "Failed to pair, please try again." - ) - return - - def lgtv_configuration_callback(data): - """Handle actions when configuration callback is called.""" - setup_tv( - host, name, customize, config, timeout, hass, add_entities, turn_on_action - ) - - _CONFIGURING[host] = configurator.request_config( - name, - lgtv_configuration_callback, - description="Click start and accept the pairing request on your TV.", - description_image="/static/images/config_webos.png", - submit_caption="Start pairing request", - ) - - -class LgWebOSDevice(MediaPlayerDevice): +class LgWebOSMediaPlayerEntity(MediaPlayerDevice): """Representation of a LG WebOS TV.""" - def __init__(self, host, name, customize, config, timeout, hass, on_action): + def __init__(self, client, name, customize, on_script=None): """Initialize the webos device.""" - from pylgtv import WebOsClient - - self._client = WebOsClient(host, config, timeout) - self._on_script = Script(hass, on_action) if on_action else None - self._customize = customize - + self._client = client self._name = name + self._customize = customize + self._on_script = on_script + # Assume that the TV is not muted self._muted = False # Assume that the TV is in Play mode @@ -202,65 +126,86 @@ class LgWebOSDevice(MediaPlayerDevice): self._state = None self._source_list = {} self._app_list = {} + self._input_list = {} self._channel = None self._last_icon = None - @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) - def update(self): - """Retrieve the latest data.""" - from websockets.exceptions import ConnectionClosed + async def async_added_to_hass(self): + """Connect and subscribe to state updates.""" + await self._client.register_state_update_callback( + self.async_handle_state_update + ) - try: - current_input = self._client.get_input() - if current_input is not None: - self._current_source_id = current_input - if self._state in (None, STATE_OFF): - self._state = STATE_PLAYING - else: - self._state = STATE_OFF - self._current_source = None - self._current_source_id = None - self._channel = None + # force state update if needed + if self._state is None: + await self.async_handle_state_update() - if self._state is not STATE_OFF: - self._muted = self._client.get_muted() - self._volume = self._client.get_volume() - self._channel = self._client.get_current_channel() + async def async_will_remove_from_hass(self): + """Call disconnect on removal.""" + self._client.unregister_state_update_callback(self.async_handle_state_update) - self._source_list = {} - self._app_list = {} - conf_sources = self._customize.get(CONF_SOURCES, []) + async def async_handle_state_update(self): + """Update state from WebOsClient.""" + self._current_source_id = self._client.current_appId + self._muted = self._client.muted + self._volume = self._client.volume + self._channel = self._client.current_channel + self._app_list = self._client.apps + self._input_list = self._client.inputs - for app in self._client.get_apps(): - self._app_list[app["id"]] = app - if app["id"] == self._current_source_id: - self._current_source = app["title"] - self._source_list[app["title"]] = app - elif ( - not conf_sources - or app["id"] in conf_sources - or any(word in app["title"] for word in conf_sources) - or any(word in app["id"] for word in conf_sources) - ): - self._source_list[app["title"]] = app - - for source in self._client.get_inputs(): - if source["id"] == self._current_source_id: - self._current_source = source["label"] - self._source_list[source["label"]] = source - elif ( - not conf_sources - or source["label"] in conf_sources - or any( - source["label"].find(word) != -1 for word in conf_sources - ) - ): - self._source_list[source["label"]] = source - except (OSError, ConnectionClosed, TypeError, asyncio.TimeoutError): + if self._current_source_id == "": self._state = STATE_OFF - self._current_source = None - self._current_source_id = None - self._channel = None + else: + self._state = STATE_PLAYING + + self.update_sources() + + self.async_schedule_update_ha_state(False) + + def update_sources(self): + """Update list of sources from current source, apps, inputs and configured list.""" + self._source_list = {} + conf_sources = self._customize[CONF_SOURCES] + + for app in self._app_list.values(): + if app["id"] == self._current_source_id: + self._current_source = app["title"] + self._source_list[app["title"]] = app + elif ( + not conf_sources + or app["id"] in conf_sources + or any(word in app["title"] for word in conf_sources) + or any(word in app["id"] for word in conf_sources) + ): + self._source_list[app["title"]] = app + + for source in self._input_list.values(): + if source["appId"] == self._current_source_id: + self._current_source = source["label"] + self._source_list[source["label"]] = source + elif ( + not conf_sources + or source["label"] in conf_sources + or any(source["label"].find(word) != -1 for word in conf_sources) + ): + self._source_list[source["label"]] = source + + @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) + async def async_update(self): + """Connect.""" + if not self._client.is_connected(): + try: + await self._client.connect() + except ( + OSError, + ConnectionClosed, + ConnectionRefusedError, + asyncio.TimeoutError, + asyncio.CancelledError, + PyLGTVPairException, + PyLGTVCmdException, + ): + pass @property def name(self): @@ -329,47 +274,54 @@ class LgWebOSDevice(MediaPlayerDevice): return SUPPORT_WEBOSTV | SUPPORT_TURN_ON return SUPPORT_WEBOSTV - def turn_off(self): + @cmd + async def async_turn_off(self): """Turn off media player.""" - from websockets.exceptions import ConnectionClosed + await self._client.power_off() - self._state = STATE_OFF - try: - self._client.power_off() - except (OSError, ConnectionClosed, TypeError, asyncio.TimeoutError): - pass - - def turn_on(self): + async def async_turn_on(self): """Turn on the media player.""" + connected = self._client.is_connected() if self._on_script: - self._on_script.run() + await self._on_script.async_run() - def volume_up(self): + # if connection was already active + # ensure is still alive + if connected: + await self._client.get_current_app() + + @cmd + async def async_volume_up(self): """Volume up the media player.""" - self._client.volume_up() + await self._client.volume_up() - def volume_down(self): + @cmd + async def async_volume_down(self): """Volume down media player.""" - self._client.volume_down() + await self._client.volume_down() - def set_volume_level(self, volume): + @cmd + async def async_set_volume_level(self, volume): """Set volume level, range 0..1.""" tv_volume = volume * 100 - self._client.set_volume(tv_volume) + await self._client.set_volume(tv_volume) - def mute_volume(self, mute): + @cmd + async def async_mute_volume(self, mute): """Send mute command.""" self._muted = mute - self._client.set_mute(mute) + await self._client.set_mute(mute) - def media_play_pause(self): + @cmd + async def async_media_play_pause(self): """Simulate play pause media player.""" if self._playing: - self.media_pause() + await self.media_pause() else: - self.media_play() + await self.media_play() - def select_source(self, source): + @cmd + async def async_select_source(self, source): """Select input source.""" source_dict = self._source_list.get(source) if source_dict is None: @@ -378,12 +330,13 @@ class LgWebOSDevice(MediaPlayerDevice): self._current_source_id = source_dict["id"] if source_dict.get("title"): self._current_source = source_dict["title"] - self._client.launch_app(source_dict["id"]) + await self._client.launch_app(source_dict["id"]) elif source_dict.get("label"): self._current_source = source_dict["label"] - self._client.set_input(source_dict["id"]) + await self._client.set_input(source_dict["id"]) - def play_media(self, media_type, media_id, **kwargs): + @cmd + 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) @@ -409,40 +362,47 @@ class LgWebOSDevice(MediaPlayerDevice): "Switching to channel <%s> with perfect match", perfect_match_channel_id, ) - self._client.set_channel(perfect_match_channel_id) + await self._client.set_channel(perfect_match_channel_id) elif partial_match_channel_id is not None: _LOGGER.info( "Switching to channel <%s> with partial match", partial_match_channel_id, ) - self._client.set_channel(partial_match_channel_id) + await self._client.set_channel(partial_match_channel_id) - return - - def media_play(self): + @cmd + async def async_media_play(self): """Send play command.""" self._playing = True self._state = STATE_PLAYING - self._client.play() + await self._client.play() - def media_pause(self): + @cmd + async def async_media_pause(self): """Send media pause command to media player.""" self._playing = False self._state = STATE_PAUSED - self._client.pause() + await self._client.pause() - def media_next_track(self): + @cmd + async def async_media_stop(self): + """Send stop command to media player.""" + await self._client.stop() + + @cmd + async def async_media_next_track(self): """Send next track command.""" current_input = self._client.get_input() if current_input == LIVETV_APP_ID: - self._client.channel_up() + await self._client.channel_up() else: - self._client.fast_forward() + await self._client.fast_forward() - def media_previous_track(self): + @cmd + async def async_media_previous_track(self): """Send the previous track command.""" current_input = self._client.get_input() if current_input == LIVETV_APP_ID: - self._client.channel_down() + await self._client.channel_down() else: - self._client.rewind() + await self._client.rewind() diff --git a/homeassistant/components/webostv/notify.py b/homeassistant/components/webostv/notify.py index f96c20d49aa..e75fafbfe23 100644 --- a/homeassistant/components/webostv/notify.py +++ b/homeassistant/components/webostv/notify.py @@ -1,48 +1,29 @@ """Support for LG WebOS TV notification service.""" +import asyncio import logging -import voluptuous as vol +from aiopylgtv import PyLGTVCmdException, PyLGTVPairException +from websockets.exceptions import ConnectionClosed -import homeassistant.helpers.config_validation as cv -from homeassistant.components.notify import ( - ATTR_DATA, - BaseNotificationService, - PLATFORM_SCHEMA, -) -from homeassistant.const import CONF_FILENAME, CONF_HOST, CONF_ICON +from homeassistant.components.notify import ATTR_DATA, BaseNotificationService +from homeassistant.const import CONF_HOST, CONF_ICON + +from . import DOMAIN _LOGGER = logging.getLogger(__name__) -WEBOSTV_CONFIG_FILE = "webostv.conf" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_FILENAME, default=WEBOSTV_CONFIG_FILE): cv.string, - vol.Optional(CONF_ICON): cv.string, - } -) - - -def get_service(hass, config, discovery_info=None): +async def async_get_service(hass, config, discovery_info=None): """Return the notify service.""" - from pylgtv import WebOsClient - from pylgtv import PyLGTVPairException - path = hass.config.path(config.get(CONF_FILENAME)) - client = WebOsClient(config.get(CONF_HOST), key_file_path=path, timeout_connect=8) + host = discovery_info.get(CONF_HOST) + icon_path = discovery_info.get(CONF_ICON) - if not client.is_registered(): - try: - client.register() - except PyLGTVPairException: - _LOGGER.error("Pairing with TV failed") - return None - except OSError: - _LOGGER.error("TV unreachable") - return None + client = hass.data[DOMAIN][host]["client"] - return LgWebOSNotificationService(client, config.get(CONF_ICON)) + svc = LgWebOSNotificationService(client, icon_path) + + return svc class LgWebOSNotificationService(BaseNotificationService): @@ -53,19 +34,27 @@ class LgWebOSNotificationService(BaseNotificationService): self._client = client self._icon_path = icon_path - def send_message(self, message="", **kwargs): + async def async_send_message(self, message="", **kwargs): """Send a message to the tv.""" - from pylgtv import PyLGTVPairException - try: + if not self._client.is_connected(): + await self._client.connect() + data = kwargs.get(ATTR_DATA) icon_path = ( data.get(CONF_ICON, self._icon_path) if data else self._icon_path ) - self._client.send_message(message, icon_path=icon_path) + await self._client.send_message(message, icon_path=icon_path) except PyLGTVPairException: _LOGGER.error("Pairing with TV failed") except FileNotFoundError: _LOGGER.error("Icon %s not found", icon_path) - except OSError: + except ( + OSError, + ConnectionClosed, + ConnectionRefusedError, + asyncio.TimeoutError, + asyncio.CancelledError, + PyLGTVCmdException, + ): _LOGGER.error("TV unreachable") diff --git a/homeassistant/components/websocket_api/__init__.py b/homeassistant/components/websocket_api/__init__.py index 1ec758ebd4d..2beb2aa2788 100644 --- a/homeassistant/components/websocket_api/__init__.py +++ b/homeassistant/components/websocket_api/__init__.py @@ -4,7 +4,6 @@ from homeassistant.loader import bind_hass from . import commands, connection, const, decorators, http, messages - # mypy: allow-untyped-calls, allow-untyped-defs DOMAIN = const.DOMAIN diff --git a/homeassistant/components/websocket_api/auth.py b/homeassistant/components/websocket_api/auth.py index 3971d39ee73..9e33ed74fd4 100644 --- a/homeassistant/components/websocket_api/auth.py +++ b/homeassistant/components/websocket_api/auth.py @@ -3,13 +3,12 @@ import voluptuous as vol from voluptuous.humanize import humanize_error from homeassistant.auth.models import RefreshToken, User -from homeassistant.components.http.ban import process_wrong_login, process_success_login +from homeassistant.components.http.ban import process_success_login, process_wrong_login from homeassistant.const import __version__ from .connection import ActiveConnection from .error import Disconnect - # mypy: allow-untyped-calls, allow-untyped-defs TYPE_AUTH = "auth" diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index f30ee816914..93f926b537a 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -2,16 +2,15 @@ import voluptuous as vol from homeassistant.auth.permissions.const import POLICY_READ -from homeassistant.const import MATCH_ALL, EVENT_TIME_CHANGED, EVENT_STATE_CHANGED -from homeassistant.core import callback, DOMAIN as HASS_DOMAIN -from homeassistant.exceptions import Unauthorized, ServiceNotFound, HomeAssistantError +from homeassistant.const import EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL +from homeassistant.core import DOMAIN as HASS_DOMAIN, callback +from homeassistant.exceptions import HomeAssistantError, ServiceNotFound, Unauthorized from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.service import async_get_all_descriptions from homeassistant.helpers.event import async_track_state_change +from homeassistant.helpers.service import async_get_all_descriptions from . import const, decorators, messages - # mypy: allow-untyped-calls, allow-untyped-defs @@ -45,6 +44,8 @@ def handle_subscribe_events(hass, connection, msg): Async friendly. """ + # Circular dep + # pylint: disable=import-outside-toplevel from .permissions import SUBSCRIBE_WHITELIST event_type = msg["event_type"] diff --git a/homeassistant/components/websocket_api/connection.py b/homeassistant/components/websocket_api/connection.py index 5a0284a34d4..ed24a70519d 100644 --- a/homeassistant/components/websocket_api/connection.py +++ b/homeassistant/components/websocket_api/connection.py @@ -4,12 +4,11 @@ from typing import Any, Callable, Dict, Hashable import voluptuous as vol -from homeassistant.core import callback, Context +from homeassistant.core import Context, callback from homeassistant.exceptions import Unauthorized from . import const, messages - # mypy: allow-untyped-calls, allow-untyped-defs diff --git a/homeassistant/components/websocket_api/const.py b/homeassistant/components/websocket_api/const.py index fe9792c4ab3..8ad9443a4d6 100644 --- a/homeassistant/components/websocket_api/const.py +++ b/homeassistant/components/websocket_api/const.py @@ -3,6 +3,7 @@ import asyncio from concurrent import futures from functools import partial import json + from homeassistant.helpers.json import JSONEncoder DOMAIN = "websocket_api" diff --git a/homeassistant/components/websocket_api/decorators.py b/homeassistant/components/websocket_api/decorators.py index 025131643e8..1a1330242bc 100644 --- a/homeassistant/components/websocket_api/decorators.py +++ b/homeassistant/components/websocket_api/decorators.py @@ -7,7 +7,6 @@ from homeassistant.exceptions import Unauthorized from . import messages - # mypy: allow-untyped-calls, allow-untyped-defs _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/websocket_api/http.py b/homeassistant/components/websocket_api/http.py index be1830aa07b..3921413fd28 100644 --- a/homeassistant/components/websocket_api/http.py +++ b/homeassistant/components/websocket_api/http.py @@ -4,28 +4,27 @@ from contextlib import suppress import logging from typing import Optional -from aiohttp import web, WSMsgType +from aiohttp import WSMsgType, web import async_timeout +from homeassistant.components.http import HomeAssistantView from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback -from homeassistant.components.http import HomeAssistantView +from .auth import AuthPhase, auth_required_message from .const import ( - MAX_PENDING_MSG, CANCELLATION_ERRORS, - URL, + DATA_CONNECTIONS, ERR_UNKNOWN_ERROR, + JSON_DUMP, + MAX_PENDING_MSG, SIGNAL_WEBSOCKET_CONNECTED, SIGNAL_WEBSOCKET_DISCONNECTED, - DATA_CONNECTIONS, - JSON_DUMP, + URL, ) -from .auth import AuthPhase, auth_required_message from .error import Disconnect from .messages import error_message - # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs diff --git a/homeassistant/components/websocket_api/messages.py b/homeassistant/components/websocket_api/messages.py index c8c760a6549..27d557e8110 100644 --- a/homeassistant/components/websocket_api/messages.py +++ b/homeassistant/components/websocket_api/messages.py @@ -6,7 +6,6 @@ from homeassistant.helpers import config_validation as cv from . import const - # mypy: allow-untyped-defs # Minimal requirements of a message diff --git a/homeassistant/components/websocket_api/permissions.py b/homeassistant/components/websocket_api/permissions.py index ffbb80fa19e..c270c0f0ccc 100644 --- a/homeassistant/components/websocket_api/permissions.py +++ b/homeassistant/components/websocket_api/permissions.py @@ -2,22 +2,22 @@ Separate file to avoid circular imports. """ +from homeassistant.components.frontend import EVENT_PANELS_UPDATED +from homeassistant.components.lovelace import EVENT_LOVELACE_UPDATED +from homeassistant.components.persistent_notification import ( + EVENT_PERSISTENT_NOTIFICATIONS_UPDATED, +) from homeassistant.const import ( EVENT_COMPONENT_LOADED, + EVENT_CORE_CONFIG_UPDATE, EVENT_SERVICE_REGISTERED, EVENT_SERVICE_REMOVED, EVENT_STATE_CHANGED, EVENT_THEMES_UPDATED, - EVENT_CORE_CONFIG_UPDATE, ) -from homeassistant.components.persistent_notification import ( - EVENT_PERSISTENT_NOTIFICATIONS_UPDATED, -) -from homeassistant.components.lovelace import EVENT_LOVELACE_UPDATED from homeassistant.helpers.area_registry import EVENT_AREA_REGISTRY_UPDATED from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED -from homeassistant.components.frontend import EVENT_PANELS_UPDATED # These are events that do not contain any sensitive data # Except for state_changed, which is handled accordingly. diff --git a/homeassistant/components/websocket_api/sensor.py b/homeassistant/components/websocket_api/sensor.py index f8f1257aefc..4ae39787335 100644 --- a/homeassistant/components/websocket_api/sensor.py +++ b/homeassistant/components/websocket_api/sensor.py @@ -4,12 +4,11 @@ from homeassistant.core import callback from homeassistant.helpers.entity import Entity from .const import ( + DATA_CONNECTIONS, SIGNAL_WEBSOCKET_CONNECTED, SIGNAL_WEBSOCKET_DISCONNECTED, - DATA_CONNECTIONS, ) - # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs diff --git a/homeassistant/components/wemo/.translations/da.json b/homeassistant/components/wemo/.translations/da.json index c69547c66ab..1da4d407849 100644 --- a/homeassistant/components/wemo/.translations/da.json +++ b/homeassistant/components/wemo/.translations/da.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "no_devices_found": "Ingen Wemo enheder fundet p\u00e5 netv\u00e6rket.", - "single_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning af Wemo." + "no_devices_found": "Der blev ikke fundet nogen Wemo-enheder p\u00e5 netv\u00e6rket.", + "single_instance_allowed": "Kun en enkelt konfiguration af Wemo er mulig." }, "step": { "confirm": { diff --git a/homeassistant/components/wemo/.translations/ko.json b/homeassistant/components/wemo/.translations/ko.json index 57515f2c970..cc3a70a0bc6 100644 --- a/homeassistant/components/wemo/.translations/ko.json +++ b/homeassistant/components/wemo/.translations/ko.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "Wemo \ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "Wemo \ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "Wemo" } }, diff --git a/homeassistant/components/wemo/__init__.py b/homeassistant/components/wemo/__init__.py index df2d8ed1f31..9b1c4cd465f 100644 --- a/homeassistant/components/wemo/__init__.py +++ b/homeassistant/components/wemo/__init__.py @@ -7,12 +7,10 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components.discovery import SERVICE_WEMO -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers import discovery - from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP +from homeassistant.helpers import config_validation as cv, discovery -DOMAIN = "wemo" +from .const import DOMAIN # Mapping from Wemo model_name to component. WEMO_MODEL_DISPATCH = { diff --git a/homeassistant/components/wemo/config_flow.py b/homeassistant/components/wemo/config_flow.py index 21c911a66ce..9ad7dda10ba 100644 --- a/homeassistant/components/wemo/config_flow.py +++ b/homeassistant/components/wemo/config_flow.py @@ -2,8 +2,8 @@ import pywemo -from homeassistant.helpers import config_entry_flow from homeassistant import config_entries +from homeassistant.helpers import config_entry_flow from . import DOMAIN diff --git a/homeassistant/components/wemo/const.py b/homeassistant/components/wemo/const.py new file mode 100644 index 00000000000..e9272d39bdd --- /dev/null +++ b/homeassistant/components/wemo/const.py @@ -0,0 +1,5 @@ +"""Constants for the Belkin Wemo component.""" +DOMAIN = "wemo" + +SERVICE_SET_HUMIDITY = "set_humidity" +SERVICE_RESET_FILTER_LIFE = "reset_filter_life" diff --git a/homeassistant/components/wemo/fan.py b/homeassistant/components/wemo/fan.py index 91273fa033f..5974a9eae8c 100644 --- a/homeassistant/components/wemo/fan.py +++ b/homeassistant/components/wemo/fan.py @@ -1,27 +1,27 @@ """Support for WeMo humidifier.""" import asyncio -import logging from datetime import timedelta +import logging import async_timeout from pywemo import discovery import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.fan import ( - DOMAIN, - SUPPORT_SET_SPEED, - FanEntity, - SPEED_OFF, + SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, - SPEED_HIGH, + SPEED_OFF, + SUPPORT_SET_SPEED, + FanEntity, ) -from homeassistant.exceptions import PlatformNotReady from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.exceptions import PlatformNotReady +import homeassistant.helpers.config_validation as cv from . import SUBSCRIPTION_REGISTRY +from .const import DOMAIN, SERVICE_RESET_FILTER_LIFE, SERVICE_SET_HUMIDITY SCAN_INTERVAL = timedelta(seconds=10) DATA_KEY = "fan.wemo" @@ -79,8 +79,6 @@ HASS_FAN_SPEED_TO_WEMO = { if k not in [WEMO_FAN_LOW, WEMO_FAN_HIGH] } -SERVICE_SET_HUMIDITY = "wemo_set_humidity" - SET_HUMIDITY_SCHEMA = vol.Schema( { vol.Required(ATTR_ENTITY_ID): cv.entity_ids, @@ -90,8 +88,6 @@ SET_HUMIDITY_SCHEMA = vol.Schema( } ) -SERVICE_RESET_FILTER_LIFE = "wemo_reset_filter_life" - RESET_FILTER_LIFE_SCHEMA = vol.Schema({vol.Required(ATTR_ENTITY_ID): cv.entity_ids}) diff --git a/homeassistant/components/wemo/light.py b/homeassistant/components/wemo/light.py index dab96eb8c94..37113a09bd1 100644 --- a/homeassistant/components/wemo/light.py +++ b/homeassistant/components/wemo/light.py @@ -1,7 +1,7 @@ """Support for Belkin WeMo lights.""" import asyncio -import logging from datetime import timedelta +import logging import async_timeout from pywemo import discovery @@ -9,15 +9,15 @@ import requests from homeassistant import util from homeassistant.components.light import ( - Light, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION, SUPPORT_BRIGHTNESS, - SUPPORT_COLOR_TEMP, SUPPORT_COLOR, + SUPPORT_COLOR_TEMP, SUPPORT_TRANSITION, + Light, ) from homeassistant.exceptions import PlatformNotReady import homeassistant.util.color as color_util diff --git a/homeassistant/components/wemo/services.yaml b/homeassistant/components/wemo/services.yaml index e69de29bb2d..c2415265c62 100644 --- a/homeassistant/components/wemo/services.yaml +++ b/homeassistant/components/wemo/services.yaml @@ -0,0 +1,16 @@ +set_humidity: + description: Set the target humidity of WeMo humidifier devices. + fields: + entity_id: + description: Names of the WeMo humidifier entities (1 or more entity_ids are required). + example: 'fan.wemo_humidifier' + target_humidity: + description: Target humidity. This is a float value between 0 and 100, but will be mapped to the humidity levels that WeMo humidifiers support (45, 50, 55, 60, and 100/Max) by rounding the value down to the nearest supported value. + example: 56.5 + +reset_filter_life: + description: Reset the WeMo Humidifier's filter life to 100%. + fields: + entity_id: + description: Names of the WeMo humidifier entities (1 or more entity_ids are required). + example: 'fan.wemo_humidifier' diff --git a/homeassistant/components/wemo/switch.py b/homeassistant/components/wemo/switch.py index c1d07a06902..432a0ddf2cc 100644 --- a/homeassistant/components/wemo/switch.py +++ b/homeassistant/components/wemo/switch.py @@ -1,18 +1,19 @@ """Support for WeMo switches.""" import asyncio -import logging from datetime import datetime, timedelta +import logging import async_timeout from pywemo import discovery import requests from homeassistant.components.switch import SwitchDevice +from homeassistant.const import STATE_OFF, STATE_ON, STATE_STANDBY, STATE_UNKNOWN from homeassistant.exceptions import PlatformNotReady from homeassistant.util import convert -from homeassistant.const import STATE_OFF, STATE_ON, STATE_STANDBY, STATE_UNKNOWN -from . import SUBSCRIPTION_REGISTRY, DOMAIN as WEMO_DOMAIN +from . import SUBSCRIPTION_REGISTRY +from .const import DOMAIN SCAN_INTERVAL = timedelta(seconds=10) @@ -96,7 +97,7 @@ class WemoSwitch(SwitchDevice): @property def device_info(self): """Return the device info.""" - return {"name": self._name, "identifiers": {(WEMO_DOMAIN, self._serialnumber)}} + return {"name": self._name, "identifiers": {(DOMAIN, self._serialnumber)}} @property def device_state_attributes(self): diff --git a/homeassistant/components/whois/sensor.py b/homeassistant/components/whois/sensor.py index 3c78d80ba92..dc9da1100f0 100644 --- a/homeassistant/components/whois/sensor.py +++ b/homeassistant/components/whois/sensor.py @@ -44,7 +44,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): "WHOIS lookup for %s didn't contain an expiration date", domain ) return - except whois.BaseException as ex: + except whois.BaseException as ex: # pylint: disable=broad-except _LOGGER.error("Exception %s occurred during WHOIS lookup for %s", ex, domain) return @@ -96,7 +96,7 @@ class WhoisSensor(Entity): """Get the current WHOIS data for the domain.""" try: response = self.whois(self._domain) - except whois.BaseException as ex: + except whois.BaseException as ex: # pylint: disable=broad-except _LOGGER.error("Exception %s occurred during WHOIS lookup", ex) self._empty_state_and_attributes() return diff --git a/homeassistant/components/wink/__init__.py b/homeassistant/components/wink/__init__.py index e2eb98938bb..b71d44206c8 100644 --- a/homeassistant/components/wink/__init__.py +++ b/homeassistant/components/wink/__init__.py @@ -6,8 +6,8 @@ import os import time from aiohttp.web import Response -import pywink from pubnubsubhandler import PubNubSubscriptionHandler +import pywink import voluptuous as vol from homeassistant.components.http import HomeAssistantView @@ -25,7 +25,7 @@ from homeassistant.const import ( from homeassistant.core import callback from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA +from homeassistant.helpers.config_validation import make_entity_service_schema from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import track_time_interval @@ -131,11 +131,11 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -RENAME_DEVICE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( +RENAME_DEVICE_SCHEMA = make_entity_service_schema( {vol.Required(ATTR_NAME): cv.string}, extra=vol.ALLOW_EXTRA ) -DELETE_DEVICE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA) +DELETE_DEVICE_SCHEMA = make_entity_service_schema({}, extra=vol.ALLOW_EXTRA) SET_PAIRING_MODE_SCHEMA = vol.Schema( { @@ -146,31 +146,31 @@ SET_PAIRING_MODE_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -SET_VOLUME_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( +SET_VOLUME_SCHEMA = make_entity_service_schema( {vol.Required(ATTR_VOLUME): vol.In(VOLUMES)} ) -SET_SIREN_TONE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( +SET_SIREN_TONE_SCHEMA = make_entity_service_schema( {vol.Required(ATTR_TONE): vol.In(TONES)} ) -SET_CHIME_MODE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( +SET_CHIME_MODE_SCHEMA = make_entity_service_schema( {vol.Required(ATTR_TONE): vol.In(CHIME_TONES)} ) -SET_AUTO_SHUTOFF_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( +SET_AUTO_SHUTOFF_SCHEMA = make_entity_service_schema( {vol.Required(ATTR_AUTO_SHUTOFF): vol.In(AUTO_SHUTOFF_TIMES)} ) -SET_STROBE_ENABLED_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( +SET_STROBE_ENABLED_SCHEMA = make_entity_service_schema( {vol.Required(ATTR_ENABLED): cv.boolean} ) -ENABLED_SIREN_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( +ENABLED_SIREN_SCHEMA = make_entity_service_schema( {vol.Required(ATTR_ENABLED): cv.boolean} ) -DIAL_CONFIG_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( +DIAL_CONFIG_SCHEMA = make_entity_service_schema( { vol.Optional(ATTR_MIN_VALUE): vol.Coerce(int), vol.Optional(ATTR_MAX_VALUE): vol.Coerce(int), @@ -182,7 +182,7 @@ DIAL_CONFIG_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( } ) -DIAL_STATE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( +DIAL_STATE_SCHEMA = make_entity_service_schema( { vol.Required(ATTR_VALUE): vol.Coerce(int), vol.Optional(ATTR_LABELS): cv.ensure_list(cv.string), @@ -740,7 +740,7 @@ class WinkDevice(Entity): try: if message is None: _LOGGER.error( - "Error on pubnub update for %s " "polling API for current state", + "Error on pubnub update for %s polling API for current state", self.name, ) self.schedule_update_ha_state(True) @@ -749,8 +749,7 @@ class WinkDevice(Entity): self.schedule_update_ha_state() except (ValueError, KeyError, AttributeError): _LOGGER.error( - "Error in pubnub JSON for %s " "polling API for current state", - self.name, + "Error in pubnub JSON for %s polling API for current state", self.name, ) self.schedule_update_ha_state(True) diff --git a/homeassistant/components/wink/alarm_control_panel.py b/homeassistant/components/wink/alarm_control_panel.py index 654252f5ffe..733022e91b1 100644 --- a/homeassistant/components/wink/alarm_control_panel.py +++ b/homeassistant/components/wink/alarm_control_panel.py @@ -4,6 +4,10 @@ import logging import pywink import homeassistant.components.alarm_control_panel as alarm +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, @@ -52,6 +56,11 @@ class WinkCameraDevice(WinkDevice, alarm.AlarmControlPanel): state = None return state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY + def alarm_disarm(self, code=None): """Send disarm command.""" self.wink.set_mode("home") diff --git a/homeassistant/components/wink/climate.py b/homeassistant/components/wink/climate.py index 6323fa7bbfe..85d477313f1 100644 --- a/homeassistant/components/wink/climate.py +++ b/homeassistant/components/wink/climate.py @@ -23,12 +23,12 @@ from homeassistant.components.climate.const import ( HVAC_MODE_OFF, PRESET_AWAY, PRESET_ECO, + PRESET_NONE, SUPPORT_AUX_HEAT, SUPPORT_FAN_MODE, + SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, - SUPPORT_PRESET_MODE, - PRESET_NONE, ) from homeassistant.const import ATTR_TEMPERATURE, PRECISION_TENTHS, TEMP_CELSIUS from homeassistant.helpers.temperature import display_temp as show_temp diff --git a/homeassistant/components/wink/lock.py b/homeassistant/components/wink/lock.py index 37b27c0d500..57cf9d304ec 100644 --- a/homeassistant/components/wink/lock.py +++ b/homeassistant/components/wink/lock.py @@ -18,12 +18,12 @@ from . import DOMAIN, WinkDevice _LOGGER = logging.getLogger(__name__) -SERVICE_SET_VACATION_MODE = "wink_set_lock_vacation_mode" -SERVICE_SET_ALARM_MODE = "wink_set_lock_alarm_mode" -SERVICE_SET_ALARM_SENSITIVITY = "wink_set_lock_alarm_sensitivity" -SERVICE_SET_ALARM_STATE = "wink_set_lock_alarm_state" -SERVICE_SET_BEEPER_STATE = "wink_set_lock_beeper_state" -SERVICE_ADD_KEY = "wink_add_new_lock_key_code" +SERVICE_SET_VACATION_MODE = "set_lock_vacation_mode" +SERVICE_SET_ALARM_MODE = "set_lock_alarm_mode" +SERVICE_SET_ALARM_SENSITIVITY = "set_lock_alarm_sensitivity" +SERVICE_SET_ALARM_STATE = "set_lock_alarm_state" +SERVICE_SET_BEEPER_STATE = "set_lock_beeper_state" +SERVICE_ADD_KEY = "add_new_lock_key_code" ATTR_ENABLED = "enabled" ATTR_SENSITIVITY = "sensitivity" diff --git a/homeassistant/components/wink/manifest.json b/homeassistant/components/wink/manifest.json index acf9c38e632..a1bae648292 100644 --- a/homeassistant/components/wink/manifest.json +++ b/homeassistant/components/wink/manifest.json @@ -2,10 +2,7 @@ "domain": "wink", "name": "Wink", "documentation": "https://www.home-assistant.io/integrations/wink", - "requirements": [ - "pubnubsub-handler==1.0.8", - "python-wink==1.10.5" - ], - "dependencies": ["configurator"], + "requirements": ["pubnubsub-handler==1.0.8", "python-wink==1.10.5"], + "dependencies": ["configurator", "http"], "codeowners": [] } diff --git a/homeassistant/components/wink/services.yaml b/homeassistant/components/wink/services.yaml index a3b489f9cf5..93d53159702 100644 --- a/homeassistant/components/wink/services.yaml +++ b/homeassistant/components/wink/services.yaml @@ -151,4 +151,67 @@ set_nimbus_dial_state: example: 250 labels: description: The values shown on the dial labels ["Dial 1", "test"] the first value is what is shown by default the second value is shown when the nimbus is pressed - example: ["example", "test"] \ No newline at end of file + example: ["example", "test"] + +set_lock_vacation_mode: + description: Set vacation mode for all or specified locks. Disables all user codes. + fields: + entity_id: + description: Name of lock to unlock. + example: 'lock.front_door' + enabled: + description: enable or disable. true or false. + example: true + +set_lock_alarm_mode: + description: Set alarm mode for all or specified locks. + fields: + entity_id: + description: Name of lock to unlock. + example: 'lock.front_door' + mode: + description: One of tamper, activity, or forced_entry. + example: tamper + +set_lock_alarm_sensitivity: + description: Set alarm sensitivity for all or specified locks. + fields: + entity_id: + description: Name of lock to unlock. + example: 'lock.front_door' + sensitivity: + description: One of low, medium_low, medium, medium_high, high. + example: medium + +set_lock_alarm_state: + description: Set alarm state. + fields: + entity_id: + description: Name of lock to unlock. + example: 'lock.front_door' + enabled: + description: enable or disable. true or false. + example: true + +set_lock_beeper_state: + description: Set beeper state. + fields: + entity_id: + description: Name of lock to unlock. + example: 'lock.front_door' + enabled: + description: enable or disable. true or false. + example: true + +add_new_lock_key_code: + description: Add a new user key code. + fields: + entity_id: + description: Name of lock to unlock. + example: 'lock.front_door' + name: + description: name of the new key code. + example: Bob + code: + description: new key code, length must match length of other codes. Default length is 4. + example: 1234 \ No newline at end of file diff --git a/homeassistant/components/wirelesstag/__init__.py b/homeassistant/components/wirelesstag/__init__.py index 5e0da881076..c0a30a8867f 100644 --- a/homeassistant/components/wirelesstag/__init__.py +++ b/homeassistant/components/wirelesstag/__init__.py @@ -1,18 +1,20 @@ """Support for Wireless Sensor Tags.""" import logging -from requests.exceptions import HTTPError, ConnectTimeout +from requests.exceptions import ConnectTimeout, HTTPError import voluptuous as vol +from wirelesstagpy import NotificationConfig as NC + +from homeassistant import util from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_VOLTAGE, - CONF_USERNAME, CONF_PASSWORD, + CONF_USERNAME, ) import homeassistant.helpers.config_validation as cv -from homeassistant import util -from homeassistant.helpers.entity import Entity from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -96,7 +98,6 @@ class WirelessTagPlatform: configs.extend(bi_sensor.event.build_notifications(bi_url, mac)) update_url = self.update_callback_url - from wirelesstagpy import NotificationConfig as NC update_config = NC.make_config_for_update_event(update_url, mac) @@ -198,7 +199,7 @@ def setup(hass, config): except (ConnectTimeout, HTTPError, WirelessTagsException) as ex: _LOGGER.error("Unable to connect to wirelesstag.net service: %s", str(ex)) hass.components.persistent_notification.create( - "Error: {}
" "Please restart hass after fixing this." "".format(ex), + "Error: {}
Please restart hass after fixing this.".format(ex), title=NOTIFICATION_TITLE, notification_id=NOTIFICATION_ID, ) diff --git a/homeassistant/components/withings/.translations/da.json b/homeassistant/components/withings/.translations/da.json index d2dddbbd204..e4599fe8ec2 100644 --- a/homeassistant/components/withings/.translations/da.json +++ b/homeassistant/components/withings/.translations/da.json @@ -1,9 +1,19 @@ { "config": { + "abort": { + "no_flows": "Du skal konfigurere Withings, f\u00f8r du kan godkende med den. L\u00e6s venligst dokumentationen." + }, "create_entry": { "default": "Godkendt med Withings for den valgte profil." }, "step": { + "profile": { + "data": { + "profile": "Profile" + }, + "description": "Hvilken profil har du valgt p\u00e5 Withings hjemmeside? Det er vigtigt, at profilerne matcher, ellers vil data blive m\u00e6rket forkert.", + "title": "Brugerprofil." + }, "user": { "data": { "profile": "Profil" diff --git a/homeassistant/components/withings/__init__.py b/homeassistant/components/withings/__init__.py index 482c4e96e5c..92c3f2ae155 100644 --- a/homeassistant/components/withings/__init__.py +++ b/homeassistant/components/withings/__init__.py @@ -7,11 +7,11 @@ import voluptuous as vol from withings_api import WithingsAuth from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from homeassistant.helpers import config_validation as cv, config_entry_oauth2_flow from . import config_flow, const -from .common import _LOGGER, get_data_manager, NotAuthenticatedError +from .common import _LOGGER, NotAuthenticatedError, get_data_manager DOMAIN = const.DOMAIN diff --git a/homeassistant/components/withings/common.py b/homeassistant/components/withings/common.py index 911bb08906b..9cba055bac4 100644 --- a/homeassistant/components/withings/common.py +++ b/homeassistant/components/withings/common.py @@ -1,4 +1,5 @@ """Common code for Withings.""" +from asyncio import run_coroutine_threadsafe import datetime from functools import partial import logging @@ -6,15 +7,14 @@ import re import time from typing import Any, Dict -from asyncio import run_coroutine_threadsafe import requests from withings_api import ( AbstractWithingsApi, - SleepGetResponse, MeasureGetMeasResponse, + SleepGetResponse, SleepGetSummaryResponse, ) -from withings_api.common import UnauthorizedException, AuthFailedException +from withings_api.common import AuthFailedException, UnauthorizedException from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -51,7 +51,7 @@ class ThrottleData: """Throttle data.""" def __init__(self, interval: int, data: Any): - """Constructor.""" + """Initialize throttle data.""" self._time = int(time.time()) self._interval = interval self._data = data @@ -126,7 +126,7 @@ class WithingsDataManager: service_available = None def __init__(self, hass: HomeAssistant, profile: str, api: ConfigEntryWithingsApi): - """Constructor.""" + """Initialize data manager.""" self._hass = hass self._api = api self._profile = profile @@ -226,7 +226,7 @@ class WithingsDataManager: WithingsDataManager.print_service_available() return result - except Exception as ex: # pylint: disable=broad-except + except Exception as ex: # Withings api encountered error. if isinstance(ex, (UnauthorizedException, AuthFailedException)): raise NotAuthenticatedError(ex) @@ -259,8 +259,8 @@ class WithingsDataManager: async def update_sleep(self) -> SleepGetResponse: """Update the sleep data.""" - end_date = int(time.time()) - start_date = end_date - (6 * 60 * 60) + end_date = dt.now() + start_date = end_date - datetime.timedelta(hours=2) def function(): return self._api.sleep_get(startdate=start_date, enddate=end_date) diff --git a/homeassistant/components/withings/sensor.py b/homeassistant/components/withings/sensor.py index 17eae93ec0d..aae706dec61 100644 --- a/homeassistant/components/withings/sensor.py +++ b/homeassistant/components/withings/sensor.py @@ -2,21 +2,21 @@ from typing import Callable, List, Union from withings_api.common import ( - MeasureType, GetSleepSummaryField, MeasureGetMeasResponse, + MeasureGroupAttribs, + MeasureType, SleepGetResponse, SleepGetSummaryResponse, - get_measure_value, - MeasureGroupAttribs, SleepState, + get_measure_value, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers.entity import Entity from homeassistant.util import slugify -from homeassistant.helpers import config_entry_oauth2_flow from . import const from .common import _LOGGER, WithingsDataManager, get_data_manager @@ -55,7 +55,7 @@ class WithingsAttribute: unit_of_measurement: str, icon: str, ) -> None: - """Constructor.""" + """Initialize attribute.""" self.measurement = measurement self.measure_type = measure_type self.friendly_name = friendly_name @@ -73,7 +73,7 @@ class WithingsSleepStateAttribute(WithingsAttribute): def __init__( self, measurement: str, friendly_name: str, unit_of_measurement: str, icon: str ) -> None: - """Constructor.""" + """Initialize sleep state attribute.""" super().__init__(measurement, None, friendly_name, unit_of_measurement, icon) @@ -382,7 +382,8 @@ class WithingsHealthSensor(Entity): self._state = None return - serie = data.series[len(data.series) - 1] + sorted_series = sorted(data.series, key=lambda serie: serie.startdate) + serie = sorted_series[len(sorted_series) - 1] state = None if serie.state == SleepState.AWAKE: state = const.STATE_AWAKE diff --git a/homeassistant/components/wled/.translations/bg.json b/homeassistant/components/wled/.translations/bg.json new file mode 100644 index 00000000000..d99df20187f --- /dev/null +++ b/homeassistant/components/wled/.translations/bg.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u0422\u043e\u0432\u0430 WLED \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e.", + "connection_error": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 WLED \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e." + }, + "error": { + "connection_error": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 WLED \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e." + }, + "flow_title": "WLED: {name}", + "step": { + "user": { + "data": { + "host": "\u0410\u0434\u0440\u0435\u0441" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0432\u0430\u0448\u0438\u044f WLED \u0434\u0430 \u0441\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0438\u0440\u0430 \u0441 Home Assistant.", + "title": "\u0421\u0432\u044a\u0440\u0436\u0435\u0442\u0435 \u0412\u0430\u0448\u0438\u044f WLED" + }, + "zeroconf_confirm": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u0435 WLED \u0441 \u0438\u043c\u0435 {name} `\u043a\u044a\u043c Home Assistant?", + "title": "\u041e\u0442\u043a\u0440\u0438\u0442\u043e \u0435 WLED \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + } + }, + "title": "WLED" + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/.translations/da.json b/homeassistant/components/wled/.translations/da.json new file mode 100644 index 00000000000..0ab3a789b3a --- /dev/null +++ b/homeassistant/components/wled/.translations/da.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Denne WLED-enhed er allerede konfigureret.", + "connection_error": "Kunne ikke oprette forbindelse til WLED-enheden." + }, + "error": { + "connection_error": "Kunne ikke oprette forbindelse til WLED-enheden." + }, + "flow_title": "WLED: {name}", + "step": { + "user": { + "data": { + "host": "V\u00e6rt eller IP-adresse" + }, + "description": "Indstil din WLED til at integrere med Home Assistant.", + "title": "Forbind din WLED" + }, + "zeroconf_confirm": { + "description": "\u00d8nsker du at tilf\u00f8je WLED-enhed med navnet `{name}' til Home Assistant?", + "title": "Fandt WLED-enhed" + } + }, + "title": "WLED" + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/.translations/de.json b/homeassistant/components/wled/.translations/de.json index f50a24eeac0..2d1cc5ef97d 100644 --- a/homeassistant/components/wled/.translations/de.json +++ b/homeassistant/components/wled/.translations/de.json @@ -1,6 +1,23 @@ { "config": { + "abort": { + "already_configured": "Dieses WLED-Ger\u00e4t ist bereits konfiguriert.", + "connection_error": "Verbindung zum WLED-Ger\u00e4t fehlgeschlagen." + }, + "error": { + "connection_error": "Verbindung zum WLED-Ger\u00e4t fehlgeschlagen." + }, "flow_title": "WLED: {name}", + "step": { + "user": { + "data": { + "host": "Hostname oder IP-Adresse" + } + }, + "zeroconf_confirm": { + "title": "Gefundenes WLED-Ger\u00e4t" + } + }, "title": "WLED" } } \ No newline at end of file diff --git a/homeassistant/components/wled/.translations/es.json b/homeassistant/components/wled/.translations/es.json index 7dd388d41af..b7f567698ea 100644 --- a/homeassistant/components/wled/.translations/es.json +++ b/homeassistant/components/wled/.translations/es.json @@ -7,7 +7,7 @@ "error": { "connection_error": "No se ha podido conectar al dispositivo WLED." }, - "flow_title": "WLED: {nombre}", + "flow_title": "WLED: {name}", "step": { "user": { "data": { diff --git a/homeassistant/components/wled/.translations/fr.json b/homeassistant/components/wled/.translations/fr.json index 5da30ab6288..6f275ad8199 100644 --- a/homeassistant/components/wled/.translations/fr.json +++ b/homeassistant/components/wled/.translations/fr.json @@ -17,7 +17,7 @@ "title": "Liez votre WLED" }, "zeroconf_confirm": { - "description": "Voulez-vous ajouter le dispositif WLED nomm\u00e9 '{name}' \u00e0 Home Assistant?", + "description": "Voulez-vous ajouter le dispositif WLED nomm\u00e9 `{name}` \u00e0 Home Assistant?", "title": "Dispositif WLED d\u00e9couvert" } }, diff --git a/homeassistant/components/wled/.translations/ko.json b/homeassistant/components/wled/.translations/ko.json new file mode 100644 index 00000000000..38496c01ee8 --- /dev/null +++ b/homeassistant/components/wled/.translations/ko.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "WLED \uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "connection_error": "WLED \uae30\uae30\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4." + }, + "error": { + "connection_error": "WLED \uae30\uae30\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4." + }, + "flow_title": "WLED: {name}", + "step": { + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8 \ub610\ub294 IP \uc8fc\uc18c" + }, + "description": "Home Assistant \uc5d0 WLED \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4.", + "title": "WLED \uc5f0\uacb0" + }, + "zeroconf_confirm": { + "description": "Home Assistant \uc5d0 WLED `{name}` \uc744(\ub97c) \ucd94\uac00\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "\ubc1c\uacac\ub41c WLED \uae30\uae30" + } + }, + "title": "WLED" + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/.translations/nl.json b/homeassistant/components/wled/.translations/nl.json new file mode 100644 index 00000000000..266f74ce6c2 --- /dev/null +++ b/homeassistant/components/wled/.translations/nl.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Dit WLED-apparaat is al geconfigureerd.", + "connection_error": "Kan geen verbinding maken met WLED-apparaat." + }, + "error": { + "connection_error": "Kan geen verbinding maken met WLED-apparaat." + }, + "flow_title": "WLED: {name}", + "step": { + "user": { + "data": { + "host": "Hostnaam of IP-adres" + }, + "description": "Stel uw WLED in op integratie met Home Assistant.", + "title": "Koppel je WLED" + }, + "zeroconf_confirm": { + "description": "Wil je de WLED genaamd `{name}` toevoegen aan Home Assistant?", + "title": "Ontdekt WLED-apparaat" + } + }, + "title": "WLED" + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/.translations/nn.json b/homeassistant/components/wled/.translations/nn.json new file mode 100644 index 00000000000..f50a24eeac0 --- /dev/null +++ b/homeassistant/components/wled/.translations/nn.json @@ -0,0 +1,6 @@ +{ + "config": { + "flow_title": "WLED: {name}", + "title": "WLED" + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/config_flow.py b/homeassistant/components/wled/config_flow.py index 7be283874e0..c6b11fa1eb6 100644 --- a/homeassistant/components/wled/config_flow.py +++ b/homeassistant/components/wled/config_flow.py @@ -14,7 +14,7 @@ from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME from homeassistant.helpers import ConfigType from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import DOMAIN # pylint: disable=W0611 +from .const import DOMAIN # pylint: disable=unused-import _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/wled/light.py b/homeassistant/components/wled/light.py index 3d2c9d6ef2c..8bc1a56b205 100644 --- a/homeassistant/components/wled/light.py +++ b/homeassistant/components/wled/light.py @@ -10,11 +10,13 @@ from homeassistant.components.light import ( ATTR_EFFECT, ATTR_HS_COLOR, ATTR_TRANSITION, + ATTR_WHITE_VALUE, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_TRANSITION, + SUPPORT_WHITE_VALUE, Light, ) from homeassistant.config_entries import ConfigEntry @@ -79,6 +81,7 @@ class WLEDLight(Light, WLEDDeviceEntity): self._color: Optional[Tuple[float, float]] = None self._effect: Optional[str] = None self._state: Optional[bool] = None + self._white_value: Optional[int] = None # Only apply the segment ID if it is not the first segment name = wled.device.info.name @@ -107,10 +110,15 @@ class WLEDLight(Light, WLEDDeviceEntity): """Return the brightness of this light between 1..255.""" return self._brightness + @property + def white_value(self) -> Optional[int]: + """Return the white value of this light between 0..255.""" + return self._white_value + @property def supported_features(self) -> int: """Flag supported features.""" - return ( + flags = ( SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_COLOR_TEMP @@ -118,6 +126,11 @@ class WLEDLight(Light, WLEDDeviceEntity): | SUPPORT_TRANSITION ) + if self._rgbw: + flags |= SUPPORT_WHITE_VALUE + + return flags + @property def effect_list(self) -> List[str]: """Return the list of supported effects.""" @@ -163,11 +176,21 @@ class WLEDLight(Light, WLEDDeviceEntity): if ATTR_EFFECT in kwargs: data[ATTR_EFFECT] = kwargs[ATTR_EFFECT] - # Support for RGBW strips - if self._rgbw and any(x in (ATTR_COLOR_TEMP, ATTR_HS_COLOR) for x in kwargs): - data[ATTR_COLOR_PRIMARY] = color_util.color_rgb_to_rgbw( - *data[ATTR_COLOR_PRIMARY] - ) + # Support for RGBW strips, adds white value + if self._rgbw and any( + x in (ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_WHITE_VALUE) for x in kwargs + ): + # WLED cannot just accept a white value, it needs the color. + # We use the last know color in case just the white value changes. + if not any(x in (ATTR_COLOR_TEMP, ATTR_HS_COLOR) for x in kwargs): + hue, sat = self._color + data[ATTR_COLOR_PRIMARY] = color_util.color_hsv_to_RGB(hue, sat, 100) + + # Add requested or last known white value + if ATTR_WHITE_VALUE in kwargs: + data[ATTR_COLOR_PRIMARY] += (kwargs[ATTR_WHITE_VALUE],) + else: + data[ATTR_COLOR_PRIMARY] += (self._white_value,) try: await self.wled.light(**data) @@ -186,6 +209,9 @@ class WLEDLight(Light, WLEDDeviceEntity): if ATTR_COLOR_TEMP in kwargs: self._color = color_util.color_temperature_to_hs(mireds) + if ATTR_WHITE_VALUE in kwargs: + self._white_value = kwargs[ATTR_WHITE_VALUE] + except WLEDError: _LOGGER.error("An error occurred while turning on WLED light.") self._available = False @@ -198,9 +224,9 @@ class WLEDLight(Light, WLEDDeviceEntity): self._state = self.wled.device.state.on color = self.wled.device.state.segments[self._segment].color_primary + self._color = color_util.color_RGB_to_hs(*color[:3]) if self._rgbw: - color = color_util.color_rgbw_to_rgb(*color) - self._color = color_util.color_RGB_to_hs(*color) + self._white_value = color[-1] playlist = self.wled.device.state.playlist if playlist == -1: diff --git a/homeassistant/components/workday/binary_sensor.py b/homeassistant/components/workday/binary_sensor.py index 3ca2afcc749..0aa1f5bfc42 100644 --- a/homeassistant/components/workday/binary_sensor.py +++ b/homeassistant/components/workday/binary_sensor.py @@ -1,112 +1,17 @@ """Sensor to indicate whether the current day is a workday.""" -import logging from datetime import datetime, timedelta +import logging +from typing import Any import holidays import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice from homeassistant.const import CONF_NAME, WEEKDAYS -from homeassistant.components.binary_sensor import BinarySensorDevice import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -# List of all countries currently supported by holidays -# There seems to be no way to get the list out at runtime -ALL_COUNTRIES = [ - "Argentina", - "AR", - "Aruba", - "AW", - "Australia", - "AU", - "Austria", - "AT", - "Brazil", - "BR", - "Belarus", - "BY", - "Belgium", - "BE", - "Bulgaria", - "BG", - "Canada", - "CA", - "Colombia", - "CO", - "Croatia", - "HR", - "Czech", - "CZ", - "Denmark", - "DK", - "England", - "EuropeanCentralBank", - "ECB", - "TAR", - "Finland", - "FI", - "France", - "FRA", - "Germany", - "DE", - "Hungary", - "HU", - "Honduras", - "HUD", - "India", - "IND", - "Ireland", - "IE", - "Isle of Man", - "Italy", - "IT", - "Japan", - "JP", - "Lithuania", - "LT", - "Luxembourg", - "LU", - "Mexico", - "MX", - "Netherlands", - "NL", - "NewZealand", - "NZ", - "Northern Ireland", - "Norway", - "NO", - "Polish", - "PL", - "Portugal", - "PT", - "PortugalExt", - "PTE", - "Russia", - "RU", - "Scotland", - "Slovenia", - "SI", - "Slovakia", - "SK", - "South Africa", - "ZA", - "Spain", - "ES", - "Sweden", - "SE", - "Switzerland", - "CH", - "Ukraine", - "UA", - "UnitedKingdom", - "UK", - "UnitedStates", - "US", - "Wales", -] - ALLOWED_DAYS = WEEKDAYS + ["holiday"] CONF_COUNTRY = "country" @@ -123,9 +28,28 @@ DEFAULT_EXCLUDES = ["sat", "sun", "holiday"] DEFAULT_NAME = "Workday Sensor" DEFAULT_OFFSET = 0 + +def valid_country(value: Any) -> str: + """Validate that the given country is supported.""" + value = cv.string(value) + all_supported_countries = holidays.list_supported_countries() + + try: + raw_value = value.encode("utf-8") + except UnicodeError: + raise vol.Invalid( + "The country name or the abbreviation must be a valid UTF-8 string." + ) + if not raw_value: + raise vol.Invalid("Country name or the abbreviation must not be empty.") + if value not in all_supported_countries: + raise vol.Invalid("Country is not supported.") + return value + + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { - vol.Required(CONF_COUNTRY): vol.In(ALL_COUNTRIES), + vol.Required(CONF_COUNTRY): valid_country, vol.Optional(CONF_EXCLUDES, default=DEFAULT_EXCLUDES): vol.All( cv.ensure_list, [vol.In(ALLOWED_DAYS)] ), @@ -142,13 +66,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Workday sensor.""" - sensor_name = config.get(CONF_NAME) - country = config.get(CONF_COUNTRY) - province = config.get(CONF_PROVINCE) - workdays = config.get(CONF_WORKDAYS) - excludes = config.get(CONF_EXCLUDES) - days_offset = config.get(CONF_OFFSET) add_holidays = config.get(CONF_ADD_HOLIDAYS) + country = config[CONF_COUNTRY] + days_offset = config[CONF_OFFSET] + excludes = config[CONF_EXCLUDES] + province = config.get(CONF_PROVINCE) + sensor_name = config[CONF_NAME] + workdays = config[CONF_WORKDAYS] year = (get_date(datetime.today()) + timedelta(days=days_offset)).year obj_holidays = getattr(holidays, country)(years=year) @@ -250,7 +174,7 @@ class IsWorkdaySensor(BinarySensorDevice): # Default is no workday self._state = False - # Get iso day of the week (1 = Monday, 7 = Sunday) + # Get ISO day of the week (1 = Monday, 7 = Sunday) date = get_date(datetime.today()) + 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 4b407e95235..ac3bee7d07c 100644 --- a/homeassistant/components/workday/manifest.json +++ b/homeassistant/components/workday/manifest.json @@ -3,8 +3,8 @@ "name": "Workday", "documentation": "https://www.home-assistant.io/integrations/workday", "requirements": [ - "holidays==0.9.11" + "holidays==0.9.12" ], "dependencies": [], - "codeowners": [] + "codeowners": ["@fabaff"] } \ No newline at end of file diff --git a/homeassistant/components/worldclock/sensor.py b/homeassistant/components/worldclock/sensor.py index 103c38819bb..1709ac7d23e 100644 --- a/homeassistant/components/worldclock/sensor.py +++ b/homeassistant/components/worldclock/sensor.py @@ -5,9 +5,9 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_NAME, CONF_TIME_ZONE -import homeassistant.util.dt as dt_util -from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/worxlandroid/sensor.py b/homeassistant/components/worxlandroid/sensor.py index 4e9bf0a6a4a..fa2cae53f52 100644 --- a/homeassistant/components/worxlandroid/sensor.py +++ b/homeassistant/components/worxlandroid/sensor.py @@ -1,18 +1,16 @@ """Support for Worx Landroid mower.""" -import logging import asyncio +import logging import aiohttp import async_timeout - import voluptuous as vol -import homeassistant.helpers.config_validation as cv - -from homeassistant.helpers.entity import Entity -from homeassistant.components.switch import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_HOST, CONF_PIN, CONF_TIMEOUT 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__) @@ -141,16 +139,9 @@ class WorxLandroidSensor(Entity): state = self.get_error(obj) if state is None: - state_obj = obj["settaggi"] + if obj["batteryChargerState"] == "charging": + return obj["batteryChargerState"] - if state_obj[14] == 1: - return "manual-stop" - if state_obj[5] == 1 and state_obj[13] == 0: - return "charging" - if state_obj[5] == 1 and state_obj[13] == 1: - return "charging-complete" - if state_obj[15] == 1: - return "going-home" - return "mowing" + return obj["state"] return state diff --git a/homeassistant/components/wsdot/sensor.py b/homeassistant/components/wsdot/sensor.py index 2b6c0b73563..5afa3a3efcf 100644 --- a/homeassistant/components/wsdot/sensor.py +++ b/homeassistant/components/wsdot/sensor.py @@ -1,21 +1,21 @@ """Support for Washington State Department of Transportation (WSDOT) data.""" +from datetime import datetime, timedelta, timezone import logging import re -from datetime import datetime, timezone, timedelta import requests import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_API_KEY, - CONF_NAME, ATTR_ATTRIBUTION, - CONF_ID, ATTR_NAME, + CONF_API_KEY, + CONF_ID, + CONF_NAME, ) -from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/wunderground/sensor.py b/homeassistant/components/wunderground/sensor.py index 5272b33ccb5..5d3bf1f74b8 100644 --- a/homeassistant/components/wunderground/sensor.py +++ b/homeassistant/components/wunderground/sensor.py @@ -9,27 +9,27 @@ import aiohttp import async_timeout import voluptuous as vol -from homeassistant.helpers.typing import HomeAssistantType, ConfigType from homeassistant.components import sensor from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_MONITORED_CONDITIONS, + ATTR_ATTRIBUTION, CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, - TEMP_FAHRENHEIT, - TEMP_CELSIUS, + CONF_MONITORED_CONDITIONS, + LENGTH_FEET, LENGTH_INCHES, LENGTH_KILOMETERS, LENGTH_MILES, - LENGTH_FEET, - ATTR_ATTRIBUTION, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, ) from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.entity import Entity from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.util import Throttle 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 _RESOURCE = "http://api.wunderground.com/api/{}/{}/{}/q/" _LOGGER = logging.getLogger(__name__) @@ -66,7 +66,7 @@ class WUSensorConfig: device_state_attributes=None, device_class=None, ): - """Constructor. + """Initialize sensor configuration. :param friendly_name: Friendly name :param feature: WU feature. See: @@ -98,7 +98,7 @@ class WUCurrentConditionsSensorConfig(WUSensorConfig): unit_of_measurement: Optional[str] = None, device_class=None, ): - """Constructor. + """Initialize current conditions sensor configuration. :param friendly_name: Friendly name of sensor :field: Field name in the "current_observation" dictionary. @@ -127,7 +127,7 @@ class WUDailyTextForecastSensorConfig(WUSensorConfig): def __init__( self, period: int, field: str, unit_of_measurement: Optional[str] = None ): - """Constructor. + """Initialize daily text forecast sensor configuration. :param period: forecast period number :param field: field name to use as value @@ -164,7 +164,7 @@ class WUDailySimpleForecastSensorConfig(WUSensorConfig): icon=None, device_class=None, ): - """Constructor. + """Initialize daily simple forecast sensor configuration. :param friendly_name: friendly_name of the sensor :param period: forecast period number @@ -207,7 +207,7 @@ class WUHourlyForecastSensorConfig(WUSensorConfig): """Helper for defining sensor configurations for hourly text forecasts.""" def __init__(self, period: int, field: int): - """Constructor. + """Initialize hourly forecast sensor configuration. :param period: forecast period number :param field: field name to use as value @@ -274,7 +274,7 @@ class WUAlmanacSensorConfig(WUSensorConfig): icon: str, device_class=None, ): - """Constructor. + """Initialize almanac sensor configuration. :param friendly_name: Friendly name :param field: value name returned in 'almanac' dict as returned by the WU API @@ -297,7 +297,7 @@ class WUAlertsSensorConfig(WUSensorConfig): """Helper for defining field configuration for alerts.""" def __init__(self, friendly_name: Union[str, Callable]): - """Constructor. + """Initialiize alerts sensor configuration. :param friendly_name: Friendly name """ @@ -1012,7 +1012,7 @@ class WUndergroundSensor(Entity): val = val(self.rest) except (KeyError, IndexError, TypeError, ValueError) as err: _LOGGER.warning( - "Failed to expand cfg from WU API." " Condition: %s Attr: %s Error: %s", + "Failed to expand cfg from WU API. Condition: %s Attr: %s Error: %s", self._condition, what, repr(err), diff --git a/homeassistant/components/wunderlist/__init__.py b/homeassistant/components/wunderlist/__init__.py index 122d09feaa4..4d9ff6e2235 100644 --- a/homeassistant/components/wunderlist/__init__.py +++ b/homeassistant/components/wunderlist/__init__.py @@ -4,8 +4,8 @@ import logging import voluptuous as vol from wunderpy2 import WunderApi +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_NAME, CONF_ACCESS_TOKEN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/wwlln/.translations/da.json b/homeassistant/components/wwlln/.translations/da.json index 7d9a676e163..5d4f4c40b5d 100644 --- a/homeassistant/components/wwlln/.translations/da.json +++ b/homeassistant/components/wwlln/.translations/da.json @@ -1,16 +1,16 @@ { "config": { "error": { - "identifier_exists": "Placering er allerede registreret" + "identifier_exists": "Lokalitet er allerede registreret" }, "step": { "user": { "data": { "latitude": "Breddegrad", "longitude": "L\u00e6ngdegrad", - "radius": "Radius (ved hj\u00e6lp af dit basis enhedssystem)" + "radius": "Radius (ved hj\u00e6lp af dit basisenhedssystem)" }, - "title": "Udfyld dine placeringsoplysninger." + "title": "Udfyld dine lokalitetsoplysninger." } }, "title": "World Wide Lightning Location Network (WWLLN)" diff --git a/homeassistant/components/wwlln/config_flow.py b/homeassistant/components/wwlln/config_flow.py index 3e43ba93278..f9cd022f255 100644 --- a/homeassistant/components/wwlln/config_flow.py +++ b/homeassistant/components/wwlln/config_flow.py @@ -2,7 +2,6 @@ import voluptuous as vol from homeassistant import config_entries -from homeassistant.helpers import config_validation as cv from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, @@ -12,6 +11,7 @@ from homeassistant.const import ( CONF_UNIT_SYSTEM_METRIC, ) from homeassistant.core import callback +from homeassistant.helpers import config_validation as cv from .const import CONF_WINDOW, DEFAULT_RADIUS, DEFAULT_WINDOW, DOMAIN diff --git a/homeassistant/components/x10/light.py b/homeassistant/components/x10/light.py index bb2d2d89456..1f74326d544 100644 --- a/homeassistant/components/x10/light.py +++ b/homeassistant/components/x10/light.py @@ -1,16 +1,16 @@ """Support for X10 lights.""" import logging -from subprocess import check_output, CalledProcessError, STDOUT +from subprocess import STDOUT, CalledProcessError, check_output import voluptuous as vol -from homeassistant.const import CONF_NAME, CONF_ID, CONF_DEVICES from homeassistant.components.light import ( ATTR_BRIGHTNESS, + PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, Light, - PLATFORM_SCHEMA, ) +from homeassistant.const import CONF_DEVICES, CONF_ID, CONF_NAME import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/xbox_live/sensor.py b/homeassistant/components/xbox_live/sensor.py index e837cc4bbbc..41ebf69126a 100644 --- a/homeassistant/components/xbox_live/sensor.py +++ b/homeassistant/components/xbox_live/sensor.py @@ -1,6 +1,6 @@ """Sensor for Xbox Live account status.""" -import logging from datetime import timedelta +import logging import voluptuous as vol from xboxapi import xbox_api diff --git a/homeassistant/components/xeoma/camera.py b/homeassistant/components/xeoma/camera.py index 2ca4aab7aff..d6f313c0382 100644 --- a/homeassistant/components/xeoma/camera.py +++ b/homeassistant/components/xeoma/camera.py @@ -1,6 +1,7 @@ """Support for Xeoma Cameras.""" import logging +from pyxeoma.xeoma import Xeoma, XeomaError import voluptuous as vol from homeassistant.components.camera import PLATFORM_SCHEMA, Camera @@ -40,7 +41,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Discover and setup Xeoma Cameras.""" - from pyxeoma.xeoma import Xeoma, XeomaError host = config[CONF_HOST] login = config.get(CONF_USERNAME) @@ -111,7 +111,6 @@ class XeomaCamera(Camera): async def async_camera_image(self): """Return a still image response from the camera.""" - from pyxeoma.xeoma import XeomaError try: image = await self._xeoma.async_get_camera_image( diff --git a/homeassistant/components/xfinity/device_tracker.py b/homeassistant/components/xfinity/device_tracker.py index 93603ae5797..20e13682979 100644 --- a/homeassistant/components/xfinity/device_tracker.py +++ b/homeassistant/components/xfinity/device_tracker.py @@ -3,14 +3,15 @@ import logging from requests.exceptions import RequestException import voluptuous as vol +from xfinity_gateway import XfinityGateway -import homeassistant.helpers.config_validation as cv 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__) @@ -23,7 +24,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def get_scanner(hass, config): """Validate the configuration and return an Xfinity Gateway scanner.""" - from xfinity_gateway import XfinityGateway gateway = XfinityGateway(config[DOMAIN][CONF_HOST]) scanner = None @@ -32,7 +32,7 @@ def get_scanner(hass, config): scanner = XfinityDeviceScanner(gateway) except (RequestException, ValueError): _LOGGER.error( - "Error communicating with Xfinity Gateway. " "Check host: %s", gateway.host + "Error communicating with Xfinity Gateway. Check host: %s", gateway.host ) return scanner @@ -51,7 +51,7 @@ class XfinityDeviceScanner(DeviceScanner): try: connected_devices = self.gateway.scan_devices() except (RequestException, ValueError): - _LOGGER.error("Unable to scan devices. " "Check connection to gateway") + _LOGGER.error("Unable to scan devices. Check connection to gateway") return connected_devices def get_device_name(self, device): diff --git a/homeassistant/components/xiaomi/camera.py b/homeassistant/components/xiaomi/camera.py index 363c17fe4a9..cc85f17146b 100644 --- a/homeassistant/components/xiaomi/camera.py +++ b/homeassistant/components/xiaomi/camera.py @@ -1,20 +1,23 @@ """This component provides support for Xiaomi Cameras.""" import asyncio +from ftplib import FTP, error_perm import logging +from haffmpeg.camera import CameraMjpeg +from haffmpeg.tools import IMAGE_JPEG, ImageFrame import voluptuous as vol -from homeassistant.components.camera import Camera, PLATFORM_SCHEMA -from homeassistant.exceptions import TemplateError +from homeassistant.components.camera import PLATFORM_SCHEMA, Camera from homeassistant.components.ffmpeg import DATA_FFMPEG from homeassistant.const import ( CONF_HOST, CONF_NAME, - CONF_PATH, CONF_PASSWORD, + CONF_PATH, CONF_PORT, CONF_USERNAME, ) +from homeassistant.exceptions import TemplateError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream @@ -88,7 +91,6 @@ class XiaomiCamera(Camera): def get_latest_video_url(self, host): """Retrieve the latest video file from the Xiaomi Camera FTP server.""" - from ftplib import FTP, error_perm ftp = FTP(host) try: @@ -140,7 +142,6 @@ class XiaomiCamera(Camera): async def async_camera_image(self): """Return a still image response from the camera.""" - from haffmpeg.tools import ImageFrame, IMAGE_JPEG try: host = self.host.async_render() @@ -162,7 +163,6 @@ class XiaomiCamera(Camera): async def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from the camera.""" - from haffmpeg.camera import CameraMjpeg stream = CameraMjpeg(self._manager.binary, loop=self.hass.loop) await stream.open_camera(self._last_url, extra_cmd=self._extra_arguments) diff --git a/homeassistant/components/xiaomi/device_tracker.py b/homeassistant/components/xiaomi/device_tracker.py index dbc647f4982..df16b13b931 100644 --- a/homeassistant/components/xiaomi/device_tracker.py +++ b/homeassistant/components/xiaomi/device_tracker.py @@ -4,13 +4,13 @@ import logging import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/xiaomi_aqara/__init__.py b/homeassistant/components/xiaomi_aqara/__init__.py index 7a337dcc497..ae032a8b35f 100644 --- a/homeassistant/components/xiaomi_aqara/__init__.py +++ b/homeassistant/components/xiaomi_aqara/__init__.py @@ -1,9 +1,9 @@ """Support for Xiaomi Gateways.""" +from datetime import timedelta import logging -from datetime import timedelta - import voluptuous as vol +from xiaomi_gateway import XiaomiGatewayDiscovery from homeassistant.components.discovery import SERVICE_XIAOMI_GW from homeassistant.const import ( @@ -135,8 +135,6 @@ def setup(hass, config): discovery.listen(hass, SERVICE_XIAOMI_GW, xiaomi_gw_discovered) - from xiaomi_gateway import XiaomiGatewayDiscovery - xiaomi = hass.data[PY_XIAOMI_GATEWAY] = XiaomiGatewayDiscovery( hass.add_job, gateways, interface ) diff --git a/homeassistant/components/xiaomi_miio/air_quality.py b/homeassistant/components/xiaomi_miio/air_quality.py index 3824c5b88cd..f5e7e476ac5 100644 --- a/homeassistant/components/xiaomi_miio/air_quality.py +++ b/homeassistant/components/xiaomi_miio/air_quality.py @@ -3,9 +3,9 @@ from miio import AirQualityMonitor, Device, DeviceException import voluptuous as vol from homeassistant.components.air_quality import ( - AirQualityEntity, - PLATFORM_SCHEMA, _LOGGER, + PLATFORM_SCHEMA, + AirQualityEntity, ) from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN from homeassistant.exceptions import PlatformNotReady diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py new file mode 100644 index 00000000000..f8be37b313c --- /dev/null +++ b/homeassistant/components/xiaomi_miio/const.py @@ -0,0 +1,48 @@ +"""Constants for the Xiaomi Miio component.""" +DOMAIN = "xiaomi_miio" + +# Fan Services +SERVICE_SET_BUZZER_ON = "fan_set_buzzer_on" +SERVICE_SET_BUZZER_OFF = "fan_set_buzzer_off" +SERVICE_SET_LED_ON = "fan_set_led_on" +SERVICE_SET_LED_OFF = "fan_set_led_off" +SERVICE_SET_CHILD_LOCK_ON = "fan_set_child_lock_on" +SERVICE_SET_CHILD_LOCK_OFF = "fan_set_child_lock_off" +SERVICE_SET_LED_BRIGHTNESS = "fan_set_led_brightness" +SERVICE_SET_FAVORITE_LEVEL = "fan_set_favorite_level" +SERVICE_SET_AUTO_DETECT_ON = "fan_set_auto_detect_on" +SERVICE_SET_AUTO_DETECT_OFF = "fan_set_auto_detect_off" +SERVICE_SET_LEARN_MODE_ON = "fan_set_learn_mode_on" +SERVICE_SET_LEARN_MODE_OFF = "fan_set_learn_mode_off" +SERVICE_SET_VOLUME = "fan_set_volume" +SERVICE_RESET_FILTER = "fan_reset_filter" +SERVICE_SET_EXTRA_FEATURES = "fan_set_extra_features" +SERVICE_SET_TARGET_HUMIDITY = "fan_set_target_humidity" +SERVICE_SET_DRY_ON = "fan_set_dry_on" +SERVICE_SET_DRY_OFF = "fan_set_dry_off" + +# Light Services +SERVICE_SET_SCENE = "light_set_scene" +SERVICE_SET_DELAYED_TURN_OFF = "light_set_delayed_turn_off" +SERVICE_REMINDER_ON = "light_reminder_on" +SERVICE_REMINDER_OFF = "light_reminder_off" +SERVICE_NIGHT_LIGHT_MODE_ON = "light_night_light_mode_on" +SERVICE_NIGHT_LIGHT_MODE_OFF = "light_night_light_mode_off" +SERVICE_EYECARE_MODE_ON = "light_eyecare_mode_on" +SERVICE_EYECARE_MODE_OFF = "light_eyecare_mode_off" + +# Remote Services +SERVICE_LEARN = "remote_learn_command" + +# Switch Services +SERVICE_SET_WIFI_LED_ON = "switch_set_wifi_led_on" +SERVICE_SET_WIFI_LED_OFF = "switch_set_wifi_led_off" +SERVICE_SET_POWER_MODE = "switch_set_power_mode" +SERVICE_SET_POWER_PRICE = "switch_set_power_price" + +# Vacuum Services +SERVICE_MOVE_REMOTE_CONTROL = "vacuum_remote_control_move" +SERVICE_MOVE_REMOTE_CONTROL_STEP = "vacuum_remote_control_move_step" +SERVICE_START_REMOTE_CONTROL = "vacuum_remote_control_start" +SERVICE_STOP_REMOTE_CONTROL = "vacuum_remote_control_stop" +SERVICE_CLEAN_ZONE = "vacuum_clean_zone" diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index 9e496893d56..7cb45296506 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -4,7 +4,6 @@ from enum import Enum from functools import partial import logging -import voluptuous as vol from miio import ( # pylint: disable=import-error AirFresh, AirHumidifier, @@ -12,26 +11,21 @@ from miio import ( # pylint: disable=import-error Device, DeviceException, ) - -from miio.airfresh import ( # pylint: disable=import-error; pylint: disable=import-error +from miio.airfresh import ( # pylint: disable=import-error, import-error LedBrightness as AirfreshLedBrightness, OperationMode as AirfreshOperationMode, ) -from miio.airhumidifier import ( # pylint: disable=import-error; pylint: disable=import-error +from miio.airhumidifier import ( # pylint: disable=import-error, import-error LedBrightness as AirhumidifierLedBrightness, OperationMode as AirhumidifierOperationMode, ) -from miio.airpurifier import ( # pylint: disable=import-error; pylint: disable=import-error +from miio.airpurifier import ( # pylint: disable=import-error, import-error LedBrightness as AirpurifierLedBrightness, OperationMode as AirpurifierOperationMode, ) +import voluptuous as vol -from homeassistant.components.fan import ( - DOMAIN, - PLATFORM_SCHEMA, - SUPPORT_SET_SPEED, - FanEntity, -) +from homeassistant.components.fan import PLATFORM_SCHEMA, SUPPORT_SET_SPEED, FanEntity from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_MODE, @@ -42,6 +36,28 @@ from homeassistant.const import ( from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv +from .const import ( + DOMAIN, + SERVICE_RESET_FILTER, + SERVICE_SET_AUTO_DETECT_OFF, + SERVICE_SET_AUTO_DETECT_ON, + SERVICE_SET_BUZZER_OFF, + SERVICE_SET_BUZZER_ON, + SERVICE_SET_CHILD_LOCK_OFF, + SERVICE_SET_CHILD_LOCK_ON, + SERVICE_SET_DRY_OFF, + SERVICE_SET_DRY_ON, + SERVICE_SET_EXTRA_FEATURES, + SERVICE_SET_FAVORITE_LEVEL, + SERVICE_SET_LEARN_MODE_OFF, + SERVICE_SET_LEARN_MODE_ON, + SERVICE_SET_LED_BRIGHTNESS, + SERVICE_SET_LED_OFF, + SERVICE_SET_LED_ON, + SERVICE_SET_TARGET_HUMIDITY, + SERVICE_SET_VOLUME, +) + _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Xiaomi Miio Device" @@ -368,25 +384,6 @@ FEATURE_FLAGS_AIRFRESH = ( | FEATURE_SET_EXTRA_FEATURES ) -SERVICE_SET_BUZZER_ON = "xiaomi_miio_set_buzzer_on" -SERVICE_SET_BUZZER_OFF = "xiaomi_miio_set_buzzer_off" -SERVICE_SET_LED_ON = "xiaomi_miio_set_led_on" -SERVICE_SET_LED_OFF = "xiaomi_miio_set_led_off" -SERVICE_SET_CHILD_LOCK_ON = "xiaomi_miio_set_child_lock_on" -SERVICE_SET_CHILD_LOCK_OFF = "xiaomi_miio_set_child_lock_off" -SERVICE_SET_LED_BRIGHTNESS = "xiaomi_miio_set_led_brightness" -SERVICE_SET_FAVORITE_LEVEL = "xiaomi_miio_set_favorite_level" -SERVICE_SET_AUTO_DETECT_ON = "xiaomi_miio_set_auto_detect_on" -SERVICE_SET_AUTO_DETECT_OFF = "xiaomi_miio_set_auto_detect_off" -SERVICE_SET_LEARN_MODE_ON = "xiaomi_miio_set_learn_mode_on" -SERVICE_SET_LEARN_MODE_OFF = "xiaomi_miio_set_learn_mode_off" -SERVICE_SET_VOLUME = "xiaomi_miio_set_volume" -SERVICE_RESET_FILTER = "xiaomi_miio_reset_filter" -SERVICE_SET_EXTRA_FEATURES = "xiaomi_miio_set_extra_features" -SERVICE_SET_TARGET_HUMIDITY = "xiaomi_miio_set_target_humidity" -SERVICE_SET_DRY_ON = "xiaomi_miio_set_dry_on" -SERVICE_SET_DRY_OFF = "xiaomi_miio_set_dry_off" - AIRPURIFIER_SERVICE_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_ids}) SERVICE_SCHEMA_LED_BRIGHTNESS = AIRPURIFIER_SERVICE_SCHEMA.extend( diff --git a/homeassistant/components/xiaomi_miio/light.py b/homeassistant/components/xiaomi_miio/light.py index 5b454512f33..bcc83bae454 100644 --- a/homeassistant/components/xiaomi_miio/light.py +++ b/homeassistant/components/xiaomi_miio/light.py @@ -21,7 +21,6 @@ from homeassistant.components.light import ( ATTR_COLOR_TEMP, ATTR_ENTITY_ID, ATTR_HS_COLOR, - DOMAIN, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, @@ -33,6 +32,18 @@ from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.util import color, dt +from .const import ( + DOMAIN, + SERVICE_EYECARE_MODE_OFF, + SERVICE_EYECARE_MODE_ON, + SERVICE_NIGHT_LIGHT_MODE_OFF, + SERVICE_NIGHT_LIGHT_MODE_ON, + SERVICE_REMINDER_OFF, + SERVICE_REMINDER_ON, + SERVICE_SET_DELAYED_TURN_OFF, + SERVICE_SET_SCENE, +) + _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Xiaomi Philips Light" @@ -85,15 +96,6 @@ ATTR_TOTAL_ASSISTANT_SLEEP_TIME = "total_assistant_sleep_time" ATTR_BRAND_SLEEP = "brand_sleep" ATTR_BRAND = "brand" -SERVICE_SET_SCENE = "xiaomi_miio_set_scene" -SERVICE_SET_DELAYED_TURN_OFF = "xiaomi_miio_set_delayed_turn_off" -SERVICE_REMINDER_ON = "xiaomi_miio_reminder_on" -SERVICE_REMINDER_OFF = "xiaomi_miio_reminder_off" -SERVICE_NIGHT_LIGHT_MODE_ON = "xiaomi_miio_night_light_mode_on" -SERVICE_NIGHT_LIGHT_MODE_OFF = "xiaomi_miio_night_light_mode_off" -SERVICE_EYECARE_MODE_ON = "xiaomi_miio_eyecare_mode_on" -SERVICE_EYECARE_MODE_OFF = "xiaomi_miio_eyecare_mode_off" - XIAOMI_MIIO_SERVICE_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_ids}) SERVICE_SCHEMA_SET_SCENE = XIAOMI_MIIO_SERVICE_SCHEMA.extend( @@ -465,7 +467,7 @@ class XiaomiPhilipsBulb(XiaomiPhilipsGenericLight): ) result = await self._try_command( - "Setting brightness and color temperature failed: " "%s bri, %s cct", + "Setting brightness and color temperature failed: %s bri, %s cct", self._light.set_brightness_and_color_temperature, percent_brightness, percent_color_temp, @@ -477,7 +479,7 @@ class XiaomiPhilipsBulb(XiaomiPhilipsGenericLight): elif ATTR_COLOR_TEMP in kwargs: _LOGGER.debug( - "Setting color temperature: " "%s mireds, %s%% cct", + "Setting color temperature: %s mireds, %s%% cct", color_temp, percent_color_temp, ) @@ -823,14 +825,14 @@ class XiaomiPhilipsMoonlightLamp(XiaomiPhilipsBulb): if ATTR_BRIGHTNESS in kwargs and ATTR_HS_COLOR in kwargs: _LOGGER.debug( - "Setting brightness and color: " "%s %s%%, %s", + "Setting brightness and color: %s %s%%, %s", brightness, percent_brightness, rgb, ) result = await self._try_command( - "Setting brightness and color failed: " "%s bri, %s color", + "Setting brightness and color failed: %s bri, %s color", self._light.set_brightness_and_rgb, percent_brightness, rgb, @@ -851,7 +853,7 @@ class XiaomiPhilipsMoonlightLamp(XiaomiPhilipsBulb): ) result = await self._try_command( - "Setting brightness and color temperature failed: " "%s bri, %s cct", + "Setting brightness and color temperature failed: %s bri, %s cct", self._light.set_brightness_and_color_temperature, percent_brightness, percent_color_temp, @@ -873,7 +875,7 @@ class XiaomiPhilipsMoonlightLamp(XiaomiPhilipsBulb): elif ATTR_COLOR_TEMP in kwargs: _LOGGER.debug( - "Setting color temperature: " "%s mireds, %s%% cct", + "Setting color temperature: %s mireds, %s%% cct", color_temp, percent_color_temp, ) diff --git a/homeassistant/components/xiaomi_miio/manifest.json b/homeassistant/components/xiaomi_miio/manifest.json index 849e4573bbf..4f2e752feb1 100644 --- a/homeassistant/components/xiaomi_miio/manifest.json +++ b/homeassistant/components/xiaomi_miio/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/xiaomi_miio", "requirements": [ "construct==2.9.45", - "python-miio==0.4.7" + "python-miio==0.4.8" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/xiaomi_miio/remote.py b/homeassistant/components/xiaomi_miio/remote.py index 0e2ac476e05..1e7cada1a7b 100644 --- a/homeassistant/components/xiaomi_miio/remote.py +++ b/homeassistant/components/xiaomi_miio/remote.py @@ -11,7 +11,6 @@ from homeassistant.components.remote import ( ATTR_DELAY_SECS, ATTR_NUM_REPEATS, DEFAULT_DELAY_SECS, - DOMAIN, PLATFORM_SCHEMA, RemoteDevice, ) @@ -28,9 +27,10 @@ from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.util.dt import utcnow +from .const import DOMAIN, SERVICE_LEARN + _LOGGER = logging.getLogger(__name__) -SERVICE_LEARN = "xiaomi_miio_learn_command" DATA_KEY = "remote.xiaomi_miio" CONF_SLOT = "slot" diff --git a/homeassistant/components/xiaomi_miio/services.yaml b/homeassistant/components/xiaomi_miio/services.yaml index e69de29bb2d..36dcbc950be 100644 --- a/homeassistant/components/xiaomi_miio/services.yaml +++ b/homeassistant/components/xiaomi_miio/services.yaml @@ -0,0 +1,308 @@ +fan_set_buzzer_on: + description: Turn the buzzer on. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + +fan_set_buzzer_off: + description: Turn the buzzer off. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + +fan_set_led_on: + description: Turn the led on. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + +fan_set_led_off: + description: Turn the led off. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + +fan_set_child_lock_on: + description: Turn the child lock on. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + +fan_set_child_lock_off: + description: Turn the child lock off. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + +fan_set_favorite_level: + description: Set the favorite level. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + level: + description: Level, between 0 and 16. + example: 1 + +fan_set_led_brightness: + description: Set the led brightness. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + brightness: + description: Brightness (0 = Bright, 1 = Dim, 2 = Off) + example: 1 + +fan_set_auto_detect_on: + description: Turn the auto detect on. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + +fan_set_auto_detect_off: + description: Turn the auto detect off. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + +fan_set_learn_mode_on: + description: Turn the learn mode on. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + +fan_set_learn_mode_off: + description: Turn the learn mode off. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + +fan_set_volume: + description: Set the sound volume. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + volume: + description: Volume, between 0 and 100. + example: 50 + +fan_reset_filter: + description: Reset the filter lifetime and usage. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + +fan_set_extra_features: + description: Manipulates a storage register which advertises extra features. The Mi Home app evaluates the value. A feature called "turbo mode" is unlocked in the app on value 1. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + features: + description: Integer, known values are 0 (default) and 1 (turbo mode). + example: 1 + +fan_set_target_humidity: + description: Set the target humidity. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + humidity: + description: Target humidity. Allowed values are 30, 40, 50, 60, 70 and 80. + example: 50 + +fan_set_dry_on: + description: Turn the dry mode on. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + +fan_set_dry_off: + description: Turn the dry mode off. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + +light_set_scene: + description: Set a fixed scene. + fields: + entity_id: + description: Name of the light entity. + example: "light.xiaomi_miio" + scene: + description: Number of the fixed scene, between 1 and 4. + example: 1 + +light_set_delayed_turn_off: + description: Delayed turn off. + fields: + entity_id: + description: Name of the light entity. + example: "light.xiaomi_miio" + time_period: + description: Time period for the delayed turn off. + example: "5, '0:05', {'minutes': 5}" + +light_reminder_on: + description: Enable the eye fatigue reminder/notification (EYECARE SMART LAMP 2 ONLY). + fields: + entity_id: + description: 'Name of the entity to act on.' + example: 'light.xiaomi_miio' + +light_reminder_off: + description: Disable the eye fatigue reminder/notification (EYECARE SMART LAMP 2 ONLY). + fields: + entity_id: + description: 'Name of the entity to act on.' + example: 'light.xiaomi_miio' + +light_night_light_mode_on: + description: Turn the eyecare mode on (EYECARE SMART LAMP 2 ONLY). + fields: + entity_id: + description: 'Name of the entity to act on.' + example: 'light.xiaomi_miio' + +light_night_light_mode_off: + description: Turn the eyecare mode fan_set_dry_off (EYECARE SMART LAMP 2 ONLY). + fields: + entity_id: + description: 'Name of the entity to act on.' + example: 'light.xiaomi_miio' + +light_eyecare_mode_on: + description: Enable the eye fatigue reminder/notification (EYECARE SMART LAMP 2 ONLY). + fields: + entity_id: + description: 'Name of the entity to act on.' + example: 'light.xiaomi_miio' + +light_eyecare_mode_off: + description: Disable the eye fatigue reminder/notification (EYECARE SMART LAMP 2 ONLY). + fields: + entity_id: + description: 'Name of the entity to act on.' + example: 'light.xiaomi_miio' + +remote_learn_command: + description: 'Learn an IR command, press "Call Service", point the remote at the IR device, and the learned command will be shown as a notification in Overview.' + fields: + entity_id: + description: 'Name of the entity to learn command from.' + example: 'remote.xiaomi_miio' + slot: + description: 'Define the slot used to save the IR command (Value from 1 to 1000000)' + example: '1' + timeout: + description: 'Define the timeout in seconds, before which the command must be learned.' + example: '30' + +switch_set_wifi_led_on: + description: Turn the wifi led on. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'switch.xiaomi_miio_device' + +switch_set_wifi_led_off: + description: Turn the wifi led off. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'switch.xiaomi_miio_device' + +switch_set_power_price: + description: Set the power price. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'switch.xiaomi_miio_device' + mode: + description: Power price, between 0 and 999. + example: 31 + +switch_set_power_mode: + description: Set the power mode. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'switch.xiaomi_miio_device' + mode: + description: Power mode, valid values are 'normal' and 'green'. + example: 'green' + +vacuum_remote_control_start: + description: Start remote control of the vacuum cleaner. You can then move it with `remote_control_move`, when done call `remote_control_stop`. + fields: + entity_id: + description: Name of the vacuum entity. + example: 'vacuum.xiaomi_vacuum_cleaner' + +vacuum_remote_control_stop: + description: Stop remote control mode of the vacuum cleaner. + fields: + entity_id: + description: Name of the vacuum entity. + example: 'vacuum.xiaomi_vacuum_cleaner' + +vacuum_remote_control_move: + description: Remote control the vacuum cleaner, make sure you first set it in remote control mode with `remote_control_start`. + fields: + entity_id: + description: Name of the vacuum entity. + example: 'vacuum.xiaomi_vacuum_cleaner' + velocity: + description: Speed, between -0.29 and 0.29. + example: '0.2' + rotation: + description: Rotation, between -179 degrees and 179 degrees. + example: '90' + duration: + description: Duration of the movement. + example: '1500' + +vacuum_remote_control_move_step: + description: Remote control the vacuum cleaner, only makes one move and then stops. + fields: + entity_id: + description: Name of the vacuum entity. + example: 'vacuum.xiaomi_vacuum_cleaner' + velocity: + description: Speed, between -0.29 and 0.29. + example: '0.2' + rotation: + description: Rotation, between -179 degrees and 179 degrees. + example: '90' + duration: + description: Duration of the movement. + example: '1500' + +vacuum_clean_zone: + description: Start the cleaning operation in the selected areas for the number of repeats indicated. + fields: + entity_id: + description: Name of the vacuum entity. + example: 'vacuum.xiaomi_vacuum_cleaner' + zone: + description: Array of zones. Each zone is an array of 4 integer values. + example: '[[23510,25311,25110,26362]]' + repeats: + description: Number of cleaning repeats for each zone between 1 and 3. + example: '1' diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index 023243a1995..63229b851d0 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -13,7 +13,7 @@ from miio import ( # pylint: disable=import-error from miio.powerstrip import PowerMode # pylint: disable=import-error import voluptuous as vol -from homeassistant.components.switch import DOMAIN, PLATFORM_SCHEMA, SwitchDevice +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_MODE, @@ -24,6 +24,14 @@ from homeassistant.const import ( from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv +from .const import ( + DOMAIN, + SERVICE_SET_POWER_MODE, + SERVICE_SET_POWER_PRICE, + SERVICE_SET_WIFI_LED_OFF, + SERVICE_SET_WIFI_LED_ON, +) + _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Xiaomi Miio Switch" @@ -80,11 +88,6 @@ FEATURE_FLAGS_POWER_STRIP_V2 = FEATURE_SET_WIFI_LED | FEATURE_SET_POWER_PRICE FEATURE_FLAGS_PLUG_V3 = FEATURE_SET_WIFI_LED -SERVICE_SET_WIFI_LED_ON = "xiaomi_miio_set_wifi_led_on" -SERVICE_SET_WIFI_LED_OFF = "xiaomi_miio_set_wifi_led_off" -SERVICE_SET_POWER_MODE = "xiaomi_miio_set_power_mode" -SERVICE_SET_POWER_PRICE = "xiaomi_miio_set_power_price" - SERVICE_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_ids}) SERVICE_SCHEMA_POWER_MODE = SERVICE_SCHEMA.extend( diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index b18a54ce97a..4ef34e8ff56 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -8,7 +8,6 @@ import voluptuous as vol from homeassistant.components.vacuum import ( ATTR_CLEANED_AREA, - DOMAIN, PLATFORM_SCHEMA, STATE_CLEANING, STATE_DOCKED, @@ -38,6 +37,15 @@ from homeassistant.const import ( ) import homeassistant.helpers.config_validation as cv +from .const import ( + DOMAIN, + SERVICE_CLEAN_ZONE, + SERVICE_MOVE_REMOTE_CONTROL, + SERVICE_MOVE_REMOTE_CONTROL_STEP, + SERVICE_START_REMOTE_CONTROL, + SERVICE_STOP_REMOTE_CONTROL, +) + _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Xiaomi Vacuum cleaner" @@ -52,13 +60,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( extra=vol.ALLOW_EXTRA, ) -SERVICE_MOVE_REMOTE_CONTROL = "xiaomi_remote_control_move" -SERVICE_MOVE_REMOTE_CONTROL_STEP = "xiaomi_remote_control_move_step" -SERVICE_START_REMOTE_CONTROL = "xiaomi_remote_control_start" -SERVICE_STOP_REMOTE_CONTROL = "xiaomi_remote_control_stop" -SERVICE_CLEAN_ZONE = "xiaomi_clean_zone" - -FAN_SPEEDS = {"Quiet": 38, "Balanced": 60, "Turbo": 77, "Max": 90} +FAN_SPEEDS = {"Quiet": 38, "Balanced": 60, "Turbo": 77, "Max": 90, "Gentle": 105} ATTR_CLEAN_START = "clean_start" ATTR_CLEAN_STOP = "clean_stop" @@ -377,7 +379,7 @@ class MiroboVacuum(StateVacuumDevice): fan_speed = int(fan_speed) except ValueError as exc: _LOGGER.error( - "Fan speed step not recognized (%s). " "Valid speeds are: %s", + "Fan speed step not recognized (%s). Valid speeds are: %s", exc, self.fan_speed_list, ) diff --git a/homeassistant/components/xiaomi_tv/media_player.py b/homeassistant/components/xiaomi_tv/media_player.py index 352ce0c4835..c82708852c2 100644 --- a/homeassistant/components/xiaomi_tv/media_player.py +++ b/homeassistant/components/xiaomi_tv/media_player.py @@ -1,9 +1,10 @@ """Add support for the Xiaomi TVs.""" import logging +import pymitv import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( SUPPORT_TURN_OFF, SUPPORT_TURN_ON, @@ -29,7 +30,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Xiaomi TV platform.""" - from pymitv import Discover # If a hostname is set. Discovery is skipped. host = config.get(CONF_HOST) @@ -37,14 +37,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if host is not None: # Check if there's a valid TV at the IP address. - if not Discover().check_ip(host): + if not pymitv.Discover().check_ip(host): _LOGGER.error("Could not find Xiaomi TV with specified IP: %s", host) else: # Register TV with Home Assistant. add_entities([XiaomiTV(host, name)]) else: # Otherwise, discover TVs on network. - add_entities(XiaomiTV(tv, DEFAULT_NAME) for tv in Discover().scan()) + add_entities(XiaomiTV(tv, DEFAULT_NAME) for tv in pymitv.Discover().scan()) class XiaomiTV(MediaPlayerDevice): @@ -52,11 +52,9 @@ class XiaomiTV(MediaPlayerDevice): def __init__(self, ip, name): """Receive IP address and name to construct class.""" - # Import pymitv library. - from pymitv import TV # Initialize the Xiaomi TV. - self._tv = TV(ip) + self._tv = pymitv.TV(ip) # Default name value, only to be overridden by user. self._name = name self._state = STATE_OFF diff --git a/homeassistant/components/xmpp/notify.py b/homeassistant/components/xmpp/notify.py index 5aa9dbfffd1..28d42698657 100644 --- a/homeassistant/components/xmpp/notify.py +++ b/homeassistant/components/xmpp/notify.py @@ -9,14 +9,20 @@ import string import requests import slixmpp from slixmpp.exceptions import IqError, IqTimeout, XMPPError -from slixmpp.xmlstream.xmlstream import NotConnectedError from slixmpp.plugins.xep_0363.http_upload import ( FileTooBig, FileUploadError, UploadServiceNotFound, ) +from slixmpp.xmlstream.xmlstream import NotConnectedError import voluptuous as vol +from homeassistant.components.notify import ( + ATTR_TITLE, + ATTR_TITLE_DEFAULT, + PLATFORM_SCHEMA, + BaseNotificationService, +) from homeassistant.const import ( CONF_PASSWORD, CONF_RECIPIENT, @@ -27,13 +33,6 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv import homeassistant.helpers.template as template_helper -from homeassistant.components.notify import ( - ATTR_TITLE, - ATTR_TITLE_DEFAULT, - PLATFORM_SCHEMA, - BaseNotificationService, -) - _LOGGER = logging.getLogger(__name__) ATTR_DATA = "data" @@ -190,7 +189,6 @@ async def async_send_message( message = self.Message(sto=recipient, stype="chat") message["body"] = url - # pylint: disable=invalid-sequence-index message["oob"]["url"] = url try: message.send() @@ -203,7 +201,7 @@ async def async_send_message( except FileTooBig as ex: _LOGGER.error("File too big for server, could not upload file %s", ex) except UploadServiceNotFound as ex: - _LOGGER.error("UploadServiceNotFound: " " could not upload file %s", ex) + _LOGGER.error("UploadServiceNotFound, could not upload file %s", ex) except FileUploadError as ex: _LOGGER.error("FileUploadError, could not upload file %s", ex) except requests.exceptions.SSLError as ex: diff --git a/homeassistant/components/xs1/__init__.py b/homeassistant/components/xs1/__init__.py index aeb6204265b..1fbcb49d0c9 100644 --- a/homeassistant/components/xs1/__init__.py +++ b/homeassistant/components/xs1/__init__.py @@ -63,8 +63,7 @@ def setup(hass, config): ) except ConnectionError as error: _LOGGER.error( - "Failed to create XS1 API client " "because of a connection error: %s", - error, + "Failed to create XS1 API client because of a connection error: %s", error, ) return False diff --git a/homeassistant/components/xs1/climate.py b/homeassistant/components/xs1/climate.py index 95ad10539bd..33c778c0d3d 100644 --- a/homeassistant/components/xs1/climate.py +++ b/homeassistant/components/xs1/climate.py @@ -5,8 +5,8 @@ from xs1_api_client.api_constants import ActuatorType from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( - SUPPORT_TARGET_TEMPERATURE, HVAC_MODE_HEAT, + SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ATTR_TEMPERATURE diff --git a/homeassistant/components/yale_smart_alarm/alarm_control_panel.py b/homeassistant/components/yale_smart_alarm/alarm_control_panel.py index 094263a658b..05823a511dd 100644 --- a/homeassistant/components/yale_smart_alarm/alarm_control_panel.py +++ b/homeassistant/components/yale_smart_alarm/alarm_control_panel.py @@ -2,15 +2,26 @@ import logging import voluptuous as vol +from yalesmartalarmclient.client import ( + YALE_STATE_ARM_FULL, + YALE_STATE_ARM_PARTIAL, + YALE_STATE_DISARM, + AuthenticationError, + YaleSmartAlarmClient, +) from homeassistant.components.alarm_control_panel import ( - AlarmControlPanel, PLATFORM_SCHEMA, + AlarmControlPanel, +) +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, ) from homeassistant.const import ( + CONF_NAME, CONF_PASSWORD, CONF_USERNAME, - CONF_NAME, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, @@ -42,8 +53,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): password = config[CONF_PASSWORD] area_id = config[CONF_AREA_ID] - from yalesmartalarmclient.client import YaleSmartAlarmClient, AuthenticationError - try: client = YaleSmartAlarmClient(username, password, area_id) except AuthenticationError: @@ -62,12 +71,6 @@ class YaleAlarmDevice(AlarmControlPanel): self._client = client self._state = None - from yalesmartalarmclient.client import ( - YALE_STATE_DISARM, - YALE_STATE_ARM_PARTIAL, - YALE_STATE_ARM_FULL, - ) - self._state_map = { YALE_STATE_DISARM: STATE_ALARM_DISARMED, YALE_STATE_ARM_PARTIAL: STATE_ALARM_ARMED_HOME, @@ -84,6 +87,11 @@ class YaleAlarmDevice(AlarmControlPanel): """Return the state of the device.""" return self._state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY + def update(self): """Return the state of the device.""" armed_status = self._client.get_armed_status() diff --git a/homeassistant/components/yamaha/const.py b/homeassistant/components/yamaha/const.py new file mode 100644 index 00000000000..e2a0c5eceea --- /dev/null +++ b/homeassistant/components/yamaha/const.py @@ -0,0 +1,3 @@ +"""Constants for the Yamaha component.""" +DOMAIN = "yamaha" +SERVICE_ENABLE_OUTPUT = "enable_output" diff --git a/homeassistant/components/yamaha/media_player.py b/homeassistant/components/yamaha/media_player.py index eabb1ef34f1..7ab7d5b3a47 100644 --- a/homeassistant/components/yamaha/media_player.py +++ b/homeassistant/components/yamaha/media_player.py @@ -5,24 +5,22 @@ import requests import rxv import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( - DOMAIN, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, + SUPPORT_SELECT_SOUND_MODE, SUPPORT_SELECT_SOURCE, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - SUPPORT_SELECT_SOUND_MODE, ) - from homeassistant.const import ( ATTR_ENTITY_ID, CONF_HOST, @@ -34,6 +32,8 @@ from homeassistant.const import ( ) import homeassistant.helpers.config_validation as cv +from .const import DOMAIN, SERVICE_ENABLE_OUTPUT + _LOGGER = logging.getLogger(__name__) ATTR_ENABLED = "enabled" @@ -53,8 +53,6 @@ ENABLE_OUTPUT_SCHEMA = MEDIA_PLAYER_SCHEMA.extend( {vol.Required(ATTR_ENABLED): cv.boolean, vol.Required(ATTR_PORT): cv.string} ) -SERVICE_ENABLE_OUTPUT = "yamaha_enable_output" - SUPPORT_YAMAHA = ( SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE diff --git a/homeassistant/components/yamaha/services.yaml b/homeassistant/components/yamaha/services.yaml index e69de29bb2d..592a1d1342e 100644 --- a/homeassistant/components/yamaha/services.yaml +++ b/homeassistant/components/yamaha/services.yaml @@ -0,0 +1,12 @@ +enable_output: + description: Enable or disable an output port + fields: + entity_id: + description: Name(s) of entites to enable/disable port on. + example: 'media_player.yamaha' + port: + description: Name of port to enable/disable. + example: 'hdmi1' + enabled: + description: Boolean indicating if port should be enabled or not. + example: true \ No newline at end of file diff --git a/homeassistant/components/yamaha_musiccast/media_player.py b/homeassistant/components/yamaha_musiccast/media_player.py index 18b80cc4085..ae5b78b9116 100644 --- a/homeassistant/components/yamaha_musiccast/media_player.py +++ b/homeassistant/components/yamaha_musiccast/media_player.py @@ -1,11 +1,11 @@ """Support for Yamaha MusicCast Receivers.""" import logging - import socket + import pymusiccast import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, diff --git a/homeassistant/components/yandex_transport/sensor.py b/homeassistant/components/yandex_transport/sensor.py index 4bf634a61f4..54db4882e74 100644 --- a/homeassistant/components/yandex_transport/sensor.py +++ b/homeassistant/components/yandex_transport/sensor.py @@ -1,16 +1,16 @@ """Service for obtaining information about closer bus from Transport Yandex Service.""" -import logging from datetime import timedelta +import logging import voluptuous as vol from ya_ma import YandexMapsRequester -import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt_util from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION, DEVICE_CLASS_TIMESTAMP +from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, DEVICE_CLASS_TIMESTAMP +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index c899c811a47..b947f6b448c 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -1,24 +1,25 @@ """Support for Xiaomi Yeelight WiFi color bulb.""" -import logging from datetime import timedelta +import logging import voluptuous as vol from yeelight import Bulb, BulbException + +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.discovery import SERVICE_YEELIGHT +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_DEVICES, + CONF_HOST, CONF_NAME, CONF_SCAN_INTERVAL, - CONF_HOST, - ATTR_ENTITY_ID, ) -from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN -from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.helpers import discovery -from homeassistant.helpers.discovery import load_platform import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import dispatcher_send, dispatcher_connect +from homeassistant.helpers.discovery import load_platform +from homeassistant.helpers.dispatcher import dispatcher_connect, dispatcher_send from homeassistant.helpers.event import track_time_interval _LOGGER = logging.getLogger(__name__) @@ -48,6 +49,7 @@ ACTION_STAY = "stay" ACTION_OFF = "off" ACTIVE_MODE_NIGHTLIGHT = "1" +ACTIVE_COLOR_FLOWING = "1" NIGHTLIGHT_SWITCH_TYPE_LIGHT = "light" @@ -123,6 +125,7 @@ UPDATE_REQUEST_PROPERTIES = [ "hue", "sat", "color_mode", + "flowing", "bg_power", "bg_lmode", "bg_flowing", @@ -250,10 +253,19 @@ class YeelightDevice: return self._active_mode is not None + @property + def is_color_flow_enabled(self) -> bool: + """Return true / false if color flow is currently running.""" + return self._color_flow == ACTIVE_COLOR_FLOWING + @property def _active_mode(self): return self.bulb.last_properties.get("active_mode") + @property + def _color_flow(self): + return self.bulb.last_properties.get("flowing") + @property def type(self): """Return bulb type.""" diff --git a/homeassistant/components/yeelight/binary_sensor.py b/homeassistant/components/yeelight/binary_sensor.py index da39152e9ca..29e24b510e5 100644 --- a/homeassistant/components/yeelight/binary_sensor.py +++ b/homeassistant/components/yeelight/binary_sensor.py @@ -4,7 +4,8 @@ import logging from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from . import DATA_YEELIGHT, DATA_UPDATED + +from . import DATA_UPDATED, DATA_YEELIGHT _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index 772fb00977b..c40ead2a892 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -4,60 +4,61 @@ import logging import voluptuous as vol import yeelight from yeelight import ( + BulbException, + Flow, RGBTransition, SleepTransition, - Flow, - BulbException, transitions as yee_transitions, ) -from yeelight.enums import PowerMode, LightType, BulbType, SceneClass +from yeelight.enums import BulbType, LightType, PowerMode, SceneClass -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.service import extract_entity_ids -import homeassistant.helpers.config_validation as cv -from homeassistant.util.color import ( - color_temperature_mired_to_kelvin as mired_to_kelvin, - color_temperature_kelvin_to_mired as kelvin_to_mired, -) -from homeassistant.const import CONF_HOST, ATTR_ENTITY_ID, ATTR_MODE, CONF_NAME -from homeassistant.core import callback from homeassistant.components.light import ( ATTR_BRIGHTNESS, - ATTR_HS_COLOR, - ATTR_TRANSITION, ATTR_COLOR_TEMP, - ATTR_FLASH, - FLASH_SHORT, - FLASH_LONG, ATTR_EFFECT, + ATTR_FLASH, + ATTR_HS_COLOR, + ATTR_KELVIN, + ATTR_RGB_COLOR, + ATTR_TRANSITION, + FLASH_LONG, + FLASH_SHORT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, - SUPPORT_TRANSITION, SUPPORT_COLOR_TEMP, - SUPPORT_FLASH, SUPPORT_EFFECT, + SUPPORT_FLASH, + SUPPORT_TRANSITION, Light, - ATTR_RGB_COLOR, - ATTR_KELVIN, ) +from homeassistant.const import ATTR_ENTITY_ID, ATTR_MODE, CONF_HOST, CONF_NAME +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.service import extract_entity_ids import homeassistant.util.color as color_util +from homeassistant.util.color import ( + color_temperature_kelvin_to_mired as kelvin_to_mired, + color_temperature_mired_to_kelvin as mired_to_kelvin, +) + from . import ( - CONF_TRANSITION, - DATA_YEELIGHT, - CONF_MODE_MUSIC, - CONF_SAVE_ON_CHANGE, - CONF_CUSTOM_EFFECTS, - DATA_UPDATED, - YEELIGHT_SERVICE_SCHEMA, - DOMAIN, - ATTR_TRANSITIONS, - YEELIGHT_FLOW_TRANSITION_SCHEMA, ACTION_RECOVER, - CONF_FLOW_PARAMS, ATTR_ACTION, ATTR_COUNT, - NIGHTLIGHT_SWITCH_TYPE_LIGHT, + ATTR_TRANSITIONS, + CONF_CUSTOM_EFFECTS, + CONF_FLOW_PARAMS, + CONF_MODE_MUSIC, CONF_NIGHTLIGHT_SWITCH_TYPE, + CONF_SAVE_ON_CHANGE, + CONF_TRANSITION, + DATA_UPDATED, + DATA_YEELIGHT, + DOMAIN, + NIGHTLIGHT_SWITCH_TYPE_LIGHT, + YEELIGHT_FLOW_TRANSITION_SCHEMA, + YEELIGHT_SERVICE_SCHEMA, ) _LOGGER = logging.getLogger(__name__) @@ -141,6 +142,21 @@ MODEL_TO_DEVICE_TYPE = { "ceiling4": BulbType.WhiteTempMood, } +EFFECTS_MAP = { + EFFECT_DISCO: yee_transitions.disco, + EFFECT_TEMP: yee_transitions.temp, + EFFECT_STROBE: yee_transitions.strobe, + EFFECT_STROBE_COLOR: yee_transitions.strobe_color, + EFFECT_ALARM: yee_transitions.alarm, + EFFECT_POLICE: yee_transitions.police, + EFFECT_POLICE2: yee_transitions.police2, + EFFECT_CHRISTMAS: yee_transitions.christmas, + EFFECT_RGB: yee_transitions.rgb, + EFFECT_RANDOM_LOOP: yee_transitions.randomloop, + EFFECT_LSD: yee_transitions.lsd, + EFFECT_SLOWDOWN: yee_transitions.slowdown, +} + VALID_BRIGHTNESS = vol.All(vol.Coerce(int), vol.Range(min=1, max=100)) SERVICE_SCHEMA_SET_MODE = YEELIGHT_SERVICE_SCHEMA.extend( @@ -282,7 +298,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): else: _lights_setup_helper(YeelightGenericLight) _LOGGER.warning( - "Cannot determine device type for %s, %s. " "Falling back to white only", + "Cannot determine device type for %s, %s. Falling back to white only", device.ipaddr, device.name, ) @@ -415,6 +431,7 @@ class YeelightGenericLight(Light): self._brightness = None self._color_temp = None self._hs = None + self._effect = None model_specs = self._bulb.get_model_specs() self._min_mireds = kelvin_to_mired(model_specs["color_temp"]["max"]) @@ -515,6 +532,11 @@ class YeelightGenericLight(Light): """Return the color property.""" return self._hs + @property + 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 @@ -545,6 +567,16 @@ class YeelightGenericLight(Light): def _predefined_effects(self): return YEELIGHT_MONO_EFFECT_LIST + @property + def device_state_attributes(self): + """Return the device specific state attributes.""" + + attributes = {"flowing": self.device.is_color_flow_enabled} + if self.device.is_nightlight_supported: + attributes["night_light"] = self.device.is_nightlight_enabled + + return attributes + @property def device(self): """Return yeelight device.""" @@ -553,6 +585,8 @@ class YeelightGenericLight(Light): def update(self): """Update light properties.""" self._hs = self._get_hs_from_properties() + if not self.device.is_color_flow_enabled: + self._effect = None def _get_hs_from_properties(self): rgb = self._get_property("rgb") @@ -657,45 +691,33 @@ class YeelightGenericLight(Light): @_cmd def set_effect(self, effect) -> None: """Activate effect.""" - if effect: - if effect == EFFECT_STOP: - self._bulb.stop_flow(light_type=self.light_type) - return + if not effect: + return - effects_map = { - EFFECT_DISCO: yee_transitions.disco, - EFFECT_TEMP: yee_transitions.temp, - EFFECT_STROBE: yee_transitions.strobe, - EFFECT_STROBE_COLOR: yee_transitions.strobe_color, - EFFECT_ALARM: yee_transitions.alarm, - EFFECT_POLICE: yee_transitions.police, - EFFECT_POLICE2: yee_transitions.police2, - EFFECT_CHRISTMAS: yee_transitions.christmas, - EFFECT_RGB: yee_transitions.rgb, - EFFECT_RANDOM_LOOP: yee_transitions.randomloop, - EFFECT_LSD: yee_transitions.lsd, - EFFECT_SLOWDOWN: yee_transitions.slowdown, - } + if effect == EFFECT_STOP: + self._bulb.stop_flow(light_type=self.light_type) + return - if effect in self.custom_effects_names: - flow = Flow(**self.custom_effects[effect]) - elif effect in effects_map: - flow = Flow(count=0, transitions=effects_map[effect]()) - elif effect == EFFECT_FAST_RANDOM_LOOP: - flow = Flow( - count=0, transitions=yee_transitions.randomloop(duration=250) - ) - elif effect == EFFECT_WHATSAPP: - flow = Flow(count=2, transitions=yee_transitions.pulse(37, 211, 102)) - elif effect == EFFECT_FACEBOOK: - flow = Flow(count=2, transitions=yee_transitions.pulse(59, 89, 152)) - elif effect == EFFECT_TWITTER: - flow = Flow(count=2, transitions=yee_transitions.pulse(0, 172, 237)) + if effect in self.custom_effects_names: + flow = Flow(**self.custom_effects[effect]) + elif effect in EFFECTS_MAP: + flow = Flow(count=0, transitions=EFFECTS_MAP[effect]()) + elif effect == EFFECT_FAST_RANDOM_LOOP: + flow = Flow(count=0, transitions=yee_transitions.randomloop(duration=250)) + elif effect == EFFECT_WHATSAPP: + flow = Flow(count=2, transitions=yee_transitions.pulse(37, 211, 102)) + elif effect == EFFECT_FACEBOOK: + flow = Flow(count=2, transitions=yee_transitions.pulse(59, 89, 152)) + elif effect == EFFECT_TWITTER: + flow = Flow(count=2, transitions=yee_transitions.pulse(0, 172, 237)) + else: + return - try: - self._bulb.start_flow(flow, light_type=self.light_type) - except BulbException as ex: - _LOGGER.error("Unable to set effect: %s", ex) + try: + self._bulb.start_flow(flow, light_type=self.light_type) + self._effect = effect + except BulbException as ex: + _LOGGER.error("Unable to set effect: %s", ex) def turn_on(self, **kwargs) -> None: """Turn the bulb on.""" @@ -721,7 +743,7 @@ class YeelightGenericLight(Light): self.set_music_mode(self.config[CONF_MODE_MUSIC]) except BulbException as ex: _LOGGER.error( - "Unable to turn on music mode," "consider disabling it: %s", ex + "Unable to turn on music mode, consider disabling it: %s", ex ) try: diff --git a/homeassistant/components/yeelightsunflower/light.py b/homeassistant/components/yeelightsunflower/light.py index 3424014e8f4..c49c874dc21 100644 --- a/homeassistant/components/yeelightsunflower/light.py +++ b/homeassistant/components/yeelightsunflower/light.py @@ -1,19 +1,19 @@ """Support for Yeelight Sunflower color bulbs (not Yeelight Blue or WiFi).""" import logging -import yeelightsunflower import voluptuous as vol +import yeelightsunflower -import homeassistant.helpers.config_validation as cv from homeassistant.components.light import ( - Light, - ATTR_HS_COLOR, - SUPPORT_COLOR, ATTR_BRIGHTNESS, - SUPPORT_BRIGHTNESS, + ATTR_HS_COLOR, PLATFORM_SCHEMA, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, + Light, ) from homeassistant.const import CONF_HOST +import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/yessssms/notify.py b/homeassistant/components/yessssms/notify.py index 1c1eed0e89d..70c85f7bdb3 100644 --- a/homeassistant/components/yessssms/notify.py +++ b/homeassistant/components/yessssms/notify.py @@ -1,16 +1,13 @@ """Support for the YesssSMS platform.""" import logging +from YesssSMS import YesssSMS import voluptuous as vol -from YesssSMS import YesssSMS - +from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService from homeassistant.const import CONF_PASSWORD, CONF_RECIPIENT, CONF_USERNAME import homeassistant.helpers.config_validation as cv -from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService - - from .const import CONF_PROVIDER _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/yi/camera.py b/homeassistant/components/yi/camera.py index fb1b46344ca..c8417748fd9 100644 --- a/homeassistant/components/yi/camera.py +++ b/homeassistant/components/yi/camera.py @@ -2,21 +2,24 @@ import asyncio import logging +from aioftp import Client, StatusCodeError +from haffmpeg.camera import CameraMjpeg +from haffmpeg.tools import IMAGE_JPEG, ImageFrame import voluptuous as vol -from homeassistant.components.camera import Camera, PLATFORM_SCHEMA +from homeassistant.components.camera import PLATFORM_SCHEMA, Camera from homeassistant.components.ffmpeg import DATA_FFMPEG from homeassistant.const import ( CONF_HOST, CONF_NAME, - CONF_PATH, CONF_PASSWORD, + CONF_PATH, CONF_PORT, CONF_USERNAME, ) +from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream -from homeassistant.exceptions import PlatformNotReady _LOGGER = logging.getLogger(__name__) @@ -82,8 +85,6 @@ class YiCamera(Camera): async def _get_latest_video_url(self): """Retrieve the latest video file from the customized Yi FTP server.""" - from aioftp import Client, StatusCodeError - ftp = Client() try: await ftp.connect(self.host) @@ -125,8 +126,6 @@ class YiCamera(Camera): async def async_camera_image(self): """Return a still image response from the camera.""" - from haffmpeg.tools import ImageFrame, IMAGE_JPEG - url = await self._get_latest_video_url() if url and url != self._last_url: ffmpeg = ImageFrame(self._manager.binary, loop=self.hass.loop) @@ -142,8 +141,6 @@ class YiCamera(Camera): async def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from the camera.""" - from haffmpeg.camera import CameraMjpeg - if not self._is_on: return diff --git a/homeassistant/components/yr/sensor.py b/homeassistant/components/yr/sensor.py index fc754c9a257..c9392561fc8 100644 --- a/homeassistant/components/yr/sensor.py +++ b/homeassistant/components/yr/sensor.py @@ -1,23 +1,21 @@ """Support for Yr.no weather service.""" import asyncio import logging - from random import randrange from xml.parsers.expat import ExpatError import aiohttp import async_timeout -import xmltodict import voluptuous as vol +import xmltodict -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( + ATTR_ATTRIBUTION, + CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, - CONF_ELEVATION, CONF_MONITORED_CONDITIONS, - ATTR_ATTRIBUTION, CONF_NAME, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_PRESSURE, @@ -26,8 +24,9 @@ from homeassistant.const import ( TEMP_CELSIUS, ) 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_utc_time_change, async_call_later +from homeassistant.helpers.event import async_call_later, async_track_utc_time_change from homeassistant.util import dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -161,7 +160,7 @@ class YrData: def __init__(self, hass, coordinates, forecast, devices): """Initialize the data object.""" self._url = ( - "https://aa015h6buqvih86i1.api.met.no/" "weatherapi/locationforecast/1.9/" + "https://aa015h6buqvih86i1.api.met.no/weatherapi/locationforecast/1.9/" ) self._urlparams = coordinates self._forecast = forecast diff --git a/homeassistant/components/zabbix/__init__.py b/homeassistant/components/zabbix/__init__.py index f1c5dbffead..0926f35af38 100644 --- a/homeassistant/components/zabbix/__init__.py +++ b/homeassistant/components/zabbix/__init__.py @@ -2,13 +2,14 @@ import logging from urllib.parse import urljoin +from pyzabbix import ZabbixAPI, ZabbixAPIException import voluptuous as vol from homeassistant.const import ( - CONF_PATH, CONF_HOST, - CONF_SSL, CONF_PASSWORD, + CONF_PATH, + CONF_SSL, CONF_USERNAME, ) import homeassistant.helpers.config_validation as cv @@ -37,7 +38,6 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): """Set up the Zabbix component.""" - from pyzabbix import ZabbixAPI, ZabbixAPIException conf = config[DOMAIN] if conf[CONF_SSL]: diff --git a/homeassistant/components/zabbix/sensor.py b/homeassistant/components/zabbix/sensor.py index 6b2c06eab2f..3fa29a07896 100644 --- a/homeassistant/components/zabbix/sensor.py +++ b/homeassistant/components/zabbix/sensor.py @@ -4,9 +4,9 @@ import logging import voluptuous as vol from homeassistant.components import zabbix -import homeassistant.helpers.config_validation as cv 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 _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zamg/sensor.py b/homeassistant/components/zamg/sensor.py index 9eea1f6612c..44c216eb1be 100644 --- a/homeassistant/components/zamg/sensor.py +++ b/homeassistant/components/zamg/sensor.py @@ -11,19 +11,12 @@ import pytz import requests import voluptuous as vol -from homeassistant.components.weather import ( - ATTR_WEATHER_HUMIDITY, - ATTR_WEATHER_PRESSURE, - ATTR_WEATHER_WIND_SPEED, - ATTR_WEATHER_ATTRIBUTION, - ATTR_WEATHER_TEMPERATURE, - ATTR_WEATHER_WIND_BEARING, -) from homeassistant.const import ( - CONF_NAME, + ATTR_ATTRIBUTION, CONF_LATITUDE, CONF_LONGITUDE, CONF_MONITORED_CONDITIONS, + CONF_NAME, __version__, ) import homeassistant.helpers.config_validation as cv @@ -43,15 +36,15 @@ DEFAULT_NAME = "zamg" MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10) SENSOR_TYPES = { - ATTR_WEATHER_PRESSURE: ("Pressure", "hPa", "LDstat hPa", float), + "pressure": ("Pressure", "hPa", "LDstat hPa", float), "pressure_sealevel": ("Pressure at Sea Level", "hPa", "LDred hPa", float), - ATTR_WEATHER_HUMIDITY: ("Humidity", "%", "RF %", int), - ATTR_WEATHER_WIND_SPEED: ("Wind Speed", "km/h", "WG km/h", float), - ATTR_WEATHER_WIND_BEARING: ("Wind Bearing", "°", "WR °", int), + "humidity": ("Humidity", "%", "RF %", int), + "wind_speed": ("Wind Speed", "km/h", "WG km/h", float), + "wind_bearing": ("Wind Bearing", "°", "WR °", int), "wind_max_speed": ("Top Wind Speed", "km/h", "WSG km/h", float), "wind_max_bearing": ("Top Wind Bearing", "°", "WSR °", int), "sun_last_hour": ("Sun Last Hour", "%", "SO %", int), - ATTR_WEATHER_TEMPERATURE: ("Temperature", "°C", "T °C", float), + "temperature": ("Temperature", "°C", "T °C", float), "precipitation": ("Precipitation", "l/m²", "N l/m²", float), "dewpoint": ("Dew Point", "°C", "TP °C", float), # The following probably not useful for general consumption, @@ -140,7 +133,7 @@ class ZamgSensor(Entity): def device_state_attributes(self): """Return the state attributes.""" return { - ATTR_WEATHER_ATTRIBUTION: ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_STATION: self.probe.get_data("station_name"), ATTR_UPDATED: self.probe.last_update.isoformat(), } diff --git a/homeassistant/components/zengge/light.py b/homeassistant/components/zengge/light.py index d890b193d72..42746c6bad3 100644 --- a/homeassistant/components/zengge/light.py +++ b/homeassistant/components/zengge/light.py @@ -1,20 +1,20 @@ """Support for Zengge lights.""" import logging -from zengge import zengge import voluptuous as vol +from zengge import zengge -from homeassistant.const import CONF_DEVICES, CONF_NAME from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_WHITE_VALUE, + PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_WHITE_VALUE, Light, - PLATFORM_SCHEMA, ) +from homeassistant.const import CONF_DEVICES, CONF_NAME import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 2f9fb7b4580..d6be4cdf6a0 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -1,22 +1,25 @@ """Support for exposing Home Assistant via Zeroconf.""" -# PyLint bug confuses absolute/relative imports -# https://github.com/PyCQA/pylint/issues/1931 -# pylint: disable=no-name-in-module +import ipaddress import logging import socket -import ipaddress import voluptuous as vol - -from zeroconf import ServiceBrowser, ServiceInfo, ServiceStateChange, Zeroconf +from zeroconf import ( + NonUniqueNameException, + ServiceBrowser, + ServiceInfo, + ServiceStateChange, + Zeroconf, +) from homeassistant import util from homeassistant.const import ( - EVENT_HOMEASSISTANT_STOP, + ATTR_NAME, EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP, __version__, ) -from homeassistant.generated.zeroconf import ZEROCONF, HOMEKIT +from homeassistant.generated.zeroconf import HOMEKIT, ZEROCONF _LOGGER = logging.getLogger(__name__) @@ -26,7 +29,6 @@ ATTR_HOST = "host" ATTR_PORT = "port" ATTR_HOSTNAME = "hostname" ATTR_TYPE = "type" -ATTR_NAME = "name" ATTR_PROPERTIES = "properties" ZEROCONF_TYPE = "_home-assistant._tcp.local." @@ -43,7 +45,7 @@ def setup(hass, config): params = { "version": __version__, "base_url": hass.config.api.base_url, - # always needs authentication + # Always needs authentication "requires_api_password": True, } @@ -69,7 +71,12 @@ def setup(hass, config): Wait till started or otherwise HTTP is not up and running. """ _LOGGER.info("Starting Zeroconf broadcast") - zeroconf.register_service(info) + try: + zeroconf.register_service(info) + except NonUniqueNameException: + _LOGGER.error( + "Home Assistant instance with identical name present in the local network" + ) hass.bus.listen_once(EVENT_HOMEASSISTANT_START, zeroconf_hass_start) diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 39f016e9d0e..ec4db5931dc 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -3,7 +3,7 @@ "name": "Zeroconf", "documentation": "https://www.home-assistant.io/integrations/zeroconf", "requirements": [ - "zeroconf==0.23.0" + "zeroconf==0.24.4" ], "dependencies": [ "api" diff --git a/homeassistant/components/zestimate/sensor.py b/homeassistant/components/zestimate/sensor.py index 4b8bdf5fa2e..cdf7e6304ad 100644 --- a/homeassistant/components/zestimate/sensor.py +++ b/homeassistant/components/zestimate/sensor.py @@ -3,11 +3,11 @@ from datetime import timedelta import logging import requests -import xmltodict import voluptuous as vol +import xmltodict from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_API_KEY, CONF_NAME, ATTR_ATTRIBUTION +from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_NAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity diff --git a/homeassistant/components/zha/.translations/da.json b/homeassistant/components/zha/.translations/da.json index 39f254ac9af..908d8113b2e 100644 --- a/homeassistant/components/zha/.translations/da.json +++ b/homeassistant/components/zha/.translations/da.json @@ -9,8 +9,8 @@ "step": { "user": { "data": { - "radio_type": "Radio type", - "usb_path": "Sti til USB enhed" + "radio_type": "Radio-type", + "usb_path": "Sti til USB-enhed" }, "title": "ZHA" } @@ -33,12 +33,35 @@ "close": "Luk", "dim_down": "D\u00e6mp ned", "dim_up": "D\u00e6mp op", + "face_1": "med ansigt 1 aktiveret", + "face_2": "med ansigt 2 aktiveret", + "face_3": "med ansigt 3 aktiveret", + "face_4": "med ansigt 4 aktiveret", + "face_5": "med ansigt 5 aktiveret", + "face_6": "med ansigt 6 aktiveret", + "face_any": "Med ethvert/specificeret ansigt(er) aktiveret", "left": "Venstre", "open": "\u00c5ben", - "right": "H\u00f8jre" + "right": "H\u00f8jre", + "turn_off": "Sluk", + "turn_on": "T\u00e6nd" }, "trigger_type": { - "device_shaken": "Enhed rystet" + "device_dropped": "Enhed faldt", + "device_flipped": "Enheden blev vendt \"{subtype}\"", + "device_knocked": "Enhed banket med \"{subtype}\"", + "device_rotated": "Enhed roteret \"{subtype}\"", + "device_shaken": "Enhed rystet", + "device_slid": "Enheden gled \"{subtype}\"", + "device_tilted": "Enheden vippes", + "remote_button_double_press": "\"{subtype}\"-knappen er dobbeltklikket", + "remote_button_long_press": "\"{subtype}\"-knappen trykket p\u00e5 konstant", + "remote_button_long_release": "\"{subtype}\"-knappen frigivet efter langt tryk", + "remote_button_quadruple_press": "\"{subtype}\"-knappen firedobbelt-klikket", + "remote_button_quintuple_press": "\"{subtype}\"-knappen femdobbelt-klikket", + "remote_button_short_press": "\"{subtype}\"-knappen trykket p\u00e5", + "remote_button_short_release": "\"{subtype}\"-knappen frigivet", + "remote_button_triple_press": "\"{subtype}\"-knappen tredobbeltklikkes" } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/es.json b/homeassistant/components/zha/.translations/es.json index b8529ce9047..fb2271b260a 100644 --- a/homeassistant/components/zha/.translations/es.json +++ b/homeassistant/components/zha/.translations/es.json @@ -54,14 +54,14 @@ "device_shaken": "Dispositivo agitado", "device_slid": "Dispositivo deslizado \" {subtype} \"", "device_tilted": "Dispositivo inclinado", - "remote_button_double_press": "\"{subtipo}\" bot\u00f3n de doble clic", - "remote_button_long_press": "Bot\u00f3n \"{subtipo}\" pulsado continuamente", - "remote_button_long_release": "Bot\u00f3n \"{subtipo}\" liberado despu\u00e9s de una pulsaci\u00f3n prolongada", - "remote_button_quadruple_press": "\"{subtipo}\" bot\u00f3n cu\u00e1druple pulsado", - "remote_button_quintuple_press": "\"{subtipo}\" bot\u00f3n qu\u00edntuple pulsado", - "remote_button_short_press": "Bot\u00f3n \"{subtipo}\" pulsado", - "remote_button_short_release": "Bot\u00f3n \"{subtipo}\" liberado", - "remote_button_triple_press": "\"{subtipo}\" bot\u00f3n de triple clic" + "remote_button_double_press": "\"{subtype}\" bot\u00f3n de doble clic", + "remote_button_long_press": "Bot\u00f3n \"{subtype}\" pulsado continuamente", + "remote_button_long_release": "Bot\u00f3n \"{subtype}\" liberado despu\u00e9s de una pulsaci\u00f3n prolongada", + "remote_button_quadruple_press": "\"{subtype}\" bot\u00f3n cu\u00e1druple pulsado", + "remote_button_quintuple_press": "\"{subtype}\" bot\u00f3n qu\u00edntuple pulsado", + "remote_button_short_press": "Bot\u00f3n \"{subtype}\" pulsado", + "remote_button_short_release": "Bot\u00f3n \"{subtype}\" liberado", + "remote_button_triple_press": "\"{subtype}\" bot\u00f3n de triple clic" } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/fr.json b/homeassistant/components/zha/.translations/fr.json index 9b1ba025d7c..5d8bdfa82eb 100644 --- a/homeassistant/components/zha/.translations/fr.json +++ b/homeassistant/components/zha/.translations/fr.json @@ -59,7 +59,7 @@ "remote_button_long_release": "Bouton \" {subtype} \" rel\u00e2ch\u00e9 apr\u00e8s un appui long", "remote_button_quadruple_press": "bouton \" {subtype} \" quadruple clics", "remote_button_quintuple_press": "bouton \" {subtype} \" quintuple clics", - "remote_button_short_press": "bouton \" {subtype} \" enfonc\u00e9", + "remote_button_short_press": "bouton \"{subtype}\" est press\u00e9", "remote_button_short_release": "Bouton \" {subtype} \" est rel\u00e2ch\u00e9", "remote_button_triple_press": "Bouton \"{subtype}\" \u00e0 trois clics" } diff --git a/homeassistant/components/zha/.translations/ko.json b/homeassistant/components/zha/.translations/ko.json index 3a62f5d7ebe..69b8f9ad9a4 100644 --- a/homeassistant/components/zha/.translations/ko.json +++ b/homeassistant/components/zha/.translations/ko.json @@ -47,21 +47,21 @@ "turn_on": "\ucf1c\uae30" }, "trigger_type": { - "device_dropped": "\uae30\uae30\ub97c \ub5a8\uad7c", - "device_flipped": "\"{subtype}\" \uae30\uae30\ub97c \ub4a4\uc9d1\uc74c", - "device_knocked": "\"{subtype}\" \uae30\uae30\ub97c \ub450\ub4dc\ub9bc", - "device_rotated": "\"{subtype}\" \uae30\uae30\ub97c \ud68c\uc804", - "device_shaken": "\uae30\uae30\ub97c \ud754\ub4e6", - "device_slid": "\"{subtype}\" \uae30\uae30\ub97c \uc2ac\ub77c\uc774\ub4dc", - "device_tilted": "\uae30\uae30\ub97c \uae30\uc6b8\uc784", - "remote_button_double_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub450 \ubc88 \ub204\ub984", - "remote_button_long_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \uacc4\uc18d \ub204\ub984", - "remote_button_long_release": "\"{subtype}\" \ubc84\ud2bc\uc744 \uae38\uac8c \ub20c\ub800\ub2e4\uac00 \ub5cc", - "remote_button_quadruple_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub124 \ubc88 \ub204\ub984", - "remote_button_quintuple_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub2e4\uc12f \ubc88 \ub204\ub984", - "remote_button_short_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub204\ub984", - "remote_button_short_release": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub5cc", - "remote_button_triple_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \uc138 \ubc88 \ub204\ub984" + "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_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" } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/nn.json b/homeassistant/components/zha/.translations/nn.json index ad2c240baf1..392018bb1f1 100644 --- a/homeassistant/components/zha/.translations/nn.json +++ b/homeassistant/components/zha/.translations/nn.json @@ -6,5 +6,10 @@ } }, "title": "ZHA" + }, + "device_automation": { + "action_type": { + "squawk": "Squawk" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/ru.json b/homeassistant/components/zha/.translations/ru.json index c0bc7c176a2..27682ebfd82 100644 --- a/homeassistant/components/zha/.translations/ru.json +++ b/homeassistant/components/zha/.translations/ru.json @@ -19,7 +19,7 @@ }, "device_automation": { "action_type": { - "squawk": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0441\u0438\u0440\u0435\u043d\u0443", + "squawk": "\u0422\u0440\u0430\u043d\u0441\u043f\u043e\u043d\u0434\u0435\u0440", "warn": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043e\u043f\u043e\u0432\u0435\u0449\u0435\u043d\u0438\u0435" }, "trigger_subtype": { diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index be84795d962..7303367d485 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -1,4 +1,5 @@ """Support for Zigbee Home Automation devices.""" + import logging import voluptuous as vol @@ -7,8 +8,6 @@ from homeassistant import config_entries, const as ha_const import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE -# Loading the config flow file will register the flow -from . import config_flow # noqa: F401 pylint: disable=unused-import from . import api from .core import ZHAGateway from .core.const import ( @@ -100,8 +99,7 @@ async def async_setup_entry(hass, config_entry): if config.get(CONF_ENABLE_QUIRKS, True): # needs to be done here so that the ZHA module is finished loading # before zhaquirks is imported - # pylint: disable=W0611, W0612 - import zhaquirks # noqa: F401 + import zhaquirks # noqa: F401 pylint: disable=unused-import, import-outside-toplevel, import-error zha_gateway = ZHAGateway(hass, config, config_entry) await zha_gateway.async_initialize() @@ -145,5 +143,4 @@ async def async_unload_entry(hass, config_entry): for component in COMPONENTS: await hass.config_entries.async_forward_entry_unload(config_entry, component) - del hass.data[DATA_ZHA] return True diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index 6f24db442dd..1294fcaedbd 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -23,6 +23,7 @@ from .core.const import ( ATTR_ENDPOINT_ID, ATTR_LEVEL, ATTR_MANUFACTURER, + ATTR_MEMBERS, ATTR_NAME, ATTR_VALUE, ATTR_WARNING_DEVICE_DURATION, @@ -39,6 +40,9 @@ from .core.const import ( DATA_ZHA, DATA_ZHA_GATEWAY, DOMAIN, + GROUP_ID, + GROUP_IDS, + GROUP_NAME, MFG_CLUSTER_ID_START, WARNING_DEVICE_MODE_EMERGENCY, WARNING_DEVICE_SOUND_HIGH, @@ -46,7 +50,11 @@ from .core.const import ( WARNING_DEVICE_STROBE_HIGH, WARNING_DEVICE_STROBE_YES, ) -from .core.helpers import async_is_bindable_target, get_matched_clusters +from .core.helpers import ( + async_get_device_info, + async_is_bindable_target, + get_matched_clusters, +) _LOGGER = logging.getLogger(__name__) @@ -57,17 +65,17 @@ RESPONSE = "response" DEVICE_INFO = "device_info" ATTR_DURATION = "duration" +ATTR_GROUP = "group" ATTR_IEEE_ADDRESS = "ieee_address" ATTR_IEEE = "ieee" ATTR_SOURCE_IEEE = "source_ieee" ATTR_TARGET_IEEE = "target_ieee" -BIND_REQUEST = 0x0021 -UNBIND_REQUEST = 0x0022 SERVICE_PERMIT = "permit" SERVICE_REMOVE = "remove" SERVICE_SET_ZIGBEE_CLUSTER_ATTRIBUTE = "set_zigbee_cluster_attribute" SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND = "issue_zigbee_cluster_command" +SERVICE_ISSUE_ZIGBEE_GROUP_COMMAND = "issue_zigbee_group_command" SERVICE_DIRECT_ZIGBEE_BIND = "issue_direct_zigbee_bind" SERVICE_DIRECT_ZIGBEE_UNBIND = "issue_direct_zigbee_unbind" SERVICE_WARNING_DEVICE_SQUAWK = "warning_device_squawk" @@ -139,7 +147,17 @@ SERVICE_SCHEMAS = { vol.Optional(ATTR_CLUSTER_TYPE, default=CLUSTER_TYPE_IN): cv.string, vol.Required(ATTR_COMMAND): cv.positive_int, vol.Required(ATTR_COMMAND_TYPE): cv.string, - vol.Optional(ATTR_ARGS, default=""): cv.string, + vol.Optional(ATTR_ARGS, default=[]): cv.ensure_list, + vol.Optional(ATTR_MANUFACTURER): cv.positive_int, + } + ), + SERVICE_ISSUE_ZIGBEE_GROUP_COMMAND: vol.Schema( + { + vol.Required(ATTR_GROUP): cv.positive_int, + vol.Required(ATTR_CLUSTER_ID): cv.positive_int, + vol.Optional(ATTR_CLUSTER_TYPE, default=CLUSTER_TYPE_IN): cv.string, + vol.Required(ATTR_COMMAND): cv.positive_int, + vol.Optional(ATTR_ARGS, default=[]): cv.ensure_list, vol.Optional(ATTR_MANUFACTURER): cv.positive_int, } ), @@ -199,6 +217,34 @@ async def websocket_get_devices(hass, connection, msg): connection.send_result(msg[ID], devices) +@websocket_api.require_admin +@websocket_api.async_response +@websocket_api.websocket_command({vol.Required(TYPE): "zha/devices/groupable"}) +async def websocket_get_groupable_devices(hass, connection, msg): + """Get ZHA devices that can be grouped.""" + zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + ha_device_registry = await async_get_registry(hass) + + devices = [] + for device in zha_gateway.devices.values(): + if device.is_groupable: + devices.append( + async_get_device_info( + hass, device, ha_device_registry=ha_device_registry + ) + ) + connection.send_result(msg[ID], devices) + + +@websocket_api.require_admin +@websocket_api.async_response +@websocket_api.websocket_command({vol.Required(TYPE): "zha/groups"}) +async def websocket_get_groups(hass, connection, msg): + """Get ZHA groups.""" + groups = await get_groups(hass) + connection.send_result(msg[ID], groups) + + @websocket_api.require_admin @websocket_api.async_response @websocket_api.websocket_command( @@ -224,29 +270,219 @@ async def websocket_get_device(hass, connection, msg): connection.send_result(msg[ID], device) -@callback -def async_get_device_info(hass, device, ha_device_registry=None): - """Get ZHA device.""" +@websocket_api.require_admin +@websocket_api.async_response +@websocket_api.websocket_command( + {vol.Required(TYPE): "zha/group", vol.Required(GROUP_ID): cv.positive_int} +) +async def websocket_get_group(hass, connection, msg): + """Get ZHA group.""" zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - ret_device = {} - ret_device.update(device.device_info) - ret_device["entities"] = [ - { - "entity_id": entity_ref.reference_id, - ATTR_NAME: entity_ref.device_info[ATTR_NAME], - } - for entity_ref in zha_gateway.device_registry[device.ieee] - ] + ha_device_registry = await async_get_registry(hass) + group_id = msg[GROUP_ID] + group = None - if ha_device_registry is not None: - reg_device = ha_device_registry.async_get_device( - {(DOMAIN, str(device.ieee))}, set() + if group_id in zha_gateway.application_controller.groups: + group = async_get_group_info( + hass, + zha_gateway, + zha_gateway.application_controller.groups[group_id], + ha_device_registry, ) - if reg_device is not None: - ret_device["user_given_name"] = reg_device.name_by_user - ret_device["device_reg_id"] = reg_device.id - ret_device["area_id"] = reg_device.area_id - return ret_device + if not group: + connection.send_message( + websocket_api.error_message( + msg[ID], websocket_api.const.ERR_NOT_FOUND, "ZHA Group not found" + ) + ) + return + connection.send_result(msg[ID], group) + + +@websocket_api.require_admin +@websocket_api.async_response +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zha/group/add", + vol.Required(GROUP_NAME): cv.string, + vol.Optional(ATTR_MEMBERS): vol.All(cv.ensure_list, [EUI64.convert]), + } +) +async def websocket_add_group(hass, connection, msg): + """Add a new ZHA group.""" + zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + ha_device_registry = await async_get_registry(hass) + group_id = len(zha_gateway.application_controller.groups) + 1 + group_name = msg[GROUP_NAME] + zigpy_group = async_get_group_by_name(zha_gateway, group_name) + ret_group = None + members = msg.get(ATTR_MEMBERS) + + # guard against group already existing + if zigpy_group is None: + zigpy_group = zha_gateway.application_controller.groups.add_group( + group_id, group_name + ) + if members is not None: + tasks = [] + for ieee in members: + tasks.append(zha_gateway.devices[ieee].async_add_to_group(group_id)) + await asyncio.gather(*tasks) + ret_group = async_get_group_info(hass, zha_gateway, zigpy_group, ha_device_registry) + connection.send_result(msg[ID], ret_group) + + +@websocket_api.require_admin +@websocket_api.async_response +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zha/group/remove", + vol.Required(GROUP_IDS): vol.All(cv.ensure_list, [cv.positive_int]), + } +) +async def websocket_remove_groups(hass, connection, msg): + """Remove the specified ZHA groups.""" + zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + groups = zha_gateway.application_controller.groups + group_ids = msg[GROUP_IDS] + + if len(group_ids) > 1: + tasks = [] + for group_id in group_ids: + tasks.append(remove_group(groups[group_id], zha_gateway)) + await asyncio.gather(*tasks) + else: + await remove_group(groups[group_ids[0]], zha_gateway) + ret_groups = await get_groups(hass) + connection.send_result(msg[ID], ret_groups) + + +@websocket_api.require_admin +@websocket_api.async_response +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zha/group/members/add", + vol.Required(GROUP_ID): cv.positive_int, + vol.Required(ATTR_MEMBERS): vol.All(cv.ensure_list, [EUI64.convert]), + } +) +async def websocket_add_group_members(hass, connection, msg): + """Add members to a ZHA group.""" + zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + ha_device_registry = await async_get_registry(hass) + group_id = msg[GROUP_ID] + members = msg[ATTR_MEMBERS] + zigpy_group = None + + if group_id in zha_gateway.application_controller.groups: + zigpy_group = zha_gateway.application_controller.groups[group_id] + tasks = [] + for ieee in members: + tasks.append(zha_gateway.devices[ieee].async_add_to_group(group_id)) + await asyncio.gather(*tasks) + if not zigpy_group: + connection.send_message( + websocket_api.error_message( + msg[ID], websocket_api.const.ERR_NOT_FOUND, "ZHA Group not found" + ) + ) + return + ret_group = async_get_group_info(hass, zha_gateway, zigpy_group, ha_device_registry) + connection.send_result(msg[ID], ret_group) + + +@websocket_api.require_admin +@websocket_api.async_response +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zha/group/members/remove", + vol.Required(GROUP_ID): cv.positive_int, + vol.Required(ATTR_MEMBERS): vol.All(cv.ensure_list, [EUI64.convert]), + } +) +async def websocket_remove_group_members(hass, connection, msg): + """Remove members from a ZHA group.""" + zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + ha_device_registry = await async_get_registry(hass) + group_id = msg[GROUP_ID] + members = msg[ATTR_MEMBERS] + zigpy_group = None + + if group_id in zha_gateway.application_controller.groups: + zigpy_group = zha_gateway.application_controller.groups[group_id] + tasks = [] + for ieee in members: + tasks.append(zha_gateway.devices[ieee].async_remove_from_group(group_id)) + await asyncio.gather(*tasks) + if not zigpy_group: + connection.send_message( + websocket_api.error_message( + msg[ID], websocket_api.const.ERR_NOT_FOUND, "ZHA Group not found" + ) + ) + return + ret_group = async_get_group_info(hass, zha_gateway, zigpy_group, ha_device_registry) + connection.send_result(msg[ID], ret_group) + + +async def get_groups(hass,): + """Get ZHA Groups.""" + zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + ha_device_registry = await async_get_registry(hass) + + groups = [] + for group in zha_gateway.application_controller.groups.values(): + groups.append( + async_get_group_info(hass, zha_gateway, group, ha_device_registry) + ) + return groups + + +async def remove_group(group, zha_gateway): + """Remove ZHA Group.""" + if group.members: + tasks = [] + for member_ieee in group.members.keys(): + if member_ieee[0] in zha_gateway.devices: + tasks.append( + zha_gateway.devices[member_ieee[0]].async_remove_from_group( + group.group_id + ) + ) + if tasks: + await asyncio.gather(*tasks) + else: + # we have members but none are tracked by ZHA for whatever reason + zha_gateway.application_controller.groups.pop(group.group_id) + else: + zha_gateway.application_controller.groups.pop(group.group_id) + + +@callback +def async_get_group_info(hass, zha_gateway, group, ha_device_registry): + """Get ZHA group.""" + ret_group = {} + ret_group["group_id"] = group.group_id + ret_group["name"] = group.name + ret_group["members"] = [ + async_get_device_info( + hass, + zha_gateway.get_device(member_ieee[0]), + ha_device_registry=ha_device_registry, + ) + for member_ieee in group.members.keys() + if member_ieee[0] in zha_gateway.devices + ] + return ret_group + + +@callback +def async_get_group_by_name(zha_gateway, group_name): + """Get ZHA group by name.""" + for group in zha_gateway.application_controller.groups.values(): + if group.name == group_name: + return group + return None @websocket_api.require_admin @@ -333,11 +569,15 @@ async def websocket_device_cluster_attributes(hass, connection, msg): {ID: attr_id, ATTR_NAME: attributes[attr_id][0]} ) _LOGGER.debug( - "Requested attributes for: %s %s %s %s", - f"{ATTR_CLUSTER_ID}: [{cluster_id}]", - f"{ATTR_CLUSTER_TYPE}: [{cluster_type}]", - f"{ATTR_ENDPOINT_ID}: [{endpoint_id}]", - f"{RESPONSE}: [{cluster_attributes}]", + "Requested attributes for: %s: %s, %s: '%s', %s: %s, %s: %s", + ATTR_CLUSTER_ID, + cluster_id, + ATTR_CLUSTER_TYPE, + cluster_type, + ATTR_ENDPOINT_ID, + endpoint_id, + RESPONSE, + cluster_attributes, ) connection.send_result(msg[ID], cluster_attributes) @@ -387,11 +627,15 @@ async def websocket_device_cluster_commands(hass, connection, msg): } ) _LOGGER.debug( - "Requested commands for: %s %s %s %s", - f"{ATTR_CLUSTER_ID}: [{cluster_id}]", - f"{ATTR_CLUSTER_TYPE}: [{cluster_type}]", - f"{ATTR_ENDPOINT_ID}: [{endpoint_id}]", - f"{RESPONSE}: [{cluster_commands}]", + "Requested commands for: %s: %s, %s: '%s', %s: %s, %s: %s", + ATTR_CLUSTER_ID, + cluster_id, + ATTR_CLUSTER_TYPE, + cluster_type, + ATTR_ENDPOINT_ID, + endpoint_id, + RESPONSE, + cluster_commands, ) connection.send_result(msg[ID], cluster_commands) @@ -431,14 +675,21 @@ async def websocket_read_zigbee_cluster_attributes(hass, connection, msg): [attribute], allow_cache=False, only_cache=False, manufacturer=manufacturer ) _LOGGER.debug( - "Read attribute for: %s %s %s %s %s %s %s", - f"{ATTR_CLUSTER_ID}: [{cluster_id}]", - f"{ATTR_CLUSTER_TYPE}: [{cluster_type}]", - f"{ATTR_ENDPOINT_ID}: [{endpoint_id}]", - f"{ATTR_ATTRIBUTE}: [{attribute}]", - f"{ATTR_MANUFACTURER}: [{manufacturer}]", - "{}: [{}]".format(RESPONSE, str(success.get(attribute))), - "{}: [{}]".format("failure", failure), + "Read attribute for: %s: [%s] %s: [%s] %s: [%s] %s: [%s] %s: [%s] %s: [%s] %s: [%s],", + ATTR_CLUSTER_ID, + cluster_id, + ATTR_CLUSTER_TYPE, + cluster_type, + ATTR_ENDPOINT_ID, + endpoint_id, + ATTR_ATTRIBUTE, + attribute, + ATTR_MANUFACTURER, + manufacturer, + RESPONSE, + str(success.get(attribute)), + "failure", + failure, ) connection.send_result(msg[ID], str(success.get(attribute))) @@ -461,9 +712,11 @@ async def websocket_get_bindable_devices(hass, connection, msg): ] _LOGGER.debug( - "Get bindable devices: %s %s", - f"{ATTR_SOURCE_IEEE}: [{source_ieee}]", - "{}: [{}]".format("bindable devices:", devices), + "Get bindable devices: %s: [%s], %s: [%s]", + ATTR_SOURCE_IEEE, + source_ieee, + "bindable devices", + devices, ) connection.send_message(websocket_api.result_message(msg[ID], devices)) @@ -483,11 +736,15 @@ async def websocket_bind_devices(hass, connection, msg): zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] source_ieee = msg[ATTR_SOURCE_IEEE] target_ieee = msg[ATTR_TARGET_IEEE] - await async_binding_operation(zha_gateway, source_ieee, target_ieee, BIND_REQUEST) + await async_binding_operation( + zha_gateway, source_ieee, target_ieee, zdo_types.ZDOCmd.Bind_req + ) _LOGGER.info( - "Issue bind devices: %s %s", - f"{ATTR_SOURCE_IEEE}: [{source_ieee}]", - f"{ATTR_TARGET_IEEE}: [{target_ieee}]", + "Devices bound: %s: [%s] %s: [%s]", + ATTR_SOURCE_IEEE, + source_ieee, + ATTR_TARGET_IEEE, + target_ieee, ) @@ -505,11 +762,15 @@ async def websocket_unbind_devices(hass, connection, msg): zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] source_ieee = msg[ATTR_SOURCE_IEEE] target_ieee = msg[ATTR_TARGET_IEEE] - await async_binding_operation(zha_gateway, source_ieee, target_ieee, UNBIND_REQUEST) + await async_binding_operation( + zha_gateway, source_ieee, target_ieee, zdo_types.ZDOCmd.Unbind_req + ) _LOGGER.info( - "Issue unbind devices: %s %s", - f"{ATTR_SOURCE_IEEE}: [{source_ieee}]", - f"{ATTR_TARGET_IEEE}: [{target_ieee}]", + "Devices un-bound: %s: [%s] %s: [%s]", + ATTR_SOURCE_IEEE, + source_ieee, + ATTR_TARGET_IEEE, + target_ieee, ) @@ -530,22 +791,34 @@ async def async_binding_operation(zha_gateway, source_ieee, target_ieee, operati zdo = cluster_pair.source_cluster.endpoint.device.zdo - _LOGGER.debug( - "processing binding operation for: %s %s %s", - f"{ATTR_SOURCE_IEEE}: [{source_ieee}]", - f"{ATTR_TARGET_IEEE}: [{target_ieee}]", - "{}: {}".format("cluster", cluster_pair.source_cluster.cluster_id), + op_msg = "cluster: %s %s --> [%s]" + op_params = ( + cluster_pair.source_cluster.cluster_id, + operation.name, + target_ieee, ) + zdo.debug("processing " + op_msg, *op_params) + bind_tasks.append( - zdo.request( - operation, - source_device.ieee, - cluster_pair.source_cluster.endpoint.endpoint_id, - cluster_pair.source_cluster.cluster_id, - destination_address, + ( + zdo.request( + operation, + source_device.ieee, + cluster_pair.source_cluster.endpoint.endpoint_id, + cluster_pair.source_cluster.cluster_id, + destination_address, + ), + op_msg, + op_params, ) ) - await asyncio.gather(*bind_tasks) + res = await asyncio.gather(*(t[0] for t in bind_tasks), return_exceptions=True) + for outcome, log_msg in zip(res, bind_tasks): + if isinstance(outcome, Exception): + fmt = log_msg[1] + " failed: %s" + else: + fmt = log_msg[1] + " completed: %s" + zdo.debug(fmt, *(log_msg[2] + (outcome,))) def async_load_api(hass): @@ -600,14 +873,21 @@ def async_load_api(hass): manufacturer=manufacturer, ) _LOGGER.debug( - "Set attribute for: %s %s %s %s %s %s %s", - f"{ATTR_CLUSTER_ID}: [{cluster_id}]", - f"{ATTR_CLUSTER_TYPE}: [{cluster_type}]", - f"{ATTR_ENDPOINT_ID}: [{endpoint_id}]", - f"{ATTR_ATTRIBUTE}: [{attribute}]", - f"{ATTR_VALUE}: [{value}]", - f"{ATTR_MANUFACTURER}: [{manufacturer}]", - f"{RESPONSE}: [{response}]", + "Set attribute for: %s: [%s] %s: [%s] %s: [%s] %s: [%s] %s: [%s] %s: [%s] %s: [%s]", + ATTR_CLUSTER_ID, + cluster_id, + ATTR_CLUSTER_TYPE, + cluster_type, + ATTR_ENDPOINT_ID, + endpoint_id, + ATTR_ATTRIBUTE, + attribute, + ATTR_VALUE, + value, + ATTR_MANUFACTURER, + manufacturer, + RESPONSE, + response, ) hass.helpers.service.async_register_admin_service( @@ -637,20 +917,28 @@ def async_load_api(hass): cluster_id, command, command_type, - args, + *args, cluster_type=cluster_type, manufacturer=manufacturer, ) _LOGGER.debug( - "Issue command for: %s %s %s %s %s %s %s %s", - f"{ATTR_CLUSTER_ID}: [{cluster_id}]", - f"{ATTR_CLUSTER_TYPE}: [{cluster_type}]", - f"{ATTR_ENDPOINT_ID}: [{endpoint_id}]", - f"{ATTR_COMMAND}: [{command}]", - f"{ATTR_COMMAND_TYPE}: [{command_type}]", - f"{ATTR_ARGS}: [{args}]", - f"{ATTR_MANUFACTURER}: [{manufacturer}]", - f"{RESPONSE}: [{response}]", + "Issued command for: %s: [%s] %s: [%s] %s: [%s] %s: [%s] %s: [%s] %s: %s %s: [%s] %s: %s", + ATTR_CLUSTER_ID, + cluster_id, + ATTR_CLUSTER_TYPE, + cluster_type, + ATTR_ENDPOINT_ID, + endpoint_id, + ATTR_COMMAND, + command, + ATTR_COMMAND_TYPE, + command_type, + ATTR_ARGS, + args, + ATTR_MANUFACTURER, + manufacturer, + RESPONSE, + response, ) hass.helpers.service.async_register_admin_service( @@ -660,6 +948,43 @@ def async_load_api(hass): schema=SERVICE_SCHEMAS[SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND], ) + async def issue_zigbee_group_command(service): + """Issue command on zigbee cluster on a zigbee group.""" + group_id = service.data.get(ATTR_GROUP) + cluster_id = service.data.get(ATTR_CLUSTER_ID) + command = service.data.get(ATTR_COMMAND) + args = service.data.get(ATTR_ARGS) + manufacturer = service.data.get(ATTR_MANUFACTURER) or None + group = zha_gateway.get_group(group_id) + if cluster_id >= MFG_CLUSTER_ID_START and manufacturer is None: + _LOGGER.error("Missing manufacturer attribute for cluster: %d", cluster_id) + response = None + if group is not None: + cluster = group.endpoint[cluster_id] + response = await cluster.command( + command, *args, manufacturer=manufacturer, expect_reply=True + ) + _LOGGER.debug( + "Issued group command for: %s: [%s] %s: [%s] %s: %s %s: [%s] %s: %s", + ATTR_CLUSTER_ID, + cluster_id, + ATTR_COMMAND, + command, + ATTR_ARGS, + args, + ATTR_MANUFACTURER, + manufacturer, + RESPONSE, + response, + ) + + hass.helpers.service.async_register_admin_service( + DOMAIN, + SERVICE_ISSUE_ZIGBEE_GROUP_COMMAND, + issue_zigbee_group_command, + schema=SERVICE_SCHEMAS[SERVICE_ISSUE_ZIGBEE_GROUP_COMMAND], + ) + async def warning_device_squawk(service): """Issue the squawk command for an IAS warning device.""" ieee = service.data[ATTR_IEEE] @@ -674,20 +999,24 @@ def async_load_api(hass): await channel.squawk(mode, strobe, level) else: _LOGGER.error( - "Squawking IASWD: %s is missing the required IASWD channel!", - "{}: [{}]".format(ATTR_IEEE, str(ieee)), + "Squawking IASWD: %s: [%s] is missing the required IASWD channel!", + ATTR_IEEE, + str(ieee), ) else: _LOGGER.error( - "Squawking IASWD: %s could not be found!", - "{}: [{}]".format(ATTR_IEEE, str(ieee)), + "Squawking IASWD: %s: [%s] could not be found!", ATTR_IEEE, str(ieee) ) _LOGGER.debug( - "Squawking IASWD: %s %s %s %s", - "{}: [{}]".format(ATTR_IEEE, str(ieee)), - "{}: [{}]".format(ATTR_WARNING_DEVICE_MODE, mode), - "{}: [{}]".format(ATTR_WARNING_DEVICE_STROBE, strobe), - "{}: [{}]".format(ATTR_LEVEL, level), + "Squawking IASWD: %s: [%s] %s: [%s] %s: [%s] %s: [%s]", + ATTR_IEEE, + str(ieee), + ATTR_WARNING_DEVICE_MODE, + mode, + ATTR_WARNING_DEVICE_STROBE, + strobe, + ATTR_LEVEL, + level, ) hass.helpers.service.async_register_admin_service( @@ -716,20 +1045,24 @@ def async_load_api(hass): ) else: _LOGGER.error( - "Warning IASWD: %s is missing the required IASWD channel!", - "{}: [{}]".format(ATTR_IEEE, str(ieee)), + "Warning IASWD: %s: [%s] is missing the required IASWD channel!", + ATTR_IEEE, + str(ieee), ) else: _LOGGER.error( - "Warning IASWD: %s could not be found!", - "{}: [{}]".format(ATTR_IEEE, str(ieee)), + "Warning IASWD: %s: [%s] could not be found!", ATTR_IEEE, str(ieee) ) _LOGGER.debug( - "Warning IASWD: %s %s %s %s", - "{}: [{}]".format(ATTR_IEEE, str(ieee)), - "{}: [{}]".format(ATTR_WARNING_DEVICE_MODE, mode), - "{}: [{}]".format(ATTR_WARNING_DEVICE_STROBE, strobe), - "{}: [{}]".format(ATTR_LEVEL, level), + "Warning IASWD: %s: [%s] %s: [%s] %s: [%s] %s: [%s]", + ATTR_IEEE, + str(ieee), + ATTR_WARNING_DEVICE_MODE, + mode, + ATTR_WARNING_DEVICE_STROBE, + strobe, + ATTR_LEVEL, + level, ) hass.helpers.service.async_register_admin_service( @@ -741,7 +1074,14 @@ def async_load_api(hass): websocket_api.async_register_command(hass, websocket_permit_devices) websocket_api.async_register_command(hass, websocket_get_devices) + websocket_api.async_register_command(hass, websocket_get_groupable_devices) + websocket_api.async_register_command(hass, websocket_get_groups) websocket_api.async_register_command(hass, websocket_get_device) + websocket_api.async_register_command(hass, websocket_get_group) + websocket_api.async_register_command(hass, websocket_add_group) + websocket_api.async_register_command(hass, websocket_remove_groups) + websocket_api.async_register_command(hass, websocket_add_group_members) + websocket_api.async_register_command(hass, websocket_remove_group_members) websocket_api.async_register_command(hass, websocket_reconfigure_node) websocket_api.async_register_command(hass, websocket_device_clusters) websocket_api.async_register_command(hass, websocket_device_cluster_attributes) @@ -758,5 +1098,6 @@ def async_unload_api(hass): hass.services.async_remove(DOMAIN, SERVICE_REMOVE) hass.services.async_remove(DOMAIN, SERVICE_SET_ZIGBEE_CLUSTER_ATTRIBUTE) hass.services.async_remove(DOMAIN, SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND) + hass.services.async_remove(DOMAIN, SERVICE_ISSUE_ZIGBEE_GROUP_COMMAND) hass.services.async_remove(DOMAIN, SERVICE_WARNING_DEVICE_SQUAWK) hass.services.async_remove(DOMAIN, SERVICE_WARNING_DEVICE_WARN) diff --git a/homeassistant/components/zha/binary_sensor.py b/homeassistant/components/zha/binary_sensor.py index 24c2b92e739..d8bc1187be8 100644 --- a/homeassistant/components/zha/binary_sensor.py +++ b/homeassistant/components/zha/binary_sensor.py @@ -1,4 +1,5 @@ """Binary sensors on Zigbee Home Automation networks.""" +import functools import logging from homeassistant.components.binary_sensor import ( @@ -18,20 +19,16 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from .core.const import ( - CHANNEL_ATTRIBUTE, + CHANNEL_ACCELEROMETER, + CHANNEL_OCCUPANCY, CHANNEL_ON_OFF, CHANNEL_ZONE, DATA_ZHA, DATA_ZHA_DISPATCHERS, - SENSOR_ACCELERATION, - SENSOR_OCCUPANCY, - SENSOR_OPENING, - SENSOR_TYPE, SIGNAL_ATTR_UPDATED, - UNKNOWN, ZHA_DISCOVERY_NEW, - ZONE, ) +from .core.registries import ZHA_ENTITIES from .entity import ZhaEntity _LOGGER = logging.getLogger(__name__) @@ -46,20 +43,7 @@ CLASS_MAPPING = { 0x002D: DEVICE_CLASS_VIBRATION, } - -async def get_ias_device_class(channel): - """Get the HA device class from the channel.""" - zone_type = await channel.get_attribute_value("zone_type") - return CLASS_MAPPING.get(zone_type) - - -DEVICE_CLASS_REGISTRY = { - UNKNOWN: None, - SENSOR_OPENING: DEVICE_CLASS_OPENING, - ZONE: get_ias_device_class, - SENSOR_OCCUPANCY: DEVICE_CLASS_OCCUPANCY, - SENSOR_ACCELERATION: DEVICE_CLASS_MOVING, -} +STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -94,52 +78,39 @@ async def _async_setup_entities( """Set up the ZHA binary sensors.""" entities = [] for discovery_info in discovery_infos: - entities.append(BinarySensor(**discovery_info)) + zha_dev = discovery_info["zha_device"] + channels = discovery_info["channels"] - async_add_entities(entities, update_before_add=True) + entity = ZHA_ENTITIES.get_entity(DOMAIN, zha_dev, channels, BinarySensor) + if entity: + entities.append(entity(**discovery_info)) + + if entities: + async_add_entities(entities, update_before_add=True) class BinarySensor(ZhaEntity, BinarySensorDevice): """ZHA BinarySensor.""" - _domain = DOMAIN - _device_class = None + DEVICE_CLASS = None - def __init__(self, **kwargs): + def __init__(self, unique_id, zha_device, channels, **kwargs): """Initialize the ZHA binary sensor.""" - super().__init__(**kwargs) - self._device_state_attributes = {} - self._zone_channel = self.cluster_channels.get(CHANNEL_ZONE) - self._on_off_channel = self.cluster_channels.get(CHANNEL_ON_OFF) - self._attr_channel = self.cluster_channels.get(CHANNEL_ATTRIBUTE) - self._zha_sensor_type = kwargs[SENSOR_TYPE] + super().__init__(unique_id, zha_device, channels, **kwargs) + self._channel = channels[0] + self._device_class = self.DEVICE_CLASS - async def _determine_device_class(self): - """Determine the device class for this binary sensor.""" - device_class_supplier = DEVICE_CLASS_REGISTRY.get(self._zha_sensor_type) - if callable(device_class_supplier): - channel = self.cluster_channels.get(self._zha_sensor_type) - if channel is None: - return None - return await device_class_supplier(channel) - return device_class_supplier + async def get_device_class(self): + """Get the HA device class from the channel.""" + pass async def async_added_to_hass(self): """Run when about to be added to hass.""" - self._device_class = await self._determine_device_class() await super().async_added_to_hass() - if self._on_off_channel: - await self.async_accept_signal( - self._on_off_channel, SIGNAL_ATTR_UPDATED, self.async_set_state - ) - if self._zone_channel: - await self.async_accept_signal( - self._zone_channel, SIGNAL_ATTR_UPDATED, self.async_set_state - ) - if self._attr_channel: - await self.async_accept_signal( - self._attr_channel, SIGNAL_ATTR_UPDATED, self.async_set_state - ) + await self.get_device_class() + await self.async_accept_signal( + self._channel, SIGNAL_ATTR_UPDATED, self.async_set_state + ) @callback def async_restore_last_state(self, last_state): @@ -149,7 +120,7 @@ class BinarySensor(ZhaEntity, BinarySensorDevice): @property def is_on(self) -> bool: - """Return if the switch is on based on the statemachine.""" + """Return True if the switch is on based on the state machine.""" if self._state is None: return False return self._state @@ -167,13 +138,43 @@ class BinarySensor(ZhaEntity, BinarySensorDevice): async def async_update(self): """Attempt to retrieve on off state from the binary sensor.""" await super().async_update() - if self._on_off_channel: - self._state = await self._on_off_channel.get_attribute_value("on_off") - if self._zone_channel: - value = await self._zone_channel.get_attribute_value("zone_status") - if value is not None: - self._state = value & 3 - if self._attr_channel: - self._state = await self._attr_channel.get_attribute_value( - self._attr_channel.value_attribute - ) + attribute = getattr(self._channel, "value_attribute", "on_off") + self._state = await self._channel.get_attribute_value(attribute) + + +@STRICT_MATCH(channel_names=CHANNEL_ACCELEROMETER) +class Accelerometer(BinarySensor): + """ZHA BinarySensor.""" + + DEVICE_CLASS = DEVICE_CLASS_MOVING + + +@STRICT_MATCH(channel_names=CHANNEL_OCCUPANCY) +class Occupancy(BinarySensor): + """ZHA BinarySensor.""" + + DEVICE_CLASS = DEVICE_CLASS_OCCUPANCY + + +@STRICT_MATCH(channel_names=CHANNEL_ON_OFF) +class Opening(BinarySensor): + """ZHA BinarySensor.""" + + DEVICE_CLASS = DEVICE_CLASS_OPENING + + +@STRICT_MATCH(channel_names=CHANNEL_ZONE) +class IASZone(BinarySensor): + """ZHA IAS BinarySensor.""" + + async def get_device_class(self) -> None: + """Get the HA device class from the channel.""" + zone_type = await self._channel.get_attribute_value("zone_type") + self._device_class = CLASS_MAPPING.get(zone_type) + + async def async_update(self): + """Attempt to retrieve on off state from the binary sensor.""" + await super().async_update() + value = await self._channel.get_attribute_value("zone_status") + if value is not None: + self._state = value & 3 diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index 474cb15b41a..5ee0d0ee9bb 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -1,4 +1,5 @@ """Config flow for ZHA.""" +import asyncio from collections import OrderedDict import os @@ -9,11 +10,14 @@ from homeassistant import config_entries from .core.const import ( CONF_RADIO_TYPE, CONF_USB_PATH, + CONTROLLER, + DEFAULT_BAUDRATE, DEFAULT_DATABASE_NAME, DOMAIN, + ZHA_GW_RADIO, RadioType, ) -from .core.helpers import check_zigpy_connection +from .core.registries import RADIO_TYPES @config_entries.HANDLERS.register(DOMAIN) @@ -57,3 +61,20 @@ class ZhaFlowHandler(config_entries.ConfigFlow): return self.async_create_entry( title=import_info[CONF_USB_PATH], data=import_info ) + + +async def check_zigpy_connection(usb_path, radio_type, database_path): + """Test zigpy radio connection.""" + try: + radio = RADIO_TYPES[radio_type][ZHA_GW_RADIO]() + controller_application = RADIO_TYPES[radio_type][CONTROLLER] + except KeyError: + return False + try: + await radio.connect(usb_path, DEFAULT_BAUDRATE) + controller = controller_application(radio, database_path) + await asyncio.wait_for(controller.startup(auto_form=True), timeout=30) + await controller.shutdown() + except Exception: # pylint: disable=broad-except + return False + return True diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py index 29cecb7784e..5a337b2a537 100644 --- a/homeassistant/components/zha/core/channels/__init__.py +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -17,7 +17,6 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_send from ..const import ( - CHANNEL_ATTRIBUTE, CHANNEL_EVENT_RELAY, CHANNEL_ZDO, REPORT_CONFIG_DEFAULT, @@ -90,19 +89,19 @@ class ZigbeeChannel(LogMixin): self._generic_id = f"channel_0x{cluster.cluster_id:04x}" self._cluster = cluster self._zha_device = device - self._unique_id = "{}:{}:0x{:04x}".format( - str(device.ieee), cluster.endpoint.endpoint_id, cluster.cluster_id - ) - # this keeps logs consistent with zigpy logging - self._log_id = "0x{:04x}:{}:0x{:04x}".format( - device.nwk, cluster.endpoint.endpoint_id, cluster.cluster_id - ) + self._id = f"{cluster.endpoint.endpoint_id}:0x{cluster.cluster_id:04x}" + self._unique_id = f"{str(device.ieee)}:{self._id}" self._report_config = CLUSTER_REPORT_CONFIGS.get( self._cluster.cluster_id, self.REPORT_CONFIG ) self._status = ChannelStatus.CREATED self._cluster.add_listener(self) + @property + def id(self) -> str: + """Return channel id unique for this device only.""" + return self._id + @property def generic_id(self): """Return the generic id for this channel.""" @@ -264,8 +263,8 @@ class ZigbeeChannel(LogMixin): def log(self, level, msg, *args): """Log a message.""" - msg = "[%s]: " + msg - args = (self._log_id,) + args + msg = "[%s:%s]: " + msg + args = (self.device.nwk, self._id,) + args _LOGGER.log(level, msg, *args) def __getattr__(self, name): @@ -280,7 +279,6 @@ class ZigbeeChannel(LogMixin): class AttributeListeningChannel(ZigbeeChannel): """Channel for attribute reports from the cluster.""" - CHANNEL_NAME = CHANNEL_ATTRIBUTE REPORT_CONFIG = [{"attr": 0, "config": REPORT_CONFIG_DEFAULT}] def __init__(self, cluster, device): @@ -394,15 +392,17 @@ class EventRelayChannel(ZigbeeChannel): ) -# pylint: disable=wrong-import-position -from . import closures # noqa: F401 -from . import general # noqa: F401 -from . import homeautomation # noqa: F401 -from . import hvac # noqa: F401 -from . import lighting # noqa: F401 -from . import lightlink # noqa: F401 -from . import manufacturerspecific # noqa: F401 -from . import measurement # noqa: F401 -from . import protocol # noqa: F401 -from . import security # noqa: F401 -from . import smartenergy # noqa: F401 +# pylint: disable=wrong-import-position, import-outside-toplevel +from . import ( # noqa: F401 isort:skip + closures, + general, + homeautomation, + hvac, + lighting, + lightlink, + manufacturerspecific, + measurement, + protocol, + security, + smartenergy, +) diff --git a/homeassistant/components/zha/core/channels/homeautomation.py b/homeassistant/components/zha/core/channels/homeautomation.py index dda6c1f4c13..d9d8f57eaaf 100644 --- a/homeassistant/components/zha/core/channels/homeautomation.py +++ b/homeassistant/components/zha/core/channels/homeautomation.py @@ -5,6 +5,7 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/integrations/zha/ """ import logging +from typing import Optional import zigpy.zcl.clusters.homeautomation as homeautomation @@ -65,6 +66,12 @@ class ElectricalMeasurementChannel(AttributeListeningChannel): REPORT_CONFIG = ({"attr": "active_power", "config": REPORT_CONFIG_DEFAULT},) + def __init__(self, cluster, device): + """Initialize Metering.""" + super().__init__(cluster, device) + self._divisor = None + self._multiplier = None + async def async_update(self): """Retrieve latest state.""" self.debug("async_update") @@ -78,8 +85,39 @@ class ElectricalMeasurementChannel(AttributeListeningChannel): async def async_initialize(self, from_cache): """Initialize channel.""" await self.get_attribute_value("active_power", from_cache=from_cache) + await self.fetch_config(from_cache) await super().async_initialize(from_cache) + async def fetch_config(self, from_cache): + """Fetch config from device and updates format specifier.""" + divisor = await self.get_attribute_value( + "ac_power_divisor", from_cache=from_cache + ) + if divisor is None: + divisor = await self.get_attribute_value( + "power_divisor", from_cache=from_cache + ) + self._divisor = divisor + + mult = await self.get_attribute_value( + "ac_power_multiplier", from_cache=from_cache + ) + if mult is None: + mult = await self.get_attribute_value( + "power_multiplier", from_cache=from_cache + ) + self._multiplier = mult + + @property + def divisor(self) -> Optional[int]: + """Return active power divisor.""" + return self._divisor or 1 + + @property + def multiplier(self) -> Optional[int]: + """Return active power divisor.""" + return self._multiplier or 1 + @registries.ZIGBEE_CHANNEL_REGISTRY.register( homeautomation.MeterIdentification.cluster_id diff --git a/homeassistant/components/zha/core/channels/hvac.py b/homeassistant/components/zha/core/channels/hvac.py index 14d982ab1e8..db4745d51c3 100644 --- a/homeassistant/components/zha/core/channels/hvac.py +++ b/homeassistant/components/zha/core/channels/hvac.py @@ -6,6 +6,7 @@ https://home-assistant.io/integrations/zha/ """ import logging +from zigpy.exceptions import DeliveryError import zigpy.zcl.clusters.hvac as hvac from homeassistant.core import callback @@ -35,7 +36,6 @@ class FanChannel(ZigbeeChannel): async def async_set_speed(self, value) -> None: """Set the speed of the fan.""" - from zigpy.exceptions import DeliveryError try: await self.cluster.write_attributes({"fan_mode": value}) diff --git a/homeassistant/components/zha/core/channels/manufacturerspecific.py b/homeassistant/components/zha/core/channels/manufacturerspecific.py index 31dd5cd63d1..39f45f6c4a2 100644 --- a/homeassistant/components/zha/core/channels/manufacturerspecific.py +++ b/homeassistant/components/zha/core/channels/manufacturerspecific.py @@ -18,7 +18,6 @@ from ..const import ( SIGNAL_ATTR_UPDATED, ) - _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zha/core/channels/security.py b/homeassistant/components/zha/core/channels/security.py index e4840dae86d..69e4ea1a27a 100644 --- a/homeassistant/components/zha/core/channels/security.py +++ b/homeassistant/components/zha/core/channels/security.py @@ -6,6 +6,7 @@ https://home-assistant.io/integrations/zha/ """ import logging +from zigpy.exceptions import DeliveryError import zigpy.zcl.clusters.security as security from homeassistant.core import callback @@ -149,7 +150,6 @@ class IASZoneChannel(ZigbeeChannel): if self._zha_device.manufacturer == "LUMI": self.debug("finished IASZoneChannel configuration") return - from zigpy.exceptions import DeliveryError self.debug("started IASZoneChannel configuration") diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index ac83c2cdcd8..c658febfd2d 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -24,6 +24,7 @@ ATTR_LEVEL = "level" ATTR_LQI = "lqi" ATTR_MANUFACTURER = "manufacturer" ATTR_MANUFACTURER_CODE = "manufacturer_code" +ATTR_MEMBERS = "members" ATTR_MODEL = "model" ATTR_NAME = "name" ATTR_NWK = "nwk" @@ -42,6 +43,7 @@ ATTR_WARNING_DEVICE_STROBE_INTENSITY = "intensity" BAUD_RATES = [2400, 4800, 9600, 14400, 19200, 38400, 57600, 115200, 128000, 256000] +CHANNEL_ACCELEROMETER = "accelerometer" CHANNEL_ATTRIBUTE = "attribute" CHANNEL_BASIC = "basic" CHANNEL_COLOR = "light_color" @@ -49,10 +51,16 @@ CHANNEL_DOORLOCK = "door_lock" CHANNEL_ELECTRICAL_MEASUREMENT = "electrical_measurement" CHANNEL_EVENT_RELAY = "event_relay" CHANNEL_FAN = "fan" +CHANNEL_HUMIDITY = "humidity" CHANNEL_IAS_WD = "ias_wd" +CHANNEL_ILLUMINANCE = "illuminance" CHANNEL_LEVEL = ATTR_LEVEL +CHANNEL_OCCUPANCY = "occupancy" CHANNEL_ON_OFF = "on_off" CHANNEL_POWER_CONFIGURATION = "power" +CHANNEL_PRESSURE = "pressure" +CHANNEL_SMARTENERGY_METERING = "smartenergy_metering" +CHANNEL_TEMPERATURE = "temperature" CHANNEL_ZDO = "zdo" CHANNEL_ZONE = ZONE = "ias_zone" @@ -105,6 +113,10 @@ DISCOVERY_KEY = "zha_discovery_info" DOMAIN = "zha" +GROUP_ID = "group_id" +GROUP_IDS = "group_ids" +GROUP_NAME = "group_name" + MFG_CLUSTER_ID_START = 0xFC00 POWER_MAINS_POWERED = "Mains" @@ -161,15 +173,15 @@ REPORT_CONFIG_OP = ( SENSOR_ACCELERATION = "acceleration" SENSOR_BATTERY = "battery" -SENSOR_ELECTRICAL_MEASUREMENT = "electrical_measurement" +SENSOR_ELECTRICAL_MEASUREMENT = CHANNEL_ELECTRICAL_MEASUREMENT SENSOR_GENERIC = "generic" -SENSOR_HUMIDITY = "humidity" -SENSOR_ILLUMINANCE = "illuminance" +SENSOR_HUMIDITY = CHANNEL_HUMIDITY +SENSOR_ILLUMINANCE = CHANNEL_ILLUMINANCE SENSOR_METERING = "metering" -SENSOR_OCCUPANCY = "occupancy" +SENSOR_OCCUPANCY = CHANNEL_OCCUPANCY SENSOR_OPENING = "opening" -SENSOR_PRESSURE = "pressure" -SENSOR_TEMPERATURE = "temperature" +SENSOR_PRESSURE = CHANNEL_PRESSURE +SENSOR_TEMPERATURE = CHANNEL_TEMPERATURE SENSOR_TYPE = "sensor_type" SIGNAL_ATTR_UPDATED = "attribute_updated" diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index b3be8037ff6..77e0263c06c 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -11,8 +11,9 @@ import logging import time import zigpy.exceptions -import zigpy.quirks from zigpy.profiles import zha, zll +import zigpy.quirks +from zigpy.zcl.clusters.general import Groups from homeassistant.core import callback from homeassistant.helpers.dispatcher import ( @@ -179,6 +180,17 @@ class ZHADevice(LogMixin): """Return true if this device is an end device.""" return self._zigpy_device.node_desc.is_end_device + @property + def is_groupable(self): + """Return true if this device has a group cluster.""" + if not self.available: + return False + clusters = self.async_get_clusters() + for cluster_map in clusters.values(): + for clusters in cluster_map.values(): + if Groups.cluster_id in clusters: + return True + @property def gateway(self): """Return the gateway for this device.""" @@ -479,7 +491,7 @@ class ZHADevice(LogMixin): cluster_id, command, command_type, - args, + *args, cluster_type=CLUSTER_TYPE_IN, manufacturer=None, ): @@ -487,7 +499,6 @@ class ZHADevice(LogMixin): cluster = self.async_get_cluster(endpoint_id, cluster_id, cluster_type) if cluster is None: return None - response = None if command_type == CLUSTER_COMMAND_SERVER: response = await cluster.command( command, *args, manufacturer=manufacturer, expect_reply=True @@ -507,6 +518,14 @@ class ZHADevice(LogMixin): ) return response + async def async_add_to_group(self, group_id): + """Add this device to the provided zigbee group.""" + await self._zigpy_device.add_to_group(group_id) + + async def async_remove_from_group(self, group_id): + """Remove this device from the provided zigbee group.""" + await self._zigpy_device.remove_from_group(group_id) + def log(self, level, msg, *args): """Log a message.""" msg = "[%s](%s): " + msg diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py index e23862a7d3e..d128ed274c0 100644 --- a/homeassistant/components/zha/core/discovery.py +++ b/homeassistant/components/zha/core/discovery.py @@ -11,30 +11,18 @@ import zigpy.profiles from zigpy.zcl.clusters.general import OnOff, PowerConfiguration from homeassistant import const as ha_const -from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR -from homeassistant.components.sensor import DOMAIN as SENSOR from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_send from .channels import AttributeListeningChannel, EventRelayChannel, ZDOChannel -from .const import ( - COMPONENTS, - CONF_DEVICE_CONFIG, - DATA_ZHA, - SENSOR_GENERIC, - SENSOR_TYPE, - UNKNOWN, - ZHA_DISCOVERY_NEW, -) +from .const import COMPONENTS, CONF_DEVICE_CONFIG, DATA_ZHA, ZHA_DISCOVERY_NEW from .registries import ( - BINARY_SENSOR_TYPES, CHANNEL_ONLY_CLUSTERS, COMPONENT_CLUSTERS, DEVICE_CLASS, EVENT_RELAY_CLUSTERS, OUTPUT_CHANNEL_ONLY_CLUSTERS, REMOTE_DEVICE_TYPES, - SENSOR_TYPES, SINGLE_INPUT_CLUSTER_DEVICE_CLASS, SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS, ZIGBEE_CHANNEL_REGISTRY, @@ -163,15 +151,6 @@ def _async_handle_profile_match( "component": component, } - if component == BINARY_SENSOR: - discovery_info.update({SENSOR_TYPE: UNKNOWN}) - for cluster_id in profile_clusters: - if cluster_id in BINARY_SENSOR_TYPES: - discovery_info.update( - {SENSOR_TYPE: BINARY_SENSOR_TYPES.get(cluster_id, UNKNOWN)} - ) - break - return discovery_info @@ -291,13 +270,4 @@ def _async_handle_single_cluster_match( "component": component, } - if component == SENSOR: - discovery_info.update( - {SENSOR_TYPE: SENSOR_TYPES.get(cluster.cluster_id, SENSOR_GENERIC)} - ) - if component == BINARY_SENSOR: - discovery_info.update( - {SENSOR_TYPE: BINARY_SENSOR_TYPES.get(cluster.cluster_id, UNKNOWN)} - ) - return discovery_info diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 77702c8f3de..72931c665ee 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -20,7 +20,6 @@ from homeassistant.helpers.device_registry import ( ) from homeassistant.helpers.dispatcher import async_dispatcher_send -from ..api import async_get_device_info from .const import ( ATTR_IEEE, ATTR_MANUFACTURER, @@ -65,6 +64,7 @@ from .const import ( ) from .device import DeviceStatus, ZHADevice from .discovery import async_dispatch_discovery_info, async_process_endpoint +from .helpers import async_get_device_info from .patches import apply_application_controller_patch from .registries import RADIO_TYPES from .store import async_get_registry @@ -222,6 +222,10 @@ class ZHAGateway: """Return ZHADevice for given ieee.""" return self._devices.get(ieee) + def get_group(self, group_id): + """Return Group for given group id.""" + return self.application_controller.groups[group_id] + def get_entity_reference(self, entity_id): """Return entity reference for given entity_id if found.""" for entity_reference in itertools.chain.from_iterable( diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index d3f06090dae..981a03fe7b5 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -4,29 +4,20 @@ Helpers for Zigbee Home Automation. For more details about this component, please refer to the documentation at https://home-assistant.io/integrations/zha/ """ -import asyncio import collections import logging -import bellows.ezsp -import bellows.zigbee.application import zigpy.types -import zigpy_deconz.api -import zigpy_deconz.zigbee.application -import zigpy_xbee.api -import zigpy_xbee.zigbee.application -import zigpy_zigate.api -import zigpy_zigate.zigbee.application from homeassistant.core import callback from .const import ( + ATTR_NAME, CLUSTER_TYPE_IN, CLUSTER_TYPE_OUT, DATA_ZHA, DATA_ZHA_GATEWAY, - DEFAULT_BAUDRATE, - RadioType, + DOMAIN, ) from .registries import BINDABLE_CLUSTERS @@ -56,30 +47,6 @@ async def safe_read( return {} -async def check_zigpy_connection(usb_path, radio_type, database_path): - """Test zigpy radio connection.""" - if radio_type == RadioType.ezsp.name: - radio = bellows.ezsp.EZSP() - ControllerApplication = bellows.zigbee.application.ControllerApplication - elif radio_type == RadioType.xbee.name: - radio = zigpy_xbee.api.XBee() - ControllerApplication = zigpy_xbee.zigbee.application.ControllerApplication - elif radio_type == RadioType.deconz.name: - radio = zigpy_deconz.api.Deconz() - ControllerApplication = zigpy_deconz.zigbee.application.ControllerApplication - elif radio_type == RadioType.zigate.name: - radio = zigpy_zigate.api.ZiGate() - ControllerApplication = zigpy_zigate.zigbee.application.ControllerApplication - try: - await radio.connect(usb_path, DEFAULT_BAUDRATE) - controller = ControllerApplication(radio, database_path) - await asyncio.wait_for(controller.startup(auto_form=True), timeout=30) - await controller.shutdown() - except Exception: # pylint: disable=broad-except - return False - return True - - def get_attr_id_by_name(cluster, attr_name): """Get the attribute id for a cluster attribute by its name.""" return next( @@ -164,3 +131,28 @@ class LogMixin: def error(self, msg, *args): """Error level log.""" return self.log(logging.ERROR, msg, *args) + + +@callback +def async_get_device_info(hass, device, ha_device_registry=None): + """Get ZHA device.""" + zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + ret_device = {} + ret_device.update(device.device_info) + ret_device["entities"] = [ + { + "entity_id": entity_ref.reference_id, + ATTR_NAME: entity_ref.device_info[ATTR_NAME], + } + for entity_ref in zha_gateway.device_registry[device.ieee] + ] + + if ha_device_registry is not None: + reg_device = ha_device_registry.async_get_device( + {(DOMAIN, str(device.ieee))}, set() + ) + if reg_device is not None: + ret_device["user_given_name"] = reg_device.name_by_user + ret_device["device_reg_id"] = reg_device.id + ret_device["area_id"] = reg_device.area_id + return ret_device diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index dd007e125a6..d2ba0243a5c 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -5,7 +5,9 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/integrations/zha/ """ import collections +from typing import Callable, Set, Union +import attr import bellows.ezsp import bellows.zigbee.application import zigpy.profiles.zha @@ -27,28 +29,11 @@ from homeassistant.components.sensor import DOMAIN as SENSOR from homeassistant.components.switch import DOMAIN as SWITCH # importing channels updates registries -from . import channels # noqa: F401 pylint: disable=wrong-import-position,unused-import -from .const import ( - CONTROLLER, - SENSOR_ACCELERATION, - SENSOR_BATTERY, - SENSOR_ELECTRICAL_MEASUREMENT, - SENSOR_HUMIDITY, - SENSOR_ILLUMINANCE, - SENSOR_METERING, - SENSOR_OCCUPANCY, - SENSOR_OPENING, - SENSOR_PRESSURE, - SENSOR_TEMPERATURE, - ZHA_GW_RADIO, - ZHA_GW_RADIO_DESCRIPTION, - ZONE, - RadioType, -) -from .decorators import DictRegistry, SetRegistry +from . import channels # noqa: F401 pylint: disable=unused-import +from .const import CONTROLLER, ZHA_GW_RADIO, ZHA_GW_RADIO_DESCRIPTION, RadioType +from .decorators import CALLABLE_T, DictRegistry, SetRegistry BINARY_SENSOR_CLUSTERS = SetRegistry() -BINARY_SENSOR_TYPES = {} BINDABLE_CLUSTERS = SetRegistry() CHANNEL_ONLY_CLUSTERS = SetRegistry() CLUSTER_REPORT_CONFIGS = {} @@ -60,7 +45,6 @@ LIGHT_CLUSTERS = SetRegistry() OUTPUT_CHANNEL_ONLY_CLUSTERS = SetRegistry() RADIO_TYPES = {} REMOTE_DEVICE_TYPES = collections.defaultdict(list) -SENSOR_TYPES = {} SINGLE_INPUT_CLUSTER_DEVICE_CLASS = {} SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS = {} SWITCH_CLUSTERS = SetRegistry() @@ -110,15 +94,6 @@ def establish_device_mappings(): BINARY_SENSOR_CLUSTERS.add(SMARTTHINGS_ACCELERATION_CLUSTER) - BINARY_SENSOR_TYPES.update( - { - SMARTTHINGS_ACCELERATION_CLUSTER: SENSOR_ACCELERATION, - zcl.clusters.general.OnOff.cluster_id: SENSOR_OPENING, - zcl.clusters.measurement.OccupancySensing.cluster_id: SENSOR_OCCUPANCY, - zcl.clusters.security.IasZone.cluster_id: ZONE, - } - ) - DEVICE_CLASS[zigpy.profiles.zha.PROFILE_ID].update( { SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE: DEVICE_TRACKER, @@ -176,19 +151,6 @@ def establish_device_mappings(): {zcl.clusters.general.OnOff: BINARY_SENSOR} ) - SENSOR_TYPES.update( - { - SMARTTHINGS_HUMIDITY_CLUSTER: SENSOR_HUMIDITY, - zcl.clusters.general.PowerConfiguration.cluster_id: SENSOR_BATTERY, - zcl.clusters.homeautomation.ElectricalMeasurement.cluster_id: SENSOR_ELECTRICAL_MEASUREMENT, - zcl.clusters.measurement.IlluminanceMeasurement.cluster_id: SENSOR_ILLUMINANCE, - zcl.clusters.measurement.PressureMeasurement.cluster_id: SENSOR_PRESSURE, - zcl.clusters.measurement.RelativeHumidity.cluster_id: SENSOR_HUMIDITY, - zcl.clusters.measurement.TemperatureMeasurement.cluster_id: SENSOR_TEMPERATURE, - zcl.clusters.smartenergy.Metering.cluster_id: SENSOR_METERING, - } - ) - zha = zigpy.profiles.zha REMOTE_DEVICE_TYPES[zha.PROFILE_ID].append(zha.DeviceType.COLOR_CONTROLLER) REMOTE_DEVICE_TYPES[zha.PROFILE_ID].append(zha.DeviceType.COLOR_DIMMER_SWITCH) @@ -207,3 +169,135 @@ def establish_device_mappings(): REMOTE_DEVICE_TYPES[zll.PROFILE_ID].append(zll.DeviceType.CONTROL_BRIDGE) REMOTE_DEVICE_TYPES[zll.PROFILE_ID].append(zll.DeviceType.CONTROLLER) REMOTE_DEVICE_TYPES[zll.PROFILE_ID].append(zll.DeviceType.SCENE_CONTROLLER) + + +def set_or_callable(value): + """Convert single str or None to a set. Pass through callables and sets.""" + if value is None: + return frozenset() + if callable(value): + return value + if isinstance(value, (frozenset, set, list)): + return frozenset(value) + return frozenset([str(value)]) + + +@attr.s(frozen=True) +class MatchRule: + """Match a ZHA Entity to a channel name or generic id.""" + + channel_names: Union[Callable, Set[str], str] = attr.ib( + factory=frozenset, converter=set_or_callable + ) + generic_ids: Union[Callable, Set[str], str] = attr.ib( + factory=frozenset, converter=set_or_callable + ) + manufacturers: Union[Callable, Set[str], str] = attr.ib( + factory=frozenset, converter=set_or_callable + ) + models: Union[Callable, Set[str], str] = attr.ib( + factory=frozenset, converter=set_or_callable + ) + + +class ZHAEntityRegistry: + """Channel to ZHA Entity mapping.""" + + def __init__(self): + """Initialize Registry instance.""" + self._strict_registry = collections.defaultdict(dict) + self._loose_registry = collections.defaultdict(dict) + + def get_entity( + self, component: str, zha_device, chnls: dict, default: CALLABLE_T = None + ) -> CALLABLE_T: + """Match a ZHA Channels to a ZHA Entity class.""" + for match in self._strict_registry[component]: + if self._strict_matched(zha_device, chnls, match): + return self._strict_registry[component][match] + + return default + + 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, + ) -> Callable[[CALLABLE_T], CALLABLE_T]: + """Decorate a strict match rule.""" + + rule = MatchRule(channel_names, generic_ids, manufacturers, models) + + def decorator(zha_ent: CALLABLE_T) -> CALLABLE_T: + """Register a strict match rule. + + All non empty fields of a match rule must match. + """ + self._strict_registry[component][rule] = zha_ent + return zha_ent + + return decorator + + 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, + ) -> Callable[[CALLABLE_T], CALLABLE_T]: + """Decorate a loose match rule.""" + + rule = MatchRule(channel_names, generic_ids, manufacturers, models) + + def decorator(zha_entity: CALLABLE_T) -> CALLABLE_T: + """Register a loose match rule. + + All non empty fields of a match rule must match. + """ + self._loose_registry[component][rule] = zha_entity + return zha_entity + + return decorator + + def _strict_matched(self, zha_device, chnls: dict, rule: MatchRule) -> bool: + """Return True if this device matches the criteria.""" + return all(self._matched(zha_device, chnls, rule)) + + def _loose_matched(self, zha_device, chnls: dict, rule: MatchRule) -> bool: + """Return True if this device matches the criteria.""" + return any(self._matched(zha_device, chnls, rule)) + + @staticmethod + def _matched(zha_device, chnls: dict, rule: MatchRule) -> list: + """Return a list of field matches.""" + if not any(attr.asdict(rule).values()): + return [False] + + matches = [] + if rule.channel_names: + channel_names = {ch.name for ch in chnls} + matches.append(rule.channel_names.issubset(channel_names)) + + if rule.generic_ids: + all_generic_ids = {ch.generic_id for ch in chnls} + matches.append(rule.generic_ids.issubset(all_generic_ids)) + + if rule.manufacturers: + if callable(rule.manufacturers): + matches.append(rule.manufacturers(zha_device.manufacturer)) + else: + matches.append(zha_device.manufacturer in rule.manufacturers) + + if rule.models: + if callable(rule.models): + matches.append(rule.models(zha_device.model)) + else: + matches.append(zha_device.model in rule.models) + + return matches + + +ZHA_ENTITIES = ZHAEntityRegistry() diff --git a/homeassistant/components/zha/core/store.py b/homeassistant/components/zha/core/store.py index cea38517767..46fef76b656 100644 --- a/homeassistant/components/zha/core/store.py +++ b/homeassistant/components/zha/core/store.py @@ -1,9 +1,8 @@ """Data storage helper for ZHA.""" -# pylint: disable=W0611 +# pylint: disable=unused-import from collections import OrderedDict import logging -from typing import MutableMapping -from typing import cast +from typing import MutableMapping, cast import attr diff --git a/homeassistant/components/zha/device_tracker.py b/homeassistant/components/zha/device_tracker.py index 60a1f6c3c40..76548935814 100644 --- a/homeassistant/components/zha/device_tracker.py +++ b/homeassistant/components/zha/device_tracker.py @@ -1,4 +1,5 @@ """Support for the ZHA platform.""" +import functools import logging import time @@ -14,9 +15,11 @@ from .core.const import ( SIGNAL_ATTR_UPDATED, ZHA_DISCOVERY_NEW, ) +from .core.registries import ZHA_ENTITIES from .entity import ZhaEntity -from .sensor import battery_percentage_remaining_formatter +from .sensor import Battery +STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN) _LOGGER = logging.getLogger(__name__) @@ -47,11 +50,20 @@ async def _async_setup_entities( """Set up the ZHA device trackers.""" entities = [] for discovery_info in discovery_infos: - entities.append(ZHADeviceScannerEntity(**discovery_info)) + zha_dev = discovery_info["zha_device"] + channels = discovery_info["channels"] - async_add_entities(entities, update_before_add=True) + entity = ZHA_ENTITIES.get_entity( + DOMAIN, zha_dev, channels, ZHADeviceScannerEntity + ) + if entity: + entities.append(entity(**discovery_info)) + + if entities: + async_add_entities(entities, update_before_add=True) +@STRICT_MATCH(channel_names=CHANNEL_POWER_CONFIGURATION) class ZHADeviceScannerEntity(ScannerEntity, ZhaEntity): """Represent a tracked device.""" @@ -100,7 +112,7 @@ class ZHADeviceScannerEntity(ScannerEntity, ZhaEntity): """Handle tracking.""" self.debug("battery_percentage_remaining updated: %s", value) self._connected = True - self._battery_level = battery_percentage_remaining_formatter(value) + self._battery_level = Battery.formatter(value) self.async_schedule_update_ha_state() @property diff --git a/homeassistant/components/zha/device_trigger.py b/homeassistant/components/zha/device_trigger.py index cdd62b11d1e..b7c46e5a40a 100644 --- a/homeassistant/components/zha/device_trigger.py +++ b/homeassistant/components/zha/device_trigger.py @@ -2,11 +2,11 @@ import voluptuous as vol import homeassistant.components.automation.event as event +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, ) from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE -from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA from . import DOMAIN from .core.helpers import async_get_zha_device diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index 108d8e27a9f..102472d25b0 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -30,8 +30,6 @@ RESTART_GRACE_PERIOD = 7200 # 2 hours class ZhaEntity(RestoreEntity, LogMixin, entity.Entity): """A base class for ZHA entities.""" - _domain = None # Must be overridden by subclasses - def __init__(self, unique_id, zha_device, channels, skip_entity_id=False, **kwargs): """Init ZHA entity.""" self._force_update = False diff --git a/homeassistant/components/zha/fan.py b/homeassistant/components/zha/fan.py index 43ad2291cb7..f489447e530 100644 --- a/homeassistant/components/zha/fan.py +++ b/homeassistant/components/zha/fan.py @@ -1,4 +1,5 @@ """Fans on Zigbee Home Automation networks.""" +import functools import logging from homeassistant.components.fan import ( @@ -20,6 +21,7 @@ from .core.const import ( SIGNAL_ATTR_UPDATED, ZHA_DISCOVERY_NEW, ) +from .core.registries import ZHA_ENTITIES from .entity import ZhaEntity _LOGGER = logging.getLogger(__name__) @@ -45,6 +47,7 @@ SPEED_LIST = [ VALUE_TO_SPEED = dict(enumerate(SPEED_LIST)) SPEED_TO_VALUE = {speed: i for i, speed in enumerate(SPEED_LIST)} +STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -79,16 +82,21 @@ async def _async_setup_entities( """Set up the ZHA fans.""" entities = [] for discovery_info in discovery_infos: - entities.append(ZhaFan(**discovery_info)) + zha_dev = discovery_info["zha_device"] + channels = discovery_info["channels"] - async_add_entities(entities, update_before_add=True) + entity = ZHA_ENTITIES.get_entity(DOMAIN, zha_dev, channels, ZhaFan) + if entity: + entities.append(entity(**discovery_info)) + + if entities: + async_add_entities(entities, update_before_add=True) +@STRICT_MATCH(channel_names=CHANNEL_FAN) class ZhaFan(ZhaEntity, FanEntity): """Representation of a ZHA fan.""" - _domain = DOMAIN - def __init__(self, unique_id, zha_device, channels, **kwargs): """Init this sensor.""" super().__init__(unique_id, zha_device, channels, **kwargs) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index fb388afac0f..eb7d3297b43 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -1,5 +1,6 @@ """Lights on Zigbee Home Automation networks.""" from datetime import timedelta +import functools import logging from zigpy.zcl.foundation import Status @@ -21,6 +22,7 @@ from .core.const import ( SIGNAL_SET_LEVEL, ZHA_DISCOVERY_NEW, ) +from .core.registries import ZHA_ENTITIES from .entity import ZhaEntity _LOGGER = logging.getLogger(__name__) @@ -36,6 +38,7 @@ UPDATE_COLORLOOP_HUE = 0x8 UNSUPPORTED_ATTRIBUTE = 0x86 SCAN_INTERVAL = timedelta(minutes=60) +STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, light.DOMAIN) PARALLEL_UPDATES = 5 @@ -71,17 +74,21 @@ async def _async_setup_entities( """Set up the ZHA lights.""" entities = [] for discovery_info in discovery_infos: - zha_light = Light(**discovery_info) - entities.append(zha_light) + zha_dev = discovery_info["zha_device"] + channels = discovery_info["channels"] - async_add_entities(entities, update_before_add=True) + entity = ZHA_ENTITIES.get_entity(light.DOMAIN, zha_dev, channels, Light) + if entity: + entities.append(entity(**discovery_info)) + + if entities: + async_add_entities(entities, update_before_add=True) +@STRICT_MATCH(channel_names=CHANNEL_ON_OFF) class Light(ZhaEntity, light.Light): """Representation of a ZHA or ZLL light.""" - _domain = light.DOMAIN - def __init__(self, unique_id, zha_device, channels, **kwargs): """Initialize the ZHA light.""" super().__init__(unique_id, zha_device, channels, **kwargs) diff --git a/homeassistant/components/zha/lock.py b/homeassistant/components/zha/lock.py index a2151b4bdcb..bf82252246c 100644 --- a/homeassistant/components/zha/lock.py +++ b/homeassistant/components/zha/lock.py @@ -1,4 +1,5 @@ """Locks on Zigbee Home Automation networks.""" +import functools import logging from zigpy.zcl.foundation import Status @@ -19,6 +20,7 @@ from .core.const import ( SIGNAL_ATTR_UPDATED, ZHA_DISCOVERY_NEW, ) +from .core.registries import ZHA_ENTITIES from .entity import ZhaEntity _LOGGER = logging.getLogger(__name__) @@ -26,6 +28,7 @@ _LOGGER = logging.getLogger(__name__) """ The first state is Zigbee 'Not fully locked' """ STATE_LIST = [STATE_UNLOCKED, STATE_LOCKED, STATE_UNLOCKED] +STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN) VALUE_TO_STATE = dict(enumerate(STATE_LIST)) @@ -62,16 +65,21 @@ async def _async_setup_entities( """Set up the ZHA locks.""" entities = [] for discovery_info in discovery_infos: - entities.append(ZhaDoorLock(**discovery_info)) + zha_dev = discovery_info["zha_device"] + channels = discovery_info["channels"] - async_add_entities(entities, update_before_add=True) + entity = ZHA_ENTITIES.get_entity(DOMAIN, zha_dev, channels, ZhaDoorLock) + if entity: + entities.append(entity(**discovery_info)) + + if entities: + async_add_entities(entities, update_before_add=True) +@STRICT_MATCH(channel_names=CHANNEL_DOORLOCK) class ZhaDoorLock(ZhaEntity, LockDevice): """Representation of a ZHA lock.""" - _domain = DOMAIN - def __init__(self, unique_id, zha_device, channels, **kwargs): """Init this sensor.""" super().__init__(unique_id, zha_device, channels, **kwargs) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 8781625d326..3beca6fd3c5 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -4,11 +4,11 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ - "bellows-homeassistant==0.11.0", - "zha-quirks==0.0.28", + "bellows-homeassistant==0.12.0", + "zha-quirks==0.0.30", "zigpy-deconz==0.7.0", - "zigpy-homeassistant==0.11.0", - "zigpy-xbee-homeassistant==0.7.0", + "zigpy-homeassistant==0.12.0", + "zigpy-xbee-homeassistant==0.8.0", "zigpy-zigate==0.5.0" ], "dependencies": [], diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index b260dfc5459..2d39d562bf5 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -1,4 +1,5 @@ """Sensors on Zigbee Home Automation networks.""" +import functools import logging import numbers @@ -16,25 +17,20 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from .core.const import ( - CHANNEL_ATTRIBUTE, CHANNEL_ELECTRICAL_MEASUREMENT, + CHANNEL_HUMIDITY, + CHANNEL_ILLUMINANCE, CHANNEL_POWER_CONFIGURATION, + CHANNEL_PRESSURE, + CHANNEL_SMARTENERGY_METERING, + CHANNEL_TEMPERATURE, DATA_ZHA, DATA_ZHA_DISPATCHERS, - SENSOR_BATTERY, - SENSOR_ELECTRICAL_MEASUREMENT, - SENSOR_GENERIC, - SENSOR_HUMIDITY, - SENSOR_ILLUMINANCE, - SENSOR_METERING, - SENSOR_PRESSURE, - SENSOR_TEMPERATURE, - SENSOR_TYPE, SIGNAL_ATTR_UPDATED, SIGNAL_STATE_ATTR, - UNKNOWN, ZHA_DISCOVERY_NEW, ) +from .core.registries import SMARTTHINGS_HUMIDITY_CLUSTER, ZHA_ENTITIES from .entity import ZhaEntity PARALLEL_UPDATES = 5 @@ -56,115 +52,8 @@ BATTERY_SIZES = { 255: "Unknown", } - -# Formatter functions -def pass_through_formatter(value): - """No op update function.""" - return value - - -def illuminance_formatter(value): - """Convert Illimination data.""" - if value is None: - return None - return round(pow(10, ((value - 1) / 10000)), 1) - - -def temperature_formatter(value): - """Convert temperature data.""" - if value is None: - return None - return round(value / 100, 1) - - -def humidity_formatter(value): - """Return the state of the entity.""" - if value is None: - return None - return round(float(value) / 100, 1) - - -def active_power_formatter(value): - """Return the state of the entity.""" - if value is None: - return None - return round(float(value) / 10, 1) - - -def pressure_formatter(value): - """Return the state of the entity.""" - if value is None: - return None - - return round(float(value)) - - -def battery_percentage_remaining_formatter(value): - """Return the state of the entity.""" - # per zcl specs battery percent is reported at 200% ¯\_(ツ)_/¯ - if not isinstance(value, numbers.Number) or value == -1: - return value - value = value / 2 - value = int(round(value)) - return value - - -async def async_battery_device_state_attr_provider(channel): - """Return device statr attrs for battery sensors.""" - state_attrs = {} - battery_size = await channel.get_attribute_value("battery_size") - if battery_size is not None: - state_attrs["battery_size"] = BATTERY_SIZES.get(battery_size, "Unknown") - battery_quantity = await channel.get_attribute_value("battery_quantity") - if battery_quantity is not None: - state_attrs["battery_quantity"] = battery_quantity - return state_attrs - - -FORMATTER_FUNC_REGISTRY = { - SENSOR_HUMIDITY: humidity_formatter, - SENSOR_TEMPERATURE: temperature_formatter, - SENSOR_PRESSURE: pressure_formatter, - SENSOR_ELECTRICAL_MEASUREMENT: active_power_formatter, - SENSOR_ILLUMINANCE: illuminance_formatter, - SENSOR_GENERIC: pass_through_formatter, - SENSOR_BATTERY: battery_percentage_remaining_formatter, -} - -UNIT_REGISTRY = { - SENSOR_HUMIDITY: "%", - SENSOR_TEMPERATURE: TEMP_CELSIUS, - SENSOR_PRESSURE: "hPa", - SENSOR_ILLUMINANCE: "lx", - SENSOR_ELECTRICAL_MEASUREMENT: POWER_WATT, - SENSOR_GENERIC: None, - SENSOR_BATTERY: "%", -} - -CHANNEL_REGISTRY = { - SENSOR_ELECTRICAL_MEASUREMENT: CHANNEL_ELECTRICAL_MEASUREMENT, - SENSOR_BATTERY: CHANNEL_POWER_CONFIGURATION, -} - -POLLING_REGISTRY = {SENSOR_ELECTRICAL_MEASUREMENT: True} - -FORCE_UPDATE_REGISTRY = {SENSOR_ELECTRICAL_MEASUREMENT: False} - -DEVICE_CLASS_REGISTRY = { - UNKNOWN: None, - SENSOR_HUMIDITY: DEVICE_CLASS_HUMIDITY, - SENSOR_TEMPERATURE: DEVICE_CLASS_TEMPERATURE, - SENSOR_PRESSURE: DEVICE_CLASS_PRESSURE, - SENSOR_ILLUMINANCE: DEVICE_CLASS_ILLUMINANCE, - SENSOR_METERING: DEVICE_CLASS_POWER, - SENSOR_ELECTRICAL_MEASUREMENT: DEVICE_CLASS_POWER, - SENSOR_BATTERY: DEVICE_CLASS_BATTERY, -} - - -DEVICE_STATE_ATTR_PROVIDER_REGISTRY = { - SENSOR_BATTERY: async_battery_device_state_attr_provider -} +CHANNEL_ST_HUMIDITY_CLUSTER = f"channel_0x{SMARTTHINGS_HUMIDITY_CLUSTER:04x}" +STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -201,48 +90,39 @@ async def _async_setup_entities( for discovery_info in discovery_infos: entities.append(await make_sensor(discovery_info)) - async_add_entities(entities, update_before_add=True) + if entities: + async_add_entities(entities, update_before_add=True) async def make_sensor(discovery_info): """Create ZHA sensors factory.""" - return Sensor(**discovery_info) + + zha_dev = discovery_info["zha_device"] + channels = discovery_info["channels"] + + entity = ZHA_ENTITIES.get_entity(DOMAIN, zha_dev, channels, Sensor) + return entity(**discovery_info) class Sensor(ZhaEntity): """Base ZHA sensor.""" - _domain = DOMAIN + _decimals = 1 + _device_class = None + _divisor = 1 + _multiplier = 1 + _unit = None def __init__(self, unique_id, zha_device, channels, **kwargs): """Init this sensor.""" super().__init__(unique_id, zha_device, channels, **kwargs) - self._sensor_type = kwargs.get(SENSOR_TYPE, SENSOR_GENERIC) - self._channel = self.cluster_channels.get( - CHANNEL_REGISTRY.get(self._sensor_type, CHANNEL_ATTRIBUTE) - ) - if self._sensor_type == SENSOR_METERING: - self._unit = self._channel.unit_of_measurement - self._formatter_function = self._channel.formatter_function - else: - self._unit = UNIT_REGISTRY.get(self._sensor_type) - self._formatter_function = FORMATTER_FUNC_REGISTRY.get( - self._sensor_type, pass_through_formatter - ) - self._force_update = FORCE_UPDATE_REGISTRY.get(self._sensor_type, False) - self._should_poll = POLLING_REGISTRY.get(self._sensor_type, False) - self._device_class = DEVICE_CLASS_REGISTRY.get(self._sensor_type, None) - self.state_attr_provider = DEVICE_STATE_ATTR_PROVIDER_REGISTRY.get( - self._sensor_type, None - ) + self._channel = channels[0] async def async_added_to_hass(self): """Run when about to be added to hass.""" await super().async_added_to_hass() - if self.state_attr_provider is not None: - self._device_state_attributes = await self.state_attr_provider( - self._channel - ) + self._device_state_attributes = await self.async_state_attr_provider() + await self.async_accept_signal( self._channel, SIGNAL_ATTR_UPDATED, self.async_set_state ) @@ -271,14 +151,9 @@ class Sensor(ZhaEntity): def async_set_state(self, state): """Handle state update from channel.""" - # this is necessary because HA saves the unit based on what shows in - # the UI and not based on what the sensor has configured so we need - # to flip it back after state restoration - if self._sensor_type == SENSOR_METERING: - self._unit = self._channel.unit_of_measurement - else: - self._unit = UNIT_REGISTRY.get(self._sensor_type) - self._state = self._formatter_function(state) + if state is not None: + state = self.formatter(state) + self._state = state self.async_schedule_update_ha_state() @callback @@ -286,3 +161,119 @@ class Sensor(ZhaEntity): """Restore previous state.""" self._state = last_state.state self._unit = last_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + + @callback + async def async_state_attr_provider(self): + """Initialize device state attributes.""" + return {} + + def formatter(self, value): + """Numeric pass-through formatter.""" + if self._decimals > 0: + return round( + float(value * self._multiplier) / self._divisor, self._decimals + ) + return round(float(value * self._multiplier) / self._divisor) + + +@STRICT_MATCH(channel_names=CHANNEL_POWER_CONFIGURATION) +class Battery(Sensor): + """Battery sensor of power configuration cluster.""" + + _device_class = DEVICE_CLASS_BATTERY + _unit = "%" + + @staticmethod + def formatter(value): + """Return the state of the entity.""" + # per zcl specs battery percent is reported at 200% ¯\_(ツ)_/¯ + if not isinstance(value, numbers.Number) or value == -1: + return value + value = round(value / 2) + return value + + async def async_state_attr_provider(self): + """Return device state attrs for battery sensors.""" + state_attrs = {} + battery_size = await self._channel.get_attribute_value("battery_size") + if battery_size is not None: + state_attrs["battery_size"] = BATTERY_SIZES.get(battery_size, "Unknown") + battery_quantity = await self._channel.get_attribute_value("battery_quantity") + if battery_quantity is not None: + state_attrs["battery_quantity"] = battery_quantity + return state_attrs + + +@STRICT_MATCH(channel_names=CHANNEL_ELECTRICAL_MEASUREMENT) +class ElectricalMeasurement(Sensor): + """Active power measurement.""" + + _device_class = DEVICE_CLASS_POWER + _divisor = 10 + _unit = POWER_WATT + + @property + def should_poll(self) -> bool: + """Return True if HA needs to poll for state changes.""" + return True + + def formatter(self, value) -> int: + """Return 'normalized' value.""" + return round(value * self._channel.multiplier / self._channel.divisor) + + +@STRICT_MATCH(generic_ids=CHANNEL_ST_HUMIDITY_CLUSTER) +@STRICT_MATCH(channel_names=CHANNEL_HUMIDITY) +class Humidity(Sensor): + """Humidity sensor.""" + + _device_class = DEVICE_CLASS_HUMIDITY + _divisor = 100 + _unit = "%" + + +@STRICT_MATCH(channel_names=CHANNEL_ILLUMINANCE) +class Illuminance(Sensor): + """Illuminance Sensor.""" + + _device_class = DEVICE_CLASS_ILLUMINANCE + _unit = "lx" + + @staticmethod + def formatter(value): + """Convert illumination data.""" + return round(pow(10, ((value - 1) / 10000)), 1) + + +@STRICT_MATCH(channel_names=CHANNEL_SMARTENERGY_METERING) +class SmartEnergyMetering(Sensor): + """Metering sensor.""" + + _device_class = DEVICE_CLASS_POWER + + def formatter(self, value): + """Pass through channel formatter.""" + return self._channel.formatter_function(value) + + @property + def unit_of_measurement(self): + """Return Unit of measurement.""" + return self._channel.unit_of_measurement + + +@STRICT_MATCH(channel_names=CHANNEL_PRESSURE) +class Pressure(Sensor): + """Pressure sensor.""" + + _device_class = DEVICE_CLASS_PRESSURE + _decimals = 0 + _unit = "hPa" + + +@STRICT_MATCH(channel_names=CHANNEL_TEMPERATURE) +class Temperature(Sensor): + """Temperature Sensor.""" + + _device_class = DEVICE_CLASS_TEMPERATURE + _divisor = 100 + _unit = TEMP_CELSIUS diff --git a/homeassistant/components/zha/services.yaml b/homeassistant/components/zha/services.yaml index d279af46335..ab8e9c19580 100644 --- a/homeassistant/components/zha/services.yaml +++ b/homeassistant/components/zha/services.yaml @@ -78,7 +78,27 @@ issue_zigbee_cluster_command: example: "server" args: description: args to pass to the command - example: {} + example: '[arg1, arg2, argN]' + manufacturer: + description: manufacturer code + example: 0x00FC + +issue_zigbee_group_command: + description: >- + Issue command on the specified cluster on the specified group. + fields: + group: + description: Hexadecimal address of the group + example: 0x0222 + cluster_id: + description: ZCL cluster to send command to + example: 6 + command: + description: id of the command to execute + example: 0 + args: + description: args to pass to the command + example: '[arg1, arg2, argN]' manufacturer: description: manufacturer code example: 0x00FC diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py index bfe816d614a..cbd29925f62 100644 --- a/homeassistant/components/zha/switch.py +++ b/homeassistant/components/zha/switch.py @@ -1,4 +1,5 @@ """Switches on Zigbee Home Automation networks.""" +import functools import logging from zigpy.zcl.foundation import Status @@ -15,9 +16,11 @@ from .core.const import ( SIGNAL_ATTR_UPDATED, ZHA_DISCOVERY_NEW, ) +from .core.registries import ZHA_ENTITIES from .entity import ZhaEntity _LOGGER = logging.getLogger(__name__) +STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -52,16 +55,21 @@ async def _async_setup_entities( """Set up the ZHA switches.""" entities = [] for discovery_info in discovery_infos: - entities.append(Switch(**discovery_info)) + zha_dev = discovery_info["zha_device"] + channels = discovery_info["channels"] - async_add_entities(entities, update_before_add=True) + entity = ZHA_ENTITIES.get_entity(DOMAIN, zha_dev, channels, Switch) + if entity: + entities.append(entity(**discovery_info)) + + if entities: + async_add_entities(entities, update_before_add=True) +@STRICT_MATCH(channel_names=CHANNEL_ON_OFF) class Switch(ZhaEntity, SwitchDevice): """ZHA switch.""" - _domain = DOMAIN - def __init__(self, **kwargs): """Initialize the ZHA switch.""" super().__init__(**kwargs) diff --git a/homeassistant/components/zhong_hong/climate.py b/homeassistant/components/zhong_hong/climate.py index f1a363cfede..b94e19d6dbd 100644 --- a/homeassistant/components/zhong_hong/climate.py +++ b/homeassistant/components/zhong_hong/climate.py @@ -2,6 +2,8 @@ import logging import voluptuous as vol +from zhong_hong_hvac.hub import ZhongHongGateway +from zhong_hong_hvac.hvac import HVAC as ZhongHongHVAC from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice from homeassistant.components.climate.const import ( @@ -71,7 +73,6 @@ MODE_TO_STATE = { def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the ZhongHong HVAC platform.""" - from zhong_hong_hvac.hub import ZhongHongGateway host = config.get(CONF_HOST) port = config.get(CONF_PORT) @@ -117,9 +118,8 @@ class ZhongHongClimate(ClimateDevice): def __init__(self, hub, addr_out, addr_in): """Set up the ZhongHong climate devices.""" - from zhong_hong_hvac.hvac import HVAC - self._device = HVAC(hub, addr_out, addr_in) + self._device = ZhongHongHVAC(hub, addr_out, addr_in) self._hub = hub self._current_operation = None self._current_temperature = None diff --git a/homeassistant/components/zigbee/__init__.py b/homeassistant/components/zigbee/__init__.py index e74726a70f9..475d63a5c3b 100644 --- a/homeassistant/components/zigbee/__init__.py +++ b/homeassistant/components/zigbee/__init__.py @@ -1,24 +1,24 @@ """Support for Zigbee devices.""" -import logging from binascii import hexlify, unhexlify +import logging -import xbee_helper.const as xb_const -from xbee_helper import ZigBee -from xbee_helper.device import convert_adc -from xbee_helper.exceptions import ZigBeeException, ZigBeeTxFailure from serial import Serial, SerialException import voluptuous as vol +from xbee_helper import ZigBee +import xbee_helper.const as xb_const +from xbee_helper.device import convert_adc +from xbee_helper.exceptions import ZigBeeException, ZigBeeTxFailure from homeassistant.const import ( - EVENT_HOMEASSISTANT_STOP, + CONF_ADDRESS, CONF_DEVICE, CONF_NAME, CONF_PIN, - CONF_ADDRESS, + EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.helpers.entity import Entity from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -335,7 +335,7 @@ class ZigBeeDigitalIn(Entity): pin_name = DIGITAL_PINS[self._config.pin] if pin_name not in sample: _LOGGER.warning( - "Pin %s (%s) was not in the sample provided by Zigbee device " "%s.", + "Pin %s (%s) was not in the sample provided by Zigbee device %s.", self._config.pin, pin_name, hexlify(self._config.address), diff --git a/homeassistant/components/zigbee/switch.py b/homeassistant/components/zigbee/switch.py index 16af6ec7e3a..4e8d21f438a 100644 --- a/homeassistant/components/zigbee/switch.py +++ b/homeassistant/components/zigbee/switch.py @@ -5,7 +5,6 @@ from homeassistant.components.switch import SwitchDevice from . import PLATFORM_SCHEMA, ZigBeeDigitalOut, ZigBeeDigitalOutConfig - CONF_ON_STATE = "on_state" DEFAULT_ON_STATE = "high" diff --git a/homeassistant/components/ziggo_mediabox_xl/media_player.py b/homeassistant/components/ziggo_mediabox_xl/media_player.py index a5f8b38ac37..83a7dbbaba9 100644 --- a/homeassistant/components/ziggo_mediabox_xl/media_player.py +++ b/homeassistant/components/ziggo_mediabox_xl/media_player.py @@ -3,8 +3,9 @@ import logging import socket import voluptuous as vol +from ziggo_mediabox_xl import ZiggoMediaboxXL -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, @@ -44,7 +45,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Ziggo Mediabox XL platform.""" - from ziggo_mediabox_xl import ZiggoMediaboxXL hass.data[DATA_KNOWN_DEVICES] = known_devices = set() diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py index 6ae62be3eb9..357d4eac172 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -4,29 +4,28 @@ from typing import Set, cast import voluptuous as vol -from homeassistant.core import callback, State -from homeassistant.loader import bind_hass -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( - CONF_NAME, + ATTR_LATITUDE, + ATTR_LONGITUDE, + CONF_ICON, CONF_LATITUDE, CONF_LONGITUDE, - CONF_ICON, + CONF_NAME, CONF_RADIUS, EVENT_CORE_CONFIG_UPDATE, ) +from homeassistant.core import State, callback from homeassistant.helpers import config_per_platform +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id +from homeassistant.loader import bind_hass from homeassistant.util import slugify -from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE from homeassistant.util.location import distance - from .config_flow import configured_zones -from .const import CONF_PASSIVE, DOMAIN, HOME_ZONE, ATTR_PASSIVE, ATTR_RADIUS +from .const import ATTR_PASSIVE, ATTR_RADIUS, CONF_PASSIVE, DOMAIN, HOME_ZONE from .zone import Zone - # mypy: allow-untyped-calls, allow-untyped-defs _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zone/config_flow.py b/homeassistant/components/zone/config_flow.py index 39633754772..4531ff7b834 100644 --- a/homeassistant/components/zone/config_flow.py +++ b/homeassistant/components/zone/config_flow.py @@ -4,22 +4,21 @@ from typing import Set import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant import config_entries from homeassistant.const import ( - CONF_NAME, + CONF_ICON, CONF_LATITUDE, CONF_LONGITUDE, - CONF_ICON, + CONF_NAME, CONF_RADIUS, ) from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import slugify from .const import CONF_PASSIVE, DOMAIN, HOME_ZONE - # mypy: allow-untyped-defs, no-check-untyped-defs diff --git a/homeassistant/components/zoneminder/__init__.py b/homeassistant/components/zoneminder/__init__.py index a116cc31891..3007c981480 100644 --- a/homeassistant/components/zoneminder/__init__.py +++ b/homeassistant/components/zoneminder/__init__.py @@ -2,18 +2,19 @@ import logging import voluptuous as vol +from zoneminder.zm import ZoneMinder -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( + ATTR_ID, + ATTR_NAME, CONF_HOST, CONF_PASSWORD, CONF_PATH, CONF_SSL, CONF_USERNAME, CONF_VERIFY_SSL, - ATTR_NAME, - ATTR_ID, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import async_load_platform _LOGGER = logging.getLogger(__name__) @@ -51,7 +52,6 @@ SET_RUN_STATE_SCHEMA = vol.Schema( def setup(hass, config): """Set up the ZoneMinder component.""" - from zoneminder.zm import ZoneMinder hass.data[DOMAIN] = {} diff --git a/homeassistant/components/zoneminder/sensor.py b/homeassistant/components/zoneminder/sensor.py index bfcfcb8f907..75531e79e13 100644 --- a/homeassistant/components/zoneminder/sensor.py +++ b/homeassistant/components/zoneminder/sensor.py @@ -2,6 +2,7 @@ import logging import voluptuous as vol +from zoneminder.monitor import TimePeriod from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_MONITORED_CONDITIONS @@ -95,7 +96,6 @@ class ZMSensorEvents(Entity): def __init__(self, monitor, include_archived, sensor_type): """Initialize event sensor.""" - from zoneminder.monitor import TimePeriod self._monitor = monitor self._include_archived = include_archived diff --git a/homeassistant/components/zoneminder/switch.py b/homeassistant/components/zoneminder/switch.py index d2d761aab1e..5eaf2ed4901 100644 --- a/homeassistant/components/zoneminder/switch.py +++ b/homeassistant/components/zoneminder/switch.py @@ -2,6 +2,7 @@ import logging import voluptuous as vol +from zoneminder.monitor import MonitorState from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import CONF_COMMAND_OFF, CONF_COMMAND_ON @@ -21,7 +22,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the ZoneMinder switch platform.""" - from zoneminder.monitor import MonitorState on_state = MonitorState(config.get(CONF_COMMAND_ON)) off_state = MonitorState(config.get(CONF_COMMAND_OFF)) diff --git a/homeassistant/components/zwave/.translations/da.json b/homeassistant/components/zwave/.translations/da.json index e9049026a4f..25eee9b3d91 100644 --- a/homeassistant/components/zwave/.translations/da.json +++ b/homeassistant/components/zwave/.translations/da.json @@ -2,16 +2,16 @@ "config": { "abort": { "already_configured": "Z-Wave er allerede konfigureret", - "one_instance_only": "Komponenten underst\u00f8tter kun \u00e9n Z-Wave forekomst" + "one_instance_only": "Komponenten underst\u00f8tter kun \u00e9n Z-Wave-instans" }, "error": { - "option_error": "Z-Wave validering mislykkedes. Er stien til USB enhed korrekt?" + "option_error": "Z-Wave-validering mislykkedes. Er stien til USB-enhed korrekt?" }, "step": { "user": { "data": { "network_key": "Netv\u00e6rksn\u00f8gle (efterlad blank for autogenerering)", - "usb_path": "Sti til USB enhed" + "usb_path": "Sti til USB-enhed" }, "description": "Se https://www.home-assistant.io/docs/z-wave/installation/ for oplysninger om konfigurationsvariabler", "title": "Ops\u00e6t Z-Wave" diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index 293cc45273f..32348a8becc 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -8,62 +8,60 @@ from pprint import pprint import voluptuous as vol from homeassistant import config_entries -from homeassistant.core import callback, CoreState +from homeassistant.const import ( + ATTR_ENTITY_ID, + EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP, +) +from homeassistant.core import CoreState, callback from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import ( + async_get_registry as async_get_device_registry, +) +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.entity_component import DEFAULT_SCAN_INTERVAL from homeassistant.helpers.entity_platform import EntityPlatform from homeassistant.helpers.entity_registry import ( async_get_registry as async_get_entity_registry, ) -from homeassistant.helpers.device_registry import ( - async_get_registry as async_get_device_registry, -) -from homeassistant.const import ( - ATTR_ENTITY_ID, - EVENT_HOMEASSISTANT_START, - EVENT_HOMEASSISTANT_STOP, -) from homeassistant.helpers.entity_values import EntityValues from homeassistant.helpers.event import async_track_time_change from homeassistant.util import convert import homeassistant.util.dt as dt_util -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) -from . import const from . import config_flow # noqa: F401 pylint: disable=unused-import -from . import websocket_api as wsapi +from . import const, websocket_api as wsapi, workaround from .const import ( CONF_AUTOHEAL, + CONF_CONFIG_PATH, CONF_DEBUG, + CONF_NETWORK_KEY, CONF_POLLING_INTERVAL, CONF_USB_STICK_PATH, - CONF_CONFIG_PATH, - CONF_NETWORK_KEY, + DATA_DEVICES, + DATA_ENTITY_VALUES, + DATA_NETWORK, + DATA_ZWAVE_CONFIG, DEFAULT_CONF_AUTOHEAL, DEFAULT_CONF_USB_STICK_PATH, - DEFAULT_POLLING_INTERVAL, DEFAULT_DEBUG, + DEFAULT_POLLING_INTERVAL, DOMAIN, - DATA_DEVICES, - DATA_NETWORK, - DATA_ENTITY_VALUES, - DATA_ZWAVE_CONFIG, ) -from .node_entity import ZWaveBaseEntity, ZWaveNodeEntity -from . import workaround from .discovery_schemas import DISCOVERY_SCHEMAS +from .node_entity import ZWaveBaseEntity, ZWaveNodeEntity from .util import ( + check_has_unique_id, check_node_schema, check_value_schema, - node_name, - check_has_unique_id, is_node_parsed, node_device_id_and_name, + node_name, ) _LOGGER = logging.getLogger(__name__) @@ -81,12 +79,14 @@ CONF_REFRESH_DELAY = "delay" CONF_DEVICE_CONFIG = "device_config" CONF_DEVICE_CONFIG_GLOB = "device_config_glob" CONF_DEVICE_CONFIG_DOMAIN = "device_config_domain" +CONF_TILT_OPEN_POSITION = "tilt_open_position" DEFAULT_CONF_IGNORED = False DEFAULT_CONF_INVERT_OPENCLOSE_BUTTONS = False DEFAULT_CONF_INVERT_PERCENT = False DEFAULT_CONF_REFRESH_VALUE = False DEFAULT_CONF_REFRESH_DELAY = 5 +DEFAULT_CONF_TILT_OPEN_POSITION = 50 SUPPORTED_PLATFORMS = [ "binary_sensor", @@ -216,6 +216,9 @@ DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema( vol.Optional( CONF_REFRESH_DELAY, default=DEFAULT_CONF_REFRESH_DELAY ): cv.positive_int, + vol.Optional( + CONF_TILT_OPEN_POSITION, default=DEFAULT_CONF_TILT_OPEN_POSITION + ): cv.positive_int, } ) @@ -272,7 +275,7 @@ def nice_print_node(node): value_id: _obj_to_dict(value) for value_id, value in node.values.items() } - _LOGGER.info("FOUND NODE %s \n" "%s", node.product_name, node_dict) + _LOGGER.info("FOUND NODE %s \n%s", node.product_name, node_dict) def get_config_value(node, value_index, tries=5): @@ -473,7 +476,7 @@ async def async_setup_entry(hass, config_entry): @callback def _on_timeout(sec): _LOGGER.warning( - "Z-Wave node %d not ready after %d seconds, " "continuing anyway", + "Z-Wave node %d not ready after %d seconds, continuing anyway", entity.node_id, sec, ) @@ -523,7 +526,7 @@ async def async_setup_entry(hass, config_entry): def network_complete(): """Handle the querying of all nodes on network.""" _LOGGER.info( - "Z-Wave network is complete. All nodes on the network " "have been queried" + "Z-Wave network is complete. All nodes on the network have been queried" ) hass.bus.fire(const.EVENT_NETWORK_COMPLETE) @@ -681,7 +684,7 @@ async def async_setup_entry(hass, config_entry): if value.type == const.TYPE_BOOL: value.data = int(selection == "True") _LOGGER.info( - "Setting config parameter %s on Node %s " "with bool selection %s", + "Setting config parameter %s on Node %s with bool selection %s", param, node_id, str(selection), @@ -690,7 +693,7 @@ async def async_setup_entry(hass, config_entry): if value.type == const.TYPE_LIST: value.data = str(selection) _LOGGER.info( - "Setting config parameter %s on Node %s " "with list selection %s", + "Setting config parameter %s on Node %s with list selection %s", param, node_id, str(selection), @@ -709,7 +712,7 @@ async def async_setup_entry(hass, config_entry): return value.data = int(selection) _LOGGER.info( - "Setting config parameter %s on Node %s " "with selection %s", + "Setting config parameter %s on Node %s with selection %s", param, node_id, selection, @@ -717,7 +720,7 @@ async def async_setup_entry(hass, config_entry): return node.set_config_param(param, selection, size) _LOGGER.info( - "Setting unknown config parameter %s on Node %s " "with selection %s", + "Setting unknown config parameter %s on Node %s with selection %s", param, node_id, selection, @@ -828,7 +831,7 @@ async def async_setup_entry(hass, config_entry): ) return _LOGGER.info( - "Node %s on instance %s does not have resettable " "meters.", + "Node %s on instance %s does not have resettable meters.", node_id, instance, ) diff --git a/homeassistant/components/zwave/binary_sensor.py b/homeassistant/components/zwave/binary_sensor.py index a28f53f93d4..68df3313de3 100644 --- a/homeassistant/components/zwave/binary_sensor.py +++ b/homeassistant/components/zwave/binary_sensor.py @@ -1,12 +1,14 @@ """Support for Z-Wave binary sensors.""" -import logging import datetime -import homeassistant.util.dt as dt_util +import logging + +from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.event import track_point_in_time -from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice -from . import workaround, ZWaveDeviceEntity +import homeassistant.util.dt as dt_util + +from . import ZWaveDeviceEntity, workaround from .const import COMMAND_CLASS_SENSOR_BINARY _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zwave/climate.py b/homeassistant/components/zwave/climate.py index b40fff66958..2b421db70b5 100644 --- a/homeassistant/components/zwave/climate.py +++ b/homeassistant/components/zwave/climate.py @@ -1,9 +1,12 @@ """Support for Z-Wave climate devices.""" # Because we do not compile openzwave on CI import logging +from typing import Optional, Tuple from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, CURRENT_HVAC_COOL, CURRENT_HVAC_FAN, CURRENT_HVAC_HEAT, @@ -12,24 +15,26 @@ from homeassistant.components.climate.const import ( DOMAIN, HVAC_MODE_AUTO, HVAC_MODE_COOL, - HVAC_MODE_HEAT, - HVAC_MODE_HEAT_COOL, HVAC_MODE_DRY, HVAC_MODE_FAN_ONLY, + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, + PRESET_AWAY, PRESET_BOOST, PRESET_NONE, SUPPORT_AUX_HEAT, SUPPORT_FAN_MODE, + SUPPORT_PRESET_MODE, SUPPORT_SWING_MODE, SUPPORT_TARGET_TEMPERATURE, - SUPPORT_PRESET_MODE, + 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 . import ZWaveDeviceEntity +from . import ZWaveDeviceEntity, const _LOGGER = logging.getLogger(__name__) @@ -66,6 +71,33 @@ HVAC_STATE_MAPPINGS = { "auto changeover": HVAC_MODE_HEAT_COOL, } +MODE_SETPOINT_MAPPINGS = { + "off": (), + "heat": ("setpoint_heating",), + "cool": ("setpoint_cooling",), + "auto": ("setpoint_heating", "setpoint_cooling"), + "aux heat": ("setpoint_heating",), + "furnace": ("setpoint_furnace",), + "dry air": ("setpoint_dry_air",), + "moist air": ("setpoint_moist_air",), + "auto changeover": ("setpoint_auto_changeover",), + "heat econ": ("setpoint_eco_heating",), + "cool econ": ("setpoint_eco_cooling",), + "away": ("setpoint_away_heating", "setpoint_away_cooling"), + "full power": ("setpoint_full_power",), + # aliases found in xml configs + "comfort": ("setpoint_heating",), + "heat mode": ("setpoint_heating",), + "heat (default)": ("setpoint_heating",), + "dry floor": ("setpoint_dry_air",), + "heat eco": ("setpoint_eco_heating",), + "energy saving": ("setpoint_eco_heating",), + "energy heat": ("setpoint_eco_heating",), + "vacation": ("setpoint_away_heating", "setpoint_away_cooling"), + # for tests + "heat_cool": ("setpoint_heating", "setpoint_cooling"), +} + HVAC_CURRENT_MAPPINGS = { "idle": CURRENT_HVAC_IDLE, "heat": CURRENT_HVAC_HEAT, @@ -80,6 +112,7 @@ HVAC_CURRENT_MAPPINGS = { } PRESET_MAPPINGS = { + "away": PRESET_AWAY, "full power": PRESET_BOOST, "manufacturer specific": PRESET_MANUFACTURER_SPECIFIC, } @@ -114,16 +147,21 @@ async def async_setup_entry(hass, config_entry, async_add_entities): def get_device(hass, values, **kwargs): """Create Z-Wave entity device.""" temp_unit = hass.config.units.temperature_unit - return ZWaveClimate(values, temp_unit) + if values.primary.command_class == const.COMMAND_CLASS_THERMOSTAT_SETPOINT: + return ZWaveClimateSingleSetpoint(values, temp_unit) + if values.primary.command_class == const.COMMAND_CLASS_THERMOSTAT_MODE: + return ZWaveClimateMultipleSetpoint(values, temp_unit) + return None -class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): +class ZWaveClimateBase(ZWaveDeviceEntity, ClimateDevice): """Representation of a Z-Wave Climate device.""" def __init__(self, values, temp_unit): """Initialize the Z-Wave climate device.""" ZWaveDeviceEntity.__init__(self, values, DOMAIN) self._target_temperature = None + self._target_temperature_range = (None, None) self._current_temperature = None self._hvac_action = None self._hvac_list = None # [zwave_mode] @@ -154,10 +192,23 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): 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: + """Return a tuple of current setpoint Z-Wave value(s).""" + raise NotImplementedError() + @property def supported_features(self): """Return the list of supported features.""" support = SUPPORT_TARGET_TEMPERATURE + if self._hvac_list and HVAC_MODE_HEAT_COOL in self._hvac_list: + support |= SUPPORT_TARGET_TEMPERATURE_RANGE + if self._preset_list and PRESET_AWAY in self._preset_list: + support |= SUPPORT_TARGET_TEMPERATURE_RANGE + if self.values.fan_mode: support |= SUPPORT_FAN_MODE if self._zxt_120 == 1 and self.values.zxt_120_swing_mode: @@ -193,13 +244,13 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): def _update_operation_mode(self): """Update hvac and preset modes.""" - if self.values.mode: + if self._mode(): self._hvac_list = [] self._hvac_mapping = {} self._preset_list = [] self._preset_mapping = {} - mode_list = self.values.mode.data_items + mode_list = self._mode().data_items if mode_list: for mode in mode_list: ha_mode = HVAC_STATE_MAPPINGS.get(str(mode).lower()) @@ -227,7 +278,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): # Presets are supported self._preset_list.append(PRESET_NONE) - current_mode = self.values.mode.data + current_mode = self._mode().data _LOGGER.debug("current_mode=%s", current_mode) _hvac_temp = next( ( @@ -313,15 +364,21 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): def _update_target_temp(self): """Update target temperature.""" - if self.values.primary.data == 0: - _LOGGER.debug( - "Setpoint is 0, setting default to " "current_temperature=%s", - self._current_temperature, - ) - if self._current_temperature is not None: - self._target_temperature = round((float(self._current_temperature)), 1) - else: - self._target_temperature = round((float(self.values.primary.data)), 1) + current_setpoints = self._current_mode_setpoints() + self._target_temperature = None + self._target_temperature_range = (None, None) + if len(current_setpoints) == 1: + (setpoint,) = current_setpoints + if setpoint is not None: + self._target_temperature = round((float(setpoint.data)), 1) + elif len(current_setpoints) == 2: + (setpoint_low, setpoint_high) = current_setpoints + target_low, target_high = None, None + if setpoint_low is not None: + target_low = round((float(setpoint_low.data)), 1) + if setpoint_high is not None: + target_high = round((float(setpoint_high.data)), 1) + self._target_temperature_range = (target_low, target_high) def _update_operating_state(self): """Update operating state.""" @@ -374,7 +431,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): Need to be one of HVAC_MODE_*. """ - if self.values.mode: + if self._mode(): return self._hvac_mode return self._default_hvac_mode @@ -384,7 +441,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): Need to be a subset of HVAC_MODES. """ - if self.values.mode: + if self._mode(): return self._hvac_list return [] @@ -401,7 +458,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): """Return true if aux heater.""" if not self._aux_heat: return None - if self.values.mode.data == AUX_HEAT_ZWAVE_MODE: + if self._mode().data == AUX_HEAT_ZWAVE_MODE: return True return False @@ -411,7 +468,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): Need to be one of PRESET_*. """ - if self.values.mode: + if self._mode(): return self._preset_mode return PRESET_NONE @@ -421,7 +478,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): Need to be a subset of PRESET_MODES. """ - if self.values.mode: + if self._mode(): return self._preset_list return [] @@ -430,12 +487,35 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): """Return the temperature we try to reach.""" return self._target_temperature + @property + def target_temperature_low(self) -> Optional[float]: + """Return the lowbound target temperature we try to reach.""" + return self._target_temperature_range[0] + + @property + def target_temperature_high(self) -> Optional[float]: + """Return the highbound target temperature we try to reach.""" + return self._target_temperature_range[1] + def set_temperature(self, **kwargs): """Set new target temperature.""" - _LOGGER.debug("Set temperature to %s", kwargs.get(ATTR_TEMPERATURE)) - if kwargs.get(ATTR_TEMPERATURE) is None: - return - self.values.primary.data = kwargs.get(ATTR_TEMPERATURE) + current_setpoints = self._current_mode_setpoints() + if len(current_setpoints) == 1: + (setpoint,) = current_setpoints + target_temp = kwargs.get(ATTR_TEMPERATURE) + if setpoint is not None and target_temp is not None: + _LOGGER.debug("Set temperature to %s", target_temp) + setpoint.data = target_temp + elif len(current_setpoints) == 2: + (setpoint_low, setpoint_high) = current_setpoints + target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) + target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) + if setpoint_low is not None and target_temp_low is not None: + _LOGGER.debug("Set low temperature to %s", target_temp_low) + setpoint_low.data = target_temp_low + if setpoint_high is not None and target_temp_high is not None: + _LOGGER.debug("Set high temperature to %s", target_temp_high) + setpoint_high.data = target_temp_high def set_fan_mode(self, fan_mode): """Set new target fan mode.""" @@ -447,11 +527,11 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): def set_hvac_mode(self, hvac_mode): """Set new target hvac mode.""" _LOGGER.debug("Set hvac_mode to %s", hvac_mode) - if not self.values.mode: + if not self._mode(): return operation_mode = self._hvac_mapping.get(hvac_mode) _LOGGER.debug("Set operation_mode to %s", operation_mode) - self.values.mode.data = operation_mode + self._mode().data = operation_mode def turn_aux_heat_on(self): """Turn auxillary heater on.""" @@ -459,7 +539,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): return operation_mode = AUX_HEAT_ZWAVE_MODE _LOGGER.debug("Aux heat on. Set operation mode to %s", operation_mode) - self.values.mode.data = operation_mode + self._mode().data = operation_mode def turn_aux_heat_off(self): """Turn auxillary heater off.""" @@ -470,23 +550,23 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): else: operation_mode = self._hvac_mapping.get(HVAC_MODE_OFF) _LOGGER.debug("Aux heat off. Set operation mode to %s", operation_mode) - self.values.mode.data = operation_mode + self._mode().data = operation_mode def set_preset_mode(self, preset_mode): """Set new target preset mode.""" _LOGGER.debug("Set preset_mode to %s", preset_mode) - if not self.values.mode: + if not self._mode(): return if preset_mode == PRESET_NONE: # Activate the current hvac mode self._update_operation_mode() operation_mode = self._hvac_mapping.get(self.hvac_mode) _LOGGER.debug("Set operation_mode to %s", operation_mode) - self.values.mode.data = operation_mode + self._mode().data = operation_mode else: operation_mode = self._preset_mapping.get(preset_mode, preset_mode) _LOGGER.debug("Set operation_mode to %s", operation_mode) - self.values.mode.data = operation_mode + self._mode().data = operation_mode def set_swing_mode(self, swing_mode): """Set new target swing mode.""" @@ -502,3 +582,37 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): if self._fan_action: data[ATTR_FAN_ACTION] = self._fan_action return data + + +class ZWaveClimateSingleSetpoint(ZWaveClimateBase): + """Representation of a single setpoint Z-Wave thermostat device.""" + + def __init__(self, values, temp_unit): + """Initialize the Z-Wave climate device.""" + ZWaveClimateBase.__init__(self, values, temp_unit) + + def _mode(self) -> None: + """Return thermostat mode Z-Wave value.""" + return self.values.mode + + def _current_mode_setpoints(self) -> Tuple: + """Return a tuple of current setpoint Z-Wave value(s).""" + return (self.values.primary,) + + +class ZWaveClimateMultipleSetpoint(ZWaveClimateBase): + """Representation of a multiple setpoint Z-Wave thermostat device.""" + + def __init__(self, values, temp_unit): + """Initialize the Z-Wave climate device.""" + ZWaveClimateBase.__init__(self, values, temp_unit) + + def _mode(self) -> None: + """Return thermostat mode Z-Wave value.""" + return self.values.primary + + 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, ()) + return tuple(getattr(self.values, name, None) for name in setpoints_names) diff --git a/homeassistant/components/zwave/config_flow.py b/homeassistant/components/zwave/config_flow.py index f28502db57f..b570e31c128 100644 --- a/homeassistant/components/zwave/config_flow.py +++ b/homeassistant/components/zwave/config_flow.py @@ -7,8 +7,8 @@ import voluptuous as vol from homeassistant import config_entries from .const import ( - CONF_USB_STICK_PATH, CONF_NETWORK_KEY, + CONF_USB_STICK_PATH, DEFAULT_CONF_USB_STICK_PATH, DOMAIN, ) @@ -48,8 +48,7 @@ class ZwaveFlowHandler(config_entries.ConfigFlow): try: from functools import partial - # pylint: disable=unused-variable - option = await self.hass.async_add_executor_job( # noqa: F841 + option = await self.hass.async_add_executor_job( # noqa: F841 pylint: disable=unused-variable partial( ZWaveOption, user_input[CONF_USB_STICK_PATH], diff --git a/homeassistant/components/zwave/cover.py b/homeassistant/components/zwave/cover.py index 3a389fbf2bb..5b4fb0c9934 100644 --- a/homeassistant/components/zwave/cover.py +++ b/homeassistant/components/zwave/cover.py @@ -1,24 +1,29 @@ """Support for Z-Wave covers.""" import logging -from homeassistant.core import callback + from homeassistant.components.cover import ( - DOMAIN, - SUPPORT_OPEN, - SUPPORT_CLOSE, ATTR_POSITION, + ATTR_TILT_POSITION, + DOMAIN, + SUPPORT_CLOSE, + SUPPORT_OPEN, + CoverDevice, ) -from homeassistant.components.cover import CoverDevice +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect + from . import ( - ZWaveDeviceEntity, CONF_INVERT_OPENCLOSE_BUTTONS, CONF_INVERT_PERCENT, + CONF_TILT_OPEN_POSITION, + ZWaveDeviceEntity, workaround, ) from .const import ( - COMMAND_CLASS_SWITCH_MULTILEVEL, - COMMAND_CLASS_SWITCH_BINARY, COMMAND_CLASS_BARRIER_OPERATOR, + COMMAND_CLASS_MANUFACTURER_PROPRIETARY, + COMMAND_CLASS_SWITCH_BINARY, + COMMAND_CLASS_SWITCH_MULTILEVEL, DATA_NETWORK, ) @@ -27,6 +32,23 @@ _LOGGER = logging.getLogger(__name__) SUPPORT_GARAGE = SUPPORT_OPEN | SUPPORT_CLOSE +def _to_hex_str(id_in_bytes): + """Convert a two byte value to a hex string. + + Example: 0x1234 --> '0x1234' + """ + return "0x{:04x}".format(id_in_bytes) + + +# For some reason node.manufacturer_id is of type string. So we need to convert +# the values. +FIBARO = _to_hex_str(workaround.FIBARO) +FIBARO222_SHUTTERS = [ + _to_hex_str(workaround.FGR222_SHUTTER2), + _to_hex_str(workaround.FGRM222_SHUTTER2), +] + + async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Old method of setting up Z-Wave covers.""" pass @@ -51,6 +73,17 @@ def get_device(hass, values, node_config, **kwargs): values.primary.command_class == COMMAND_CLASS_SWITCH_MULTILEVEL and values.primary.index == 0 ): + if ( + values.primary.node.manufacturer_id == FIBARO + and values.primary.node.product_type in FIBARO222_SHUTTERS + ): + return FibaroFGRM222( + hass, + values, + invert_buttons, + invert_percent, + node_config.get(CONF_TILT_OPEN_POSITION), + ) return ZwaveRollershutter(hass, values, invert_buttons, invert_percent) if values.primary.command_class == COMMAND_CLASS_SWITCH_BINARY: return ZwaveGarageDoorSwitch(values) @@ -210,3 +243,116 @@ class ZwaveGarageDoorBarrier(ZwaveGarageDoorBase): def open_cover(self, **kwargs): """Open the garage door.""" self.values.primary.data = "Opened" + + +class FibaroFGRM222(ZwaveRollershutter): + """Implementation of proprietary features for Fibaro FGR-222 / FGRM-222. + + This adds support for the tilt feature for the ventian blind mode. + To enable this you need to configure the devices to use the venetian blind + mode and to enable the proprietary command class: + * Set "3: Reports type to Blind position reports sent" + to value "the main controller using Fibaro Command Class" + * Set "10: Roller Shutter operating modes" + to value "2 - Venetian Blind Mode, with positioning" + """ + + def __init__( + self, hass, values, invert_buttons, invert_percent, open_tilt_position: int + ): + """Initialize the FGRM-222.""" + self._value_blinds = None + self._value_tilt = None + self._has_tilt_mode = False # type: bool + self._open_tilt_position = 50 # type: int + if open_tilt_position is not None: + self._open_tilt_position = open_tilt_position + super().__init__(hass, values, invert_buttons, invert_percent) + + @property + def current_cover_tilt_position(self) -> int: + """Get the tilt of the blinds. + + Saturate values <5 and >94 so that it's easier to detect the end + positions in automations. + """ + if not self._has_tilt_mode: + return None + if self._value_tilt.data <= 5: + return 0 + if self._value_tilt.data >= 95: + return 100 + return self._value_tilt.data + + def set_cover_tilt_position(self, **kwargs): + """Move the cover tilt to a specific position.""" + if not self._has_tilt_mode: + _LOGGER.error("Can't set cover tilt as device is not yet set up.") + else: + # Limit the range to [0-99], as this what that the ZWave command + # accepts. + tilt_position = max(0, min(99, kwargs.get(ATTR_TILT_POSITION))) + _LOGGER.debug("setting tilt to %d", tilt_position) + self._value_tilt.data = tilt_position + + def open_cover_tilt(self, **kwargs): + """Set slats to horizontal position.""" + self.set_cover_tilt_position(tilt_position=self._open_tilt_position) + + def close_cover_tilt(self, **kwargs): + """Close the slats.""" + self.set_cover_tilt_position(tilt_position=0) + + def set_cover_position(self, **kwargs): + """Move the roller shutter to a specific position. + + If the venetian blinds mode is not activated, fall back to + the behavior of the parent class. + """ + if not self._has_tilt_mode: + super().set_cover_position(**kwargs) + else: + _LOGGER.debug("Setting cover position to %s", kwargs.get(ATTR_POSITION)) + self._value_blinds.data = kwargs.get(ATTR_POSITION) + + def _configure_values(self): + """Get the value objects from the node.""" + for value in self.node.get_values( + class_id=COMMAND_CLASS_MANUFACTURER_PROPRIETARY + ).values(): + if value is None: + continue + if value.index == 0: + self._value_blinds = value + elif value.index == 1: + self._value_tilt = value + else: + _LOGGER.warning( + "Undefined index %d for this command class", value.index + ) + + if self._value_tilt is not None: + # We reached here because the user has configured the Fibaro to + # report using the MANUFACTURER_PROPRIETARY command class. The only + # reason for the user to configure this way is if tilt support is + # needed (aka venetian blind mode). Therefore, turn it on. + # + # Note: This is safe to do even if the user has accidentally set + # this configuration parameter, or configuration parameter 10 to + # something other than venetian blind mode. The controller will just + # ignore potential tilt settings sent from home assistant in this + # case. + self._has_tilt_mode = True + _LOGGER.info( + "Zwave node %s is a Fibaro FGR-222/FGRM-222 with tilt support.", + self.node_id, + ) + + def update_properties(self): + """React on properties being updated.""" + if not self._has_tilt_mode: + self._configure_values() + if self._value_blinds is not None: + self._current_position = self._value_blinds.data + else: + super().update_properties() diff --git a/homeassistant/components/zwave/discovery_schemas.py b/homeassistant/components/zwave/discovery_schemas.py index e2254073290..5e4b83d81e1 100644 --- a/homeassistant/components/zwave/discovery_schemas.py +++ b/homeassistant/components/zwave/discovery_schemas.py @@ -48,11 +48,15 @@ DISCOVERY_SCHEMAS = [ ), }, { - const.DISC_COMPONENT: "climate", + const.DISC_COMPONENT: "climate", # thermostat without COMMAND_CLASS_THERMOSTAT_MODE const.DISC_GENERIC_DEVICE_CLASS: [ const.GENERIC_TYPE_THERMOSTAT, const.GENERIC_TYPE_SENSOR_MULTILEVEL, ], + const.DISC_SPECIFIC_DEVICE_CLASS: [ + const.SPECIFIC_TYPE_THERMOSTAT_HEATING, + const.SPECIFIC_TYPE_SETPOINT_THERMOSTAT, + ], const.DISC_VALUES: dict( DEFAULT_VALUES_SCHEMA, **{ @@ -64,10 +68,106 @@ DISCOVERY_SCHEMAS = [ const.DISC_INDEX: [const.INDEX_SENSOR_MULTILEVEL_TEMPERATURE], const.DISC_OPTIONAL: True, }, + "fan_mode": { + const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_FAN_MODE], + const.DISC_OPTIONAL: True, + }, + "operating_state": { + const.DISC_COMMAND_CLASS: [ + const.COMMAND_CLASS_THERMOSTAT_OPERATING_STATE + ], + const.DISC_OPTIONAL: True, + }, + "fan_action": { + const.DISC_COMMAND_CLASS: [ + const.COMMAND_CLASS_THERMOSTAT_FAN_ACTION + ], + const.DISC_OPTIONAL: True, + }, "mode": { const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_MODE], const.DISC_OPTIONAL: True, }, + }, + ), + }, + { + const.DISC_COMPONENT: "climate", # thermostat with COMMAND_CLASS_THERMOSTAT_MODE + const.DISC_GENERIC_DEVICE_CLASS: [ + const.GENERIC_TYPE_THERMOSTAT, + const.GENERIC_TYPE_SENSOR_MULTILEVEL, + ], + const.DISC_SPECIFIC_DEVICE_CLASS: [ + const.SPECIFIC_TYPE_THERMOSTAT_GENERAL, + const.SPECIFIC_TYPE_THERMOSTAT_GENERAL_V2, + const.SPECIFIC_TYPE_SETBACK_THERMOSTAT, + ], + const.DISC_VALUES: dict( + DEFAULT_VALUES_SCHEMA, + **{ + const.DISC_PRIMARY: { + const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_MODE] + }, + "setpoint_heating": { + const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT], + const.DISC_INDEX: [1], + const.DISC_OPTIONAL: True, + }, + "setpoint_cooling": { + const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT], + const.DISC_INDEX: [2], + const.DISC_OPTIONAL: True, + }, + "setpoint_furnace": { + const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT], + const.DISC_INDEX: [7], + const.DISC_OPTIONAL: True, + }, + "setpoint_dry_air": { + const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT], + const.DISC_INDEX: [8], + const.DISC_OPTIONAL: True, + }, + "setpoint_moist_air": { + const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT], + const.DISC_INDEX: [9], + const.DISC_OPTIONAL: True, + }, + "setpoint_auto_changeover": { + const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT], + const.DISC_INDEX: [10], + const.DISC_OPTIONAL: True, + }, + "setpoint_eco_heating": { + const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT], + const.DISC_INDEX: [11], + const.DISC_OPTIONAL: True, + }, + "setpoint_eco_cooling": { + const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT], + const.DISC_INDEX: [12], + const.DISC_OPTIONAL: True, + }, + "setpoint_away_heating": { + const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT], + const.DISC_INDEX: [13], + const.DISC_OPTIONAL: True, + }, + "setpoint_away_cooling": { + const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT], + const.DISC_INDEX: [14], + const.DISC_OPTIONAL: True, + }, + "setpoint_full_power": { + const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT], + const.DISC_INDEX: [15], + const.DISC_OPTIONAL: True, + }, + "temperature": { + const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_SENSOR_MULTILEVEL], + const.DISC_INDEX: [const.INDEX_SENSOR_MULTILEVEL_TEMPERATURE], + const.DISC_OPTIONAL: True, + }, "fan_mode": { const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_FAN_MODE], const.DISC_OPTIONAL: True, diff --git a/homeassistant/components/zwave/fan.py b/homeassistant/components/zwave/fan.py index a52a7613d72..b77ab8dcf68 100644 --- a/homeassistant/components/zwave/fan.py +++ b/homeassistant/components/zwave/fan.py @@ -2,17 +2,18 @@ import logging import math -from homeassistant.core import callback from homeassistant.components.fan import ( DOMAIN, - FanEntity, - SPEED_OFF, + SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, - SPEED_HIGH, + SPEED_OFF, SUPPORT_SET_SPEED, + FanEntity, ) +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect + from . import ZWaveDeviceEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zwave/light.py b/homeassistant/components/zwave/light.py index bdb9021c02c..e941b2a97dc 100644 --- a/homeassistant/components/zwave/light.py +++ b/homeassistant/components/zwave/light.py @@ -1,26 +1,27 @@ """Support for Z-Wave lights.""" import logging - from threading import Timer -from homeassistant.core import callback + from homeassistant.components.light import ( - ATTR_WHITE_VALUE, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION, + ATTR_WHITE_VALUE, + DOMAIN, SUPPORT_BRIGHTNESS, - SUPPORT_COLOR_TEMP, SUPPORT_COLOR, + SUPPORT_COLOR_TEMP, SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE, - DOMAIN, Light, ) from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.util.color as color_util -from . import CONF_REFRESH_VALUE, CONF_REFRESH_DELAY, const, ZWaveDeviceEntity + +from . import CONF_REFRESH_DELAY, CONF_REFRESH_VALUE, ZWaveDeviceEntity, const _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zwave/lock.py b/homeassistant/components/zwave/lock.py index ae5640e812d..f84b1b5cfd4 100644 --- a/homeassistant/components/zwave/lock.py +++ b/homeassistant/components/zwave/lock.py @@ -3,10 +3,11 @@ import logging import voluptuous as vol -from homeassistant.core import callback from homeassistant.components.lock import DOMAIN, LockDevice -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect + from . import ZWaveDeviceEntity, const _LOGGER = logging.getLogger(__name__) @@ -269,7 +270,7 @@ class ZwaveLock(ZWaveDeviceEntity, LockDevice): workaround = DEVICE_MAPPINGS[specific_sensor_key] if workaround & WORKAROUND_V2BTZE: self._v2btze = 1 - _LOGGER.debug("Polycontrol Danalock v2 BTZE " "workaround enabled") + _LOGGER.debug("Polycontrol Danalock v2 BTZE workaround enabled") if workaround & WORKAROUND_DEVICE_STATE: self._state_workaround = True _LOGGER.debug("Notification device state workaround enabled") @@ -298,7 +299,7 @@ class ZwaveLock(ZWaveDeviceEntity, LockDevice): ): self._state = LOCK_STATUS.get(str(notification_data)) _LOGGER.debug( - "Lock state set from Access Control value and is %s, " "get=%s", + "Lock state set from Access Control value and is %s, get=%s", str(notification_data), self.state, ) diff --git a/homeassistant/components/zwave/manifest.json b/homeassistant/components/zwave/manifest.json index 9268a50a14d..c781a493b55 100644 --- a/homeassistant/components/zwave/manifest.json +++ b/homeassistant/components/zwave/manifest.json @@ -3,12 +3,7 @@ "name": "Z-Wave", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave", - "requirements": [ - "homeassistant-pyozw==0.1.4", - "pydispatcher==2.0.5" - ], + "requirements": ["homeassistant-pyozw==0.1.7", "pydispatcher==2.0.5"], "dependencies": [], - "codeowners": [ - "@home-assistant/z-wave" - ] + "codeowners": ["@home-assistant/z-wave"] } diff --git a/homeassistant/components/zwave/node_entity.py b/homeassistant/components/zwave/node_entity.py index 44241e91daf..3b94991312a 100644 --- a/homeassistant/components/zwave/node_entity.py +++ b/homeassistant/components/zwave/node_entity.py @@ -1,26 +1,26 @@ """Entity class that represents Z-Wave node.""" -import logging from itertools import count +import logging +from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_ENTITY_ID, ATTR_WAKEUP from homeassistant.core import callback -from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_WAKEUP, ATTR_ENTITY_ID -from homeassistant.helpers.entity_registry import async_get_registry from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_registry import async_get_registry from .const import ( - ATTR_NODE_ID, - COMMAND_CLASS_WAKE_UP, - ATTR_SCENE_ID, - ATTR_SCENE_DATA, ATTR_BASIC_LEVEL, - EVENT_NODE_EVENT, - EVENT_SCENE_ACTIVATED, + ATTR_NODE_ID, + ATTR_SCENE_DATA, + ATTR_SCENE_ID, COMMAND_CLASS_CENTRAL_SCENE, COMMAND_CLASS_VERSION, + COMMAND_CLASS_WAKE_UP, DOMAIN, + EVENT_NODE_EVENT, + EVENT_SCENE_ACTIVATED, ) -from .util import node_name, is_node_parsed, node_device_id_and_name +from .util import is_node_parsed, node_device_id_and_name, node_name _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zwave/sensor.py b/homeassistant/components/zwave/sensor.py index 0820feb8d0f..08ee54415ad 100644 --- a/homeassistant/components/zwave/sensor.py +++ b/homeassistant/components/zwave/sensor.py @@ -1,10 +1,12 @@ """Support for Z-Wave sensors.""" import logging -from homeassistant.core import callback -from homeassistant.components.sensor import DOMAIN, DEVICE_CLASS_BATTERY + +from homeassistant.components.sensor import DEVICE_CLASS_BATTERY, DOMAIN from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from . import const, ZWaveDeviceEntity + +from . import ZWaveDeviceEntity, const _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zwave/switch.py b/homeassistant/components/zwave/switch.py index 4b13b06d2b9..3592f534074 100644 --- a/homeassistant/components/zwave/switch.py +++ b/homeassistant/components/zwave/switch.py @@ -1,9 +1,11 @@ """Support for Z-Wave switches.""" import logging import time -from homeassistant.core import callback + from homeassistant.components.switch import DOMAIN, SwitchDevice +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect + from . import ZWaveDeviceEntity, workaround _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/config.py b/homeassistant/config.py index 864ced6a16a..ee3ccc15f81 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -1,66 +1,65 @@ """Module to help with parsing and generating configuration files.""" -from collections import OrderedDict - # pylint: disable=no-name-in-module +from collections import OrderedDict from distutils.version import LooseVersion # pylint: disable=import-error import logging import os import re import shutil -from typing import Any, Tuple, Optional, Dict, Union, Callable, Sequence, Set from types import ModuleType +from typing import Any, Callable, Dict, Optional, Sequence, Set, Tuple, Union + import voluptuous as vol from voluptuous.humanize import humanize_error from homeassistant import auth from homeassistant.auth import ( - providers as auth_providers, mfa_modules as auth_mfa_modules, + providers as auth_providers, ) from homeassistant.const import ( + ATTR_ASSUMED_STATE, ATTR_FRIENDLY_NAME, ATTR_HIDDEN, - ATTR_ASSUMED_STATE, + CONF_AUTH_MFA_MODULES, + CONF_AUTH_PROVIDERS, + CONF_CUSTOMIZE, + CONF_CUSTOMIZE_DOMAIN, + CONF_CUSTOMIZE_GLOB, + CONF_ELEVATION, + CONF_ID, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, CONF_PACKAGES, - CONF_UNIT_SYSTEM, - CONF_TIME_ZONE, - CONF_ELEVATION, - CONF_UNIT_SYSTEM_IMPERIAL, CONF_TEMPERATURE_UNIT, + CONF_TIME_ZONE, + CONF_TYPE, + CONF_UNIT_SYSTEM, + CONF_UNIT_SYSTEM_IMPERIAL, + CONF_WHITELIST_EXTERNAL_DIRS, TEMP_CELSIUS, __version__, - CONF_CUSTOMIZE, - CONF_CUSTOMIZE_DOMAIN, - CONF_CUSTOMIZE_GLOB, - CONF_WHITELIST_EXTERNAL_DIRS, - CONF_AUTH_PROVIDERS, - CONF_AUTH_MFA_MODULES, - CONF_TYPE, - CONF_ID, ) from homeassistant.core import DOMAIN as CONF_CORE, SOURCE_YAML, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError +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.loader import Integration, IntegrationNotFound from homeassistant.requirements import ( - async_get_integration_with_requirements, RequirementsNotFound, + async_get_integration_with_requirements, ) -from homeassistant.util.yaml import load_yaml, SECRET_YAML from homeassistant.util.package import is_docker_env -import homeassistant.helpers.config_validation as cv from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM -from homeassistant.helpers.entity_values import EntityValues -from homeassistant.helpers import config_per_platform, extract_domain_configs +from homeassistant.util.yaml import SECRET_YAML, load_yaml _LOGGER = logging.getLogger(__name__) DATA_PERSISTENT_ERRORS = "bootstrap_persistent_errors" RE_YAML_ERROR = re.compile(r"homeassistant\.util\.yaml") RE_ASCII = re.compile(r"\033\[[^m]*m") -HA_COMPONENT_URL = "[{}](https://home-assistant.io/integrations/{}/)" YAML_CONFIG_FILE = "configuration.yaml" VERSION_FILE = ".HA_VERSION" CONFIG_DIR_NAME = ".homeassistant" @@ -210,7 +209,7 @@ CORE_CONFIG_SCHEMA = CUSTOMIZE_CONFIG_SCHEMA.extend( { CONF_TYPE: vol.NotIn( ["insecure_example"], - "The insecure_example mfa module" " is for testing only.", + "The insecure_example mfa module is for testing only.", ) } ) @@ -412,19 +411,25 @@ def process_ha_config_upgrade(hass: HomeAssistant) -> None: @callback def async_log_exception( - ex: Exception, domain: str, config: Dict, hass: HomeAssistant + ex: Exception, + domain: str, + config: Dict, + hass: HomeAssistant, + link: Optional[str] = None, ) -> None: """Log an error for configuration validation. This method must be run in the event loop. """ if hass is not None: - async_notify_setup_error(hass, domain, True) - _LOGGER.error(_format_config_error(ex, domain, config)) + async_notify_setup_error(hass, domain, link) + _LOGGER.error(_format_config_error(ex, domain, config, link)) @callback -def _format_config_error(ex: Exception, domain: str, config: Dict) -> str: +def _format_config_error( + ex: Exception, domain: str, config: Dict, link: Optional[str] = None +) -> str: """Generate log exception for configuration validation. This method must be run in the event loop. @@ -455,11 +460,8 @@ def _format_config_error(ex: Exception, domain: str, config: Dict) -> str: getattr(domain_config, "__line__", "?"), ) - if domain != CONF_CORE: - message += ( - "Please check the docs at " - "https://home-assistant.io/integrations/{}/".format(domain) - ) + if domain != CONF_CORE and link: + message += f"Please check the docs at {link}" return message @@ -626,7 +628,6 @@ async def merge_packages_config( _log_pkg_error: Callable = _log_pkg_error, ) -> Dict: """Merge packages into the top-level configuration. Mutate config.""" - # pylint: disable=too-many-nested-blocks PACKAGES_CONFIG_SCHEMA(packages) for pack_name, pack_conf in packages.items(): for comp_name, comp_conf in pack_conf.items(): @@ -717,7 +718,7 @@ async def async_process_component_config( hass, config ) except (vol.Invalid, HomeAssistantError) as ex: - async_log_exception(ex, domain, config, hass) + async_log_exception(ex, domain, config, hass, integration.documentation) return None # No custom config validator, proceed with schema validation @@ -725,7 +726,7 @@ async def async_process_component_config( try: return component.CONFIG_SCHEMA(config) # type: ignore except vol.Invalid as ex: - async_log_exception(ex, domain, config, hass) + async_log_exception(ex, domain, config, hass, integration.documentation) return None component_platform_schema = getattr( @@ -741,7 +742,7 @@ async def async_process_component_config( try: p_validated = component_platform_schema(p_config) except vol.Invalid as ex: - async_log_exception(ex, domain, p_config, hass) + async_log_exception(ex, domain, p_config, hass, integration.documentation) continue # Not all platform components follow same pattern for platforms @@ -765,13 +766,18 @@ async def async_process_component_config( # Validate platform specific schema if hasattr(platform, "PLATFORM_SCHEMA"): - # pylint: disable=no-member try: p_validated = platform.PLATFORM_SCHEMA( # type: ignore p_config ) except vol.Invalid as ex: - async_log_exception(ex, f"{domain}.{p_name}", p_config, hass) + async_log_exception( + ex, + f"{domain}.{p_name}", + p_config, + hass, + p_integration.documentation, + ) continue platforms.append(p_validated) @@ -807,7 +813,7 @@ async def async_check_ha_config_file(hass: HomeAssistant) -> Optional[str]: @callback def async_notify_setup_error( - hass: HomeAssistant, component: str, display_link: bool = False + hass: HomeAssistant, component: str, display_link: Optional[str] = None ) -> None: """Print a persistent notification. @@ -822,11 +828,11 @@ def async_notify_setup_error( errors[component] = errors.get(component) or display_link - message = "The following components and platforms could not be set up:\n\n" + message = "The following integrations and platforms could not be set up:\n\n" for name, link in errors.items(): if link: - part = HA_COMPONENT_URL.format(name.replace("_", "-"), name) + part = f"[{name}]({link})" else: part = name diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index ae3aeebb1ee..942998767a1 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -1,21 +1,20 @@ """Manage config entries in Home Assistant.""" import asyncio -import logging import functools +import logging +from typing import Any, Callable, Dict, List, Optional, Set, Union, cast import uuid -from typing import Any, Callable, Dict, List, Optional, Set, cast import weakref import attr from homeassistant import data_entry_flow, loader -from homeassistant.core import callback, HomeAssistant -from homeassistant.exceptions import HomeAssistantError, ConfigEntryNotReady -from homeassistant.setup import async_setup_component, async_process_deps_reqs -from homeassistant.util.decorator import Registry +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError from homeassistant.helpers import entity_registry from homeassistant.helpers.event import Event - +from homeassistant.setup import async_process_deps_reqs, async_setup_component +from homeassistant.util.decorator import Registry _LOGGER = logging.getLogger(__name__) _UNDEF: dict = {} @@ -26,6 +25,16 @@ SOURCE_SSDP = "ssdp" SOURCE_USER = "user" SOURCE_ZEROCONF = "zeroconf" +# If a user wants to hide a discovery from the UI they can "Ignore" it. The config_entries/ignore_flow +# websocket command creates a config entry with this source and while it exists normal discoveries +# with the same unique id are ignored. +SOURCE_IGNORE = "ignore" + +# This is used when a user uses the "Stop Ignoring" button in the UI (the +# config_entries/ignore_flow websocket command). It's triggered after the "ignore" config entry has +# been removed and unloaded. +SOURCE_UNIGNORE = "unignore" + HANDLERS = Registry() STORAGE_KEY = "core.config_entries" @@ -86,6 +95,7 @@ class ConfigEntry: "title", "data", "options", + "unique_id", "system_options", "source", "connection_class", @@ -105,6 +115,7 @@ class ConfigEntry: connection_class: str, system_options: dict, options: Optional[dict] = None, + unique_id: Optional[str] = None, entry_id: Optional[str] = None, state: str = ENTRY_STATE_NOT_LOADED, ) -> None: @@ -139,6 +150,9 @@ class ConfigEntry: # State of the entry (LOADED, NOT_LOADED) self.state = state + # Unique ID of this entry. + self.unique_id = unique_id + # Listeners to call on update self.update_listeners: List = [] @@ -153,6 +167,9 @@ class ConfigEntry: tries: int = 0, ) -> None: """Set up an entry.""" + if self.source == SOURCE_IGNORE: + return + if integration is None: integration = await loader.async_get_integration(hass, self.domain) @@ -238,6 +255,10 @@ class ConfigEntry: Returns if unload is possible and was successful. """ + if self.source == SOURCE_IGNORE: + self.state = ENTRY_STATE_NOT_LOADED + return True + if integration is None: integration = await loader.async_get_integration(hass, self.domain) @@ -284,6 +305,9 @@ class ConfigEntry: async def async_remove(self, hass: HomeAssistant) -> None: """Invoke remove callback on component.""" + if self.source == SOURCE_IGNORE: + return + integration = await loader.async_get_integration(hass, self.domain) component = integration.get_component() if not hasattr(component, "async_remove_entry"): @@ -371,6 +395,7 @@ class ConfigEntry: "system_options": self.system_options.as_dict(), "source": self.source, "connection_class": self.connection_class, + "unique_id": self.unique_id, } @@ -445,6 +470,19 @@ class ConfigEntries: dev_reg.async_clear_config_entry(entry_id) ent_reg.async_clear_config_entry(entry_id) + # After we have fully removed an "ignore" config entry we can try and rediscover it so that a + # user is able to immediately start configuring it. We do this by starting a new flow with + # the 'unignore' step. If the integration doesn't implement async_step_unignore then + # this will be a no-op. + if entry.source == SOURCE_IGNORE: + self.hass.async_create_task( + self.hass.config_entries.flow.async_init( + entry.domain, + context={"source": SOURCE_UNIGNORE}, + data={"unique_id": entry.unique_id}, + ) + ) + return {"require_restart": not unload_success} async def async_initialize(self) -> None: @@ -474,6 +512,8 @@ class ConfigEntries: options=entry.get("options"), # New in 0.98 system_options=entry.get("system_options", {}), + # New in 0.104 + unique_id=entry.get("unique_id"), ) for entry in config["entries"] ] @@ -534,11 +574,15 @@ class ConfigEntries: self, entry: ConfigEntry, *, + unique_id: Union[str, dict, None] = _UNDEF, data: dict = _UNDEF, options: dict = _UNDEF, system_options: dict = _UNDEF, ) -> None: """Update a config entry.""" + if unique_id is not _UNDEF: + entry.unique_id = cast(Optional[str], unique_id) + if data is not _UNDEF: entry.data = data @@ -603,6 +647,34 @@ class ConfigEntries: if result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: return result + # Check if config entry exists with unique ID. Unload it. + existing_entry = None + + if flow.unique_id is not None: + # Abort all flows in progress with same unique ID. + for progress_flow in self.flow.async_progress(): + if ( + progress_flow["handler"] == flow.handler + and progress_flow["flow_id"] != flow.flow_id + and progress_flow["context"].get("unique_id") == flow.unique_id + ): + self.flow.async_abort(progress_flow["flow_id"]) + + # Find existing entry. + for check_entry in self.async_entries(result["handler"]): + if check_entry.unique_id == flow.unique_id: + existing_entry = check_entry + break + + # Unload the entry before setting up the new one. + # We will remove it only after the other one is set up, + # so that device customizations are not getting lost. + if ( + existing_entry is not None + and existing_entry.state not in UNRECOVERABLE_STATES + ): + await self.async_unload(existing_entry.entry_id) + entry = ConfigEntry( version=result["version"], domain=result["handler"], @@ -612,12 +684,17 @@ class ConfigEntries: system_options={}, source=flow.context["source"], connection_class=flow.CONNECTION_CLASS, + unique_id=flow.unique_id, ) self._entries.append(entry) - self._async_schedule_save() await self.async_setup(entry.entry_id) + if existing_entry is not None: + await self.async_remove(existing_entry.entry_id) + + self._async_schedule_save() + result["result"] = entry return result @@ -696,18 +773,67 @@ class ConfigFlow(data_entry_flow.FlowHandler): CONNECTION_CLASS = CONN_CLASS_UNKNOWN + @property + def unique_id(self) -> Optional[str]: + """Return unique ID if available.""" + # pylint: disable=no-member + if not self.context: + return None + + return cast(Optional[str], self.context.get("unique_id")) + @staticmethod @callback 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) -> None: + """Abort if the unique ID is already configured.""" + if self.unique_id is None: + return + + if self.unique_id in self._async_current_ids(): + raise data_entry_flow.AbortFlow("already_configured") + + async def async_set_unique_id( + self, unique_id: str, *, raise_on_progress: bool = True + ) -> Optional[ConfigEntry]: + """Set a unique ID for the config flow. + + Returns optionally existing config entry with same ID. + """ + if raise_on_progress: + for progress in self._async_in_progress(): + if progress["context"].get("unique_id") == unique_id: + raise data_entry_flow.AbortFlow("already_in_progress") + + # pylint: disable=no-member + self.context["unique_id"] = unique_id + + for entry in self._async_current_entries(): + 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) + @callback + def _async_current_ids(self, include_ignore: bool = True) -> Set[Optional[str]]: + """Return current unique IDs.""" + assert self.hass is not None + return set( + entry.unique_id + for entry in self.hass.config_entries.async_entries(self.handler) + if include_ignore or entry.source != SOURCE_IGNORE + ) + @callback def _async_in_progress(self) -> List[Dict]: """Return other in progress flows for current domain.""" @@ -718,6 +844,15 @@ class ConfigFlow(data_entry_flow.FlowHandler): 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]: + """Ignore this config flow.""" + await self.async_set_unique_id(user_input["unique_id"], raise_on_progress=False) + return self.async_create_entry(title="Ignored", data={}) + + 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") + class OptionsFlowManager: """Flow to set options for a configuration entry.""" diff --git a/homeassistant/const.py b/homeassistant/const.py index c15d1e55bd1..15dc5a099bc 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,10 +1,13 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -MINOR_VERSION = 102 +MINOR_VERSION = 104 PATCH_VERSION = "0.dev0" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) -REQUIRED_PYTHON_VER = (3, 6, 1) +REQUIRED_PYTHON_VER = (3, 7, 0) +# Truthy date string triggers showing related deprecation warning messages. +REQUIRED_NEXT_PYTHON_VER = (3, 8, 0) +REQUIRED_NEXT_PYTHON_DATE = "" # Format for platform files PLATFORM_FORMAT = "{platform}.{domain}" diff --git a/homeassistant/core.py b/homeassistant/core.py index f5e91769107..e76673f5727 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -14,68 +14,61 @@ import os import pathlib import threading from time import monotonic -import uuid - from types import MappingProxyType from typing import ( - Optional, - Any, - Callable, - List, - TypeVar, - Dict, - Coroutine, - Set, TYPE_CHECKING, + Any, Awaitable, + Callable, + Coroutine, + Dict, + List, Mapping, + Optional, + Set, + TypeVar, ) +import uuid from async_timeout import timeout import attr import voluptuous as vol +from homeassistant import loader, util from homeassistant.const import ( ATTR_DOMAIN, ATTR_FRIENDLY_NAME, ATTR_NOW, + ATTR_SECONDS, ATTR_SERVICE, ATTR_SERVICE_DATA, - ATTR_SECONDS, CONF_UNIT_SYSTEM_IMPERIAL, EVENT_CALL_SERVICE, EVENT_CORE_CONFIG_UPDATE, + EVENT_HOMEASSISTANT_CLOSE, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, - EVENT_HOMEASSISTANT_CLOSE, - EVENT_SERVICE_REMOVED, EVENT_SERVICE_REGISTERED, + EVENT_SERVICE_REMOVED, EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, EVENT_TIMER_OUT_OF_SYNC, MATCH_ALL, __version__, ) -from homeassistant import loader from homeassistant.exceptions import ( HomeAssistantError, InvalidEntityFormatError, InvalidStateError, - Unauthorized, ServiceNotFound, + Unauthorized, ) -from homeassistant.util.async_ import run_callback_threadsafe, fire_coroutine_threadsafe -from homeassistant import util -import homeassistant.util.dt as dt_util from homeassistant.util import location, slugify -from homeassistant.util.unit_system import ( - UnitSystem, - IMPERIAL_SYSTEM, - METRIC_SYSTEM, -) +from homeassistant.util.async_ import fire_coroutine_threadsafe, run_callback_threadsafe +import homeassistant.util.dt as dt_util +from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM, UnitSystem # Typing imports that create a circular dependency -# pylint: disable=using-constant-test if TYPE_CHECKING: from homeassistant.config_entries import ConfigEntries from homeassistant.components.http import HomeAssistantHTTP @@ -1141,6 +1134,9 @@ class ServiceRegistry: self._services[domain].pop(service) + if not self._services[domain]: + self._services.pop(domain) + self._hass.bus.async_fire( EVENT_SERVICE_REMOVED, {ATTR_DOMAIN: domain, ATTR_SERVICE: service} ) diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 58d8e4ea131..7c2b4ab6ddc 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -1,9 +1,11 @@ """Classes to help gather user submissions.""" import logging -from typing import Dict, Any, Callable, List, Optional +from typing import Any, Callable, Dict, List, Optional, cast import uuid + import voluptuous as vol -from .core import callback, HomeAssistant + +from .core import HomeAssistant, callback from .exceptions import HomeAssistantError _LOGGER = logging.getLogger(__name__) @@ -34,6 +36,16 @@ class UnknownStep(FlowError): """Unknown step specified.""" +class AbortFlow(FlowError): + """Exception to indicate a flow needs to be aborted.""" + + def __init__(self, reason: str, description_placeholders: Optional[Dict] = None): + """Initialize an abort flow exception.""" + super().__init__(f"Flow aborted: {reason}") + self.reason = reason + self.description_placeholders = description_placeholders + + class FlowManager: """Manage all the flows that are in progress.""" @@ -129,7 +141,12 @@ class FlowManager: ) ) - result: Dict = await getattr(flow, method)(user_input) + try: + 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 + ) if result["type"] not in ( RESULT_TYPE_FORM, @@ -226,13 +243,9 @@ class FlowHandler: self, *, reason: str, description_placeholders: Optional[Dict] = None ) -> Dict[str, Any]: """Abort the config flow.""" - return { - "type": RESULT_TYPE_ABORT, - "flow_id": self.flow_id, - "handler": self.handler, - "reason": reason, - "description_placeholders": description_placeholders, - } + return _create_abort_data( + self.flow_id, cast(str, self.handler), reason, description_placeholders + ) @callback def async_external_step( @@ -257,3 +270,20 @@ class FlowHandler: "handler": self.handler, "step_id": next_step_id, } + + +@callback +def _create_abort_data( + flow_id: str, + handler: str, + reason: str, + description_placeholders: Optional[Dict] = None, +) -> Dict[str, Any]: + """Return the definition of an external step for the user to take.""" + return { + "type": RESULT_TYPE_ABORT, + "flow_id": flow_id, + "handler": handler, + "reason": reason, + "description_placeholders": description_placeholders, + } diff --git a/homeassistant/exceptions.py b/homeassistant/exceptions.py index 0000a4b176e..745d80d386b 100644 --- a/homeassistant/exceptions.py +++ b/homeassistant/exceptions.py @@ -1,11 +1,10 @@ """The exceptions used by Home Assistant.""" -from typing import Optional, Tuple, TYPE_CHECKING +from typing import TYPE_CHECKING, Optional, Tuple + import jinja2 -# pylint: disable=using-constant-test if TYPE_CHECKING: - # pylint: disable=unused-import - from .core import Context # noqa: F401 + from .core import Context # noqa: F401 pylint: disable=unused-import class HomeAssistantError(Exception): diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 7e8bb478d8e..93d50257525 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -20,10 +20,13 @@ FLOWS = [ "deconz", "dialogflow", "ecobee", + "elgato", "emulated_roku", "esphome", "geofency", "geonetnz_quakes", + "geonetnz_volcano", + "gios", "glances", "gpslogger", "hangouts", @@ -34,6 +37,7 @@ FLOWS = [ "huawei_lte", "hue", "iaqualink", + "icloud", "ifttt", "ios", "ipma", @@ -42,6 +46,7 @@ FLOWS = [ "life360", "lifx", "linky", + "local_ip", "locative", "logi_circle", "luftdaten", @@ -69,7 +74,9 @@ FLOWS = [ "soma", "somfy", "sonos", + "starline", "tellduslive", + "tesla", "toon", "tplink", "traccar", diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 108fe38e647..306b3850a1b 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -12,6 +12,9 @@ ZEROCONF = { "_coap._udp.local.": [ "tradfri" ], + "_elg._tcp.local.": [ + "elgato" + ], "_esphomelib._tcp.local.": [ "esphome" ], diff --git a/homeassistant/helpers/__init__.py b/homeassistant/helpers/__init__.py index 4c1a9803d75..125d90e1162 100644 --- a/homeassistant/helpers/__init__.py +++ b/homeassistant/helpers/__init__.py @@ -1,10 +1,9 @@ """Helper methods for components within Home Assistant.""" import re -from typing import Any, Iterable, Tuple, Sequence, Dict +from typing import Any, Dict, Iterable, Sequence, Tuple from homeassistant.const import CONF_PLATFORM -# pylint: disable=invalid-name ConfigType = Dict[str, Any] diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index 7f1579cd2c6..eee891b7f88 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -1,18 +1,17 @@ """Helper for aiohttp webclient stuff.""" import asyncio -import sys from ssl import SSLContext -from typing import Any, Awaitable, Optional, cast -from typing import Union +import sys +from typing import Any, Awaitable, Optional, Union, cast import aiohttp -from aiohttp.hdrs import USER_AGENT, CONTENT_TYPE from aiohttp import web -from aiohttp.web_exceptions import HTTPGatewayTimeout, HTTPBadGateway +from aiohttp.hdrs import CONTENT_TYPE, USER_AGENT +from aiohttp.web_exceptions import HTTPBadGateway, HTTPGatewayTimeout import async_timeout -from homeassistant.core import callback, Event from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE, __version__ +from homeassistant.core import Event, callback from homeassistant.helpers.typing import HomeAssistantType from homeassistant.loader import bind_hass from homeassistant.util import ssl as ssl_util diff --git a/homeassistant/helpers/area_registry.py b/homeassistant/helpers/area_registry.py index e75b195d386..58abecffb8b 100644 --- a/homeassistant/helpers/area_registry.py +++ b/homeassistant/helpers/area_registry.py @@ -1,10 +1,9 @@ """Provide a way to connect devices to one physical location.""" -import logging -import uuid from asyncio import Event from collections import OrderedDict -from typing import MutableMapping -from typing import Iterable, Optional, cast +import logging +from typing import Iterable, MutableMapping, Optional, cast +import uuid import attr diff --git a/homeassistant/helpers/check_config.py b/homeassistant/helpers/check_config.py index 4052a94b9de..1b1e136ed89 100644 --- a/homeassistant/helpers/check_config.py +++ b/homeassistant/helpers/check_config.py @@ -1,35 +1,38 @@ """Helper to check the configuration file.""" -from collections import OrderedDict, namedtuple -from typing import List +from collections import OrderedDict +from typing import List, NamedTuple, Optional import attr import voluptuous as vol from homeassistant import loader -from homeassistant.core import HomeAssistant from homeassistant.config import ( CONF_CORE, - CORE_CONFIG_SCHEMA, CONF_PACKAGES, - merge_packages_config, + CORE_CONFIG_SCHEMA, _format_config_error, + config_per_platform, + extract_domain_configs, find_config_file, load_yaml_config_file, - extract_domain_configs, - config_per_platform, + merge_packages_config, ) -from homeassistant.requirements import ( - async_get_integration_with_requirements, - RequirementsNotFound, -) - -import homeassistant.util.yaml.loader as yaml_loader +from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.typing import ConfigType +from homeassistant.requirements import ( + RequirementsNotFound, + async_get_integration_with_requirements, +) +import homeassistant.util.yaml.loader as yaml_loader -# mypy: allow-untyped-calls, allow-untyped-defs, no-warn-return-any +class CheckConfigError(NamedTuple): + """Configuration check error.""" -CheckConfigError = namedtuple("CheckConfigError", "message domain config") + message: str + domain: Optional[str] + config: Optional[ConfigType] @attr.s @@ -38,7 +41,12 @@ class HomeAssistantConfig(OrderedDict): errors: List[CheckConfigError] = attr.ib(default=attr.Factory(list)) - def add_error(self, message, domain=None, config=None): + def add_error( + self, + message: str, + domain: Optional[str] = None, + config: Optional[ConfigType] = None, + ) -> "HomeAssistantConfig": """Add a single error.""" self.errors.append(CheckConfigError(str(message), domain, config)) return self @@ -57,7 +65,9 @@ async def async_check_ha_config_file(hass: HomeAssistant) -> HomeAssistantConfig config_dir = hass.config.config_dir result = HomeAssistantConfig() - def _pack_error(package, component, config, message): + def _pack_error( + package: str, component: str, config: ConfigType, message: str + ) -> None: """Handle errors from packages: _log_pkg_error.""" message = "Package {} setup failed. Component {} {}".format( package, component, message @@ -66,7 +76,7 @@ async def async_check_ha_config_file(hass: HomeAssistant) -> HomeAssistantConfig pack_config = core_config[CONF_PACKAGES].get(package, config) result.add_error(message, domain, pack_config) - def _comp_error(ex, domain, config): + def _comp_error(ex: Exception, domain: str, config: ConfigType) -> None: """Handle errors from components: async_log_exception.""" result.add_error(_format_config_error(ex, domain, config), domain, config) diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index df82ba6076f..02853f7615b 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -6,9 +6,6 @@ import logging import sys from typing import Callable, Container, Optional, Union, cast -from homeassistant.helpers.template import Template -from homeassistant.helpers.typing import ConfigType, TemplateVarsType -from homeassistant.core import HomeAssistant, State from homeassistant.components import zone as zone_cmp from homeassistant.components.device_automation import ( async_get_device_automation_platform, @@ -34,11 +31,14 @@ from homeassistant.const import ( SUN_EVENT_SUNSET, WEEKDAYS, ) -from homeassistant.exceptions import TemplateError, HomeAssistantError +from homeassistant.core import HomeAssistant, State +from homeassistant.exceptions import HomeAssistantError, TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.sun import get_astral_event_date -import homeassistant.util.dt as dt_util +from homeassistant.helpers.template import Template +from homeassistant.helpers.typing import ConfigType, TemplateVarsType from homeassistant.util.async_ import run_callback_threadsafe +import homeassistant.util.dt as dt_util FROM_CONFIG_FORMAT = "{}_from_config" ASYNC_FROM_CONFIG_FORMAT = "async_{}_from_config" @@ -192,7 +192,7 @@ def async_numeric_state( fvalue = float(value) except ValueError: _LOGGER.warning( - "Value cannot be processed as a number: %s " "(Offending entity: %s)", + "Value cannot be processed as a number: %s (Offending entity: %s)", entity, value, ) diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index 7a1512957a2..323c6907411 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -1,6 +1,8 @@ """Helpers for data entry flows for config entries.""" -from typing import Callable, Awaitable, Union +from typing import Awaitable, Callable, Union + from homeassistant import config_entries + from .typing import HomeAssistantType # mypy: allow-untyped-defs, no-check-untyped-defs @@ -24,7 +26,7 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow): self._domain = domain self._title = title self._discovery_function = discovery_function - self.CONNECTION_CLASS = connection_class # pylint: disable=C0103 + self.CONNECTION_CLASS = connection_class # pylint: disable=invalid-name async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index dc3d3c91f27..2fdfea8673f 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -5,26 +5,25 @@ This module exists of the following parts: - OAuth2 implementation that works with local provided client ID/secret """ +from abc import ABC, ABCMeta, abstractmethod import asyncio -from abc import ABCMeta, ABC, abstractmethod import logging -from typing import Optional, Any, Dict, cast, Awaitable, Callable +import secrets import time +from typing import Any, Awaitable, Callable, Dict, Optional, cast +from aiohttp import client, web import async_timeout -from aiohttp import web, client import jwt import voluptuous as vol from yarl import URL -from homeassistant.auth.util import generate_secret -from homeassistant.core import HomeAssistant, callback from homeassistant import config_entries from homeassistant.components.http import HomeAssistantView +from homeassistant.core import HomeAssistant, callback from .aiohttp_client import async_get_clientsession - DATA_JWT_SECRET = "oauth2_jwt_secret" DATA_VIEW_REGISTERED = "oauth2_view_reg" DATA_IMPLEMENTATIONS = "oauth2_impl" @@ -442,7 +441,7 @@ def _encode_jwt(hass: HomeAssistant, data: dict) -> str: secret = hass.data.get(DATA_JWT_SECRET) if secret is None: - secret = hass.data[DATA_JWT_SECRET] = generate_secret() + secret = hass.data[DATA_JWT_SECRET] = secrets.token_hex() return jwt.encode(data, secret, algorithm="HS256").decode() diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 7ca5a7e86f9..bcf0d42df70 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -1,17 +1,30 @@ """Helpers for config validation using voluptuous.""" -import inspect -import logging -import os -import re from datetime import ( - timedelta, + date as date_sys, datetime as datetime_sys, time as time_sys, - date as date_sys, + timedelta, ) -from socket import _GLOBAL_DEFAULT_TIMEOUT +from enum import Enum +import inspect +import logging from numbers import Number -from typing import Any, Union, TypeVar, Callable, List, Dict, Optional +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 urllib.parse import urlparse from uuid import UUID @@ -19,8 +32,9 @@ from pkg_resources import parse_version import voluptuous as vol import voluptuous_serialize -import homeassistant.util.dt as dt_util from homeassistant.const import ( + ATTR_AREA_ID, + ATTR_ENTITY_ID, CONF_ABOVE, CONF_ALIAS, CONF_BELOW, @@ -33,10 +47,10 @@ from homeassistant.const import ( CONF_PLATFORM, CONF_SCAN_INTERVAL, CONF_STATE, + CONF_TIMEOUT, CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_METRIC, CONF_VALUE_TEMPLATE, - CONF_TIMEOUT, ENTITY_MATCH_ALL, SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET, @@ -44,17 +58,14 @@ from homeassistant.const import ( TEMP_FAHRENHEIT, WEEKDAYS, __version__, - ATTR_AREA_ID, - ATTR_ENTITY_ID, ) -from homeassistant.core import valid_entity_id, split_entity_id +from homeassistant.core import split_entity_id, valid_entity_id from homeassistant.exceptions import TemplateError +from homeassistant.helpers import template as template_helper from homeassistant.helpers.logging import KeywordStyleAdapter from homeassistant.util import slugify as util_slugify +import homeassistant.util.dt as dt_util - -# mypy: allow-untyped-calls, allow-untyped-defs -# mypy: no-check-untyped-defs, no-warn-return-any # pylint: disable=invalid-name TIME_PERIOD_ERROR = "offset {} should be format 'HH:MM' or 'HH:MM:SS'" @@ -127,7 +138,7 @@ def boolean(value: Any) -> bool: raise vol.Invalid("invalid boolean value {}".format(value)) -def isdevice(value): +def isdevice(value: Any) -> str: """Validate that value is a real device.""" try: os.stat(value) @@ -136,19 +147,19 @@ def isdevice(value): raise vol.Invalid("No device at {} found".format(value)) -def matches_regex(regex): +def matches_regex(regex: str) -> Callable[[Any], str]: """Validate that the value is a string that matches a regex.""" - regex = re.compile(regex) + compiled = re.compile(regex) def validator(value: Any) -> str: """Validate that value matches the given regex.""" if not isinstance(value, str): raise vol.Invalid("not a string value: {}".format(value)) - if not regex.match(value): + if not compiled.match(value): raise vol.Invalid( "value {} does not match regular expression {}".format( - value, regex.pattern + value, compiled.pattern ) ) @@ -157,14 +168,14 @@ def matches_regex(regex): return validator -def is_regex(value): +def is_regex(value: Any) -> Pattern[Any]: """Validate that a string is a valid regular expression.""" try: r = re.compile(value) return r except TypeError: raise vol.Invalid( - "value {} is of the wrong type for a regular " "expression".format(value) + "value {} is of the wrong type for a regular expression".format(value) ) except re.error: raise vol.Invalid("value {} is not a valid regular expression".format(value)) @@ -205,9 +216,9 @@ def ensure_list(value: Union[T, List[T], None]) -> List[T]: def entity_id(value: Any) -> str: """Validate Entity ID.""" - value = string(value).lower() - if valid_entity_id(value): - return value + str_value = string(value).lower() + if valid_entity_id(str_value): + return str_value raise vol.Invalid("Entity ID {} is an invalid entity id".format(value)) @@ -254,17 +265,17 @@ def entities_domain(domain: str) -> Callable[[Union[str, List]], List[str]]: return validate -def enum(enumClass): +def enum(enumClass: Type[Enum]) -> vol.All: """Create validator for specified enum.""" return vol.All(vol.In(enumClass.__members__), enumClass.__getitem__) -def icon(value): +def icon(value: Any) -> str: """Validate icon.""" - value = str(value) + str_value = str(value) - if ":" in value: - return value + if ":" in str_value: + return str_value raise vol.Invalid('Icons should be specified in the form "prefix:name"') @@ -363,7 +374,7 @@ def time_period_seconds(value: Union[int, str]) -> timedelta: time_period = vol.Any(time_period_str, time_period_seconds, timedelta, time_period_dict) -def match_all(value): +def match_all(value: T) -> T: """Validate that matches all values.""" return value @@ -383,12 +394,12 @@ def remove_falsy(value: List[T]) -> List[T]: return [v for v in value if v] -def service(value): +def service(value: Any) -> str: """Validate service.""" # Services use same format as entities so we can use same helper. - value = string(value).lower() - if valid_entity_id(value): - return value + str_value = string(value).lower() + if valid_entity_id(str_value): + return str_value raise vol.Invalid("Service {} does not match format .".format(value)) @@ -408,7 +419,7 @@ def schema_with_slug_keys(value_schema: Union[T, Callable]) -> Callable: for key in value.keys(): slug(key) - return schema(value) + return cast(Dict, schema(value)) return verify @@ -417,10 +428,10 @@ def slug(value: Any) -> str: """Validate value is a valid slug.""" if value is None: raise vol.Invalid("Slug should not be None") - value = str(value) - slg = util_slugify(value) - if value == slg: - return value + str_value = str(value) + slg = util_slugify(str_value) + if str_value == slg: + return str_value raise vol.Invalid("invalid slug {} (try {})".format(value, slg)) @@ -459,41 +470,41 @@ unit_system = vol.All( ) -def template(value): +def template(value: Optional[Any]) -> template_helper.Template: """Validate a jinja2 template.""" - from homeassistant.helpers import template as template_helper 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") - value = template_helper.Template(str(value)) + template_value = template_helper.Template(str(value)) # type: ignore try: - value.ensure_valid() - return value + template_value.ensure_valid() + return cast(template_helper.Template, template_value) except TemplateError as ex: raise vol.Invalid("invalid template ({})".format(ex)) -def template_complex(value): +def template_complex(value: Any) -> Any: """Validate a complex jinja2 template.""" if isinstance(value, list): - return_value = value.copy() - for idx, element in enumerate(return_value): - return_value[idx] = template_complex(element) - return return_value + return_list = value.copy() + for idx, element in enumerate(return_list): + return_list[idx] = template_complex(element) + return return_list if isinstance(value, dict): - return_value = value.copy() - for key, element in return_value.items(): - return_value[key] = template_complex(element) - return return_value - - return template(value) + return_dict = value.copy() + for key, element in return_dict.items(): + return_dict[key] = template_complex(element) + return return_dict + if isinstance(value, str): + return template(value) + return value -def datetime(value): +def datetime(value: Any) -> datetime_sys: """Validate datetime.""" if isinstance(value, datetime_sys): return value @@ -509,7 +520,7 @@ def datetime(value): return date_val -def time_zone(value): +def time_zone(value: str) -> str: """Validate timezone.""" if dt_util.get_time_zone(value) is not None: return value @@ -522,7 +533,7 @@ def time_zone(value): weekdays = vol.All(ensure_list, [vol.In(WEEKDAYS)]) -def socket_timeout(value): +def socket_timeout(value: Optional[Any]) -> object: """Validate timeout float > 0.0. None coerced to socket._GLOBAL_DEFAULT_TIMEOUT bare object. @@ -533,7 +544,7 @@ def socket_timeout(value): float_value = float(value) if float_value > 0.0: return float_value - raise vol.Invalid("Invalid socket timeout value." " float > 0.0 required.") + raise vol.Invalid("Invalid socket timeout value. float > 0.0 required.") except Exception as _: raise vol.Invalid("Invalid socket timeout: {err}".format(err=_)) @@ -544,12 +555,12 @@ def url(value: Any) -> str: url_in = str(value) if urlparse(url_in).scheme in ["http", "https"]: - return vol.Schema(vol.Url())(url_in) + return cast(str, vol.Schema(vol.Url())(url_in)) raise vol.Invalid("invalid url") -def x10_address(value): +def x10_address(value: str) -> str: """Validate an x10 address.""" regex = re.compile(r"([A-Pa-p]{1})(?:[2-9]|1[0-6]?)$") if not regex.match(value): @@ -557,7 +568,7 @@ def x10_address(value): return str(value).lower() -def uuid4_hex(value): +def uuid4_hex(value: Any) -> str: """Validate a v4 UUID in hex format.""" try: result = UUID(value, version=4) @@ -678,10 +689,12 @@ def deprecated( # Validator helpers -def key_dependency(key, dependency): +def key_dependency( + key: Hashable, dependency: Hashable +) -> Callable[[Dict[Hashable, Any]], Dict[Hashable, Any]]: """Validate that all dependencies exist for key.""" - def validator(value): + def validator(value: Dict[Hashable, Any]) -> Dict[Hashable, Any]: """Test dependencies.""" if not isinstance(value, dict): raise vol.Invalid("key dependencies require a dict") @@ -696,7 +709,7 @@ def key_dependency(key, dependency): return validator -def custom_serializer(schema): +def custom_serializer(schema: Any) -> Any: """Serialize additional types for voluptuous_serialize.""" if schema is positive_time_period_dict: return {"type": "positive_time_period_dict"} @@ -715,12 +728,23 @@ PLATFORM_SCHEMA = vol.Schema( PLATFORM_SCHEMA_BASE = PLATFORM_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA) -ENTITY_SERVICE_SCHEMA = vol.Schema( - { - vol.Optional(ATTR_ENTITY_ID): comp_entity_ids, - vol.Optional(ATTR_AREA_ID): vol.All(ensure_list, [str]), - } -) + +def make_entity_service_schema( + schema: dict, *, extra: int = vol.PREVENT_EXTRA +) -> vol.All: + """Create an entity service schema.""" + return vol.All( + vol.Schema( + { + **schema, + vol.Optional(ATTR_ENTITY_ID): comp_entity_ids, + vol.Optional(ATTR_AREA_ID): vol.All(ensure_list, [str]), + }, + extra=extra, + ), + has_at_least_one_key(ATTR_ENTITY_ID, ATTR_AREA_ID), + ) + EVENT_SCHEMA = vol.Schema( { diff --git a/homeassistant/helpers/data_entry_flow.py b/homeassistant/helpers/data_entry_flow.py index 1d471052474..ac5fb608675 100644 --- a/homeassistant/helpers/data_entry_flow.py +++ b/homeassistant/helpers/data_entry_flow.py @@ -2,11 +2,10 @@ import voluptuous as vol -from homeassistant import data_entry_flow, config_entries +from homeassistant import config_entries, data_entry_flow from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.data_validator import RequestDataValidator - # mypy: allow-untyped-calls, allow-untyped-defs diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 456678edac7..512334c8d3c 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -1,9 +1,9 @@ """Provide a way to connect entities belonging to one device.""" -import logging -import uuid from asyncio import Event from collections import OrderedDict -from typing import List, Optional, cast +import logging +from typing import Any, Dict, List, Optional, cast +import uuid import attr @@ -12,9 +12,7 @@ from homeassistant.loader import bind_hass from .typing import HomeAssistantType - -# mypy: allow-untyped-calls, allow-untyped-defs -# mypy: no-check-untyped-defs, no-warn-return-any +# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) _UNDEF = object() @@ -49,7 +47,7 @@ class DeviceEntry: is_new = attr.ib(type=bool, default=False) -def format_mac(mac): +def format_mac(mac: str) -> str: """Format the mac address string for entry into dev reg.""" to_test = mac @@ -72,10 +70,11 @@ def format_mac(mac): class DeviceRegistry: """Class to hold a registry of devices.""" - def __init__(self, hass): + devices: Dict[str, DeviceEntry] + + def __init__(self, hass: HomeAssistantType) -> None: """Initialize the device registry.""" self.hass = hass - self.devices = None self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) @callback @@ -261,7 +260,7 @@ class DeviceRegistry: return new - def async_remove_device(self, device_id): + def async_remove_device(self, device_id: str) -> None: """Remove a device from the device registry.""" del self.devices[device_id] self.hass.bus.async_fire( @@ -299,12 +298,12 @@ class DeviceRegistry: self.devices = devices @callback - def async_schedule_save(self): + def async_schedule_save(self) -> None: """Schedule saving the device registry.""" self._store.async_delay_save(self._data_to_save, SAVE_DELAY) @callback - def _data_to_save(self): + def _data_to_save(self) -> Dict[str, List[Dict[str, Any]]]: """Return data of device registry to store in a file.""" data = {} @@ -328,7 +327,7 @@ class DeviceRegistry: return data @callback - def async_clear_config_entry(self, config_entry_id): + def async_clear_config_entry(self, config_entry_id: str) -> None: """Clear config entry from registry entries.""" remove = [] for dev_id, device in self.devices.items(): diff --git a/homeassistant/helpers/discovery.py b/homeassistant/helpers/discovery.py index bc1094613bb..a6162dbde55 100644 --- a/homeassistant/helpers/discovery.py +++ b/homeassistant/helpers/discovery.py @@ -5,14 +5,14 @@ There are two different types of discoveries that can be fired/listened for. - listen_platform/discover_platform is for platforms. These are used by components to allow discovery of their platforms. """ -from homeassistant import setup, core -from homeassistant.loader import bind_hass +from typing import Callable, Collection, Union + +from homeassistant import core, setup from homeassistant.const import ATTR_DISCOVERED, ATTR_SERVICE, EVENT_PLATFORM_DISCOVERED from homeassistant.exceptions import HomeAssistantError -from homeassistant.loader import DEPENDENCY_BLACKLIST +from homeassistant.loader import DEPENDENCY_BLACKLIST, bind_hass from homeassistant.util.async_ import run_callback_threadsafe - # mypy: allow-untyped-defs, no-check-untyped-defs EVENT_LOAD_PLATFORM = "load_platform.{}" @@ -20,7 +20,9 @@ ATTR_PLATFORM = "platform" @bind_hass -def listen(hass, service, callback): +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. @@ -30,7 +32,9 @@ def listen(hass, service, callback): @core.callback @bind_hass -def async_listen(hass, service, callback): +def async_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. @@ -41,7 +45,7 @@ def async_listen(hass, service, callback): service = tuple(service) @core.callback - def discovery_event_listener(event): + def discovery_event_listener(event: core.Event) -> None: """Listen for discovery events.""" if ATTR_SERVICE in event.data and event.data[ATTR_SERVICE] in service: hass.async_add_job( @@ -75,7 +79,9 @@ async def async_discover(hass, service, discovered, component, hass_config): @bind_hass -def listen_platform(hass, component, callback): +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 @@ -83,7 +89,9 @@ def listen_platform(hass, component, callback): @bind_hass -def async_listen_platform(hass, component, callback): +def async_listen_platform( + hass: core.HomeAssistant, component: str, callback: Callable +) -> None: """Register a platform loader listener. This method must be run in the event loop. @@ -91,7 +99,7 @@ def async_listen_platform(hass, component, callback): service = EVENT_LOAD_PLATFORM.format(component) @core.callback - def discovery_platform_listener(event): + def discovery_platform_listener(event: core.Event) -> None: """Listen for platform discovery events.""" if event.data.get(ATTR_SERVICE) != service: return diff --git a/homeassistant/helpers/dispatcher.py b/homeassistant/helpers/dispatcher.py index 81582f4fa54..a4e624f119f 100644 --- a/homeassistant/helpers/dispatcher.py +++ b/homeassistant/helpers/dispatcher.py @@ -6,8 +6,8 @@ from homeassistant.core import 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 +from .typing import HomeAssistantType _LOGGER = logging.getLogger(__name__) DATA_DISPATCHER = "dispatcher" diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 0d2182f88e1..b1786130b58 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -1,17 +1,21 @@ """An abstract class for entities.""" - +from abc import ABC import asyncio from datetime import datetime, timedelta -import logging import functools as ft +import logging from timeit import default_timer as timer from typing import Any, Dict, Iterable, List, Optional, Union +from homeassistant.config import DATA_CUSTOMIZE from homeassistant.const import ( ATTR_ASSUMED_STATE, + ATTR_DEVICE_CLASS, + ATTR_ENTITY_PICTURE, ATTR_FRIENDLY_NAME, ATTR_HIDDEN, ATTR_ICON, + ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, DEVICE_DEFAULT_NAME, STATE_OFF, @@ -20,22 +24,16 @@ from homeassistant.const import ( STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT, - ATTR_ENTITY_PICTURE, - ATTR_SUPPORTED_FEATURES, - ATTR_DEVICE_CLASS, ) +from homeassistant.core import CALLBACK_TYPE, Context, HomeAssistant, callback +from homeassistant.exceptions import NoEntitySpecifiedError from homeassistant.helpers.entity_platform import EntityPlatform from homeassistant.helpers.entity_registry import ( EVENT_ENTITY_REGISTRY_UPDATED, RegistryEntry, ) -from homeassistant.core import HomeAssistant, callback, CALLBACK_TYPE, Context -from homeassistant.config import DATA_CUSTOMIZE -from homeassistant.exceptions import NoEntitySpecifiedError -from homeassistant.util import ensure_unique_string, slugify +from homeassistant.util import dt as dt_util, ensure_unique_string, slugify from homeassistant.util.async_ import run_callback_threadsafe -from homeassistant.util import dt as dt_util - # mypy: allow-untyped-defs, no-check-untyped-defs, no-warn-return-any @@ -85,7 +83,7 @@ def async_generate_entity_id( return ensure_unique_string(entity_id_format.format(slugify(name)), current_ids) -class Entity: +class Entity(ABC): """An abstract class for Home Assistant entities.""" # SAFE TO OVERWRITE @@ -144,6 +142,17 @@ class Entity: """Return the state of the entity.""" return STATE_UNKNOWN + @property + def capability_attributes(self) -> Optional[Dict[str, Any]]: + """Return the capability attributes. + + Attributes that explain the capabilities of an entity. + + Implemented by component base class. Convention for attribute names + is lowercase snake_case. + """ + return None + @property def state_attributes(self) -> Optional[Dict[str, Any]]: """Return the state attributes. @@ -302,7 +311,9 @@ class Entity: start = timer() - attr = {} + attr = self.capability_attributes + attr = dict(attr) if attr else {} + if not self.available: state = STATE_UNAVAILABLE else: diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 42b19da889e..84aa8becafd 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -5,22 +5,23 @@ from itertools import chain import logging from homeassistant import config as conf_util -from homeassistant.setup import async_prepare_setup_platform +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, - CONF_SCAN_INTERVAL, CONF_ENTITY_NAMESPACE, + CONF_SCAN_INTERVAL, ENTITY_MATCH_ALL, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_per_platform, discovery -from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA +from homeassistant.helpers.config_validation import make_entity_service_schema from homeassistant.helpers.service import async_extract_entity_ids -from homeassistant.loader import bind_hass, async_get_integration +from homeassistant.loader import async_get_integration, bind_hass +from homeassistant.setup import async_prepare_setup_platform from homeassistant.util import slugify -from .entity_platform import EntityPlatform +from .entity_platform import EntityPlatform # mypy: allow-untyped-defs, no-check-untyped-defs @@ -29,7 +30,7 @@ DATA_INSTANCES = "entity_components" @bind_hass -async def async_update_entity(hass, entity_id): +async def async_update_entity(hass: HomeAssistant, entity_id: str) -> None: """Trigger an update for an entity.""" domain = entity_id.split(".", 1)[0] entity_comp = hass.data.get(DATA_INSTANCES, {}).get(domain) @@ -158,7 +159,7 @@ class EntityComponent: return await self._platforms[key].async_setup_entry(config_entry) - async def async_unload_entry(self, config_entry): + async def async_unload_entry(self, config_entry: ConfigEntry) -> bool: """Unload a config entry.""" key = config_entry.entry_id @@ -173,24 +174,16 @@ class EntityComponent: async def async_extract_from_service(self, service, expand_group=True): """Extract all known and available entities from a service call. - Will return all entities if no entities specified in call. Will return an empty list if entities specified but unknown. This method must be run in the event loop. """ data_ent_id = service.data.get(ATTR_ENTITY_ID) - if data_ent_id in (None, ENTITY_MATCH_ALL): - if data_ent_id is None: - self.logger.warning( - "Not passing an entity ID to a service to target all " - "entities is deprecated. Update your call to %s.%s to be " - "instead: entity_id: %s", - service.domain, - service.service, - ENTITY_MATCH_ALL, - ) + if data_ent_id is None: + return [] + if data_ent_id == ENTITY_MATCH_ALL: return [entity for entity in self.entities if entity.available] entity_ids = await async_extract_entity_ids(self.hass, service, expand_group) @@ -204,7 +197,7 @@ class EntityComponent: def async_register_entity_service(self, name, schema, func, required_features=None): """Register an entity service.""" if isinstance(schema, dict): - schema = ENTITY_SERVICE_SCHEMA.extend(schema) + schema = make_entity_service_schema(schema) async def handle_service(call): """Handle the service.""" @@ -245,7 +238,7 @@ class EntityComponent: await self._platforms[key].async_setup(platform_config, discovery_info) @callback - def _async_update_group(self): + def _async_update_group(self) -> None: """Set up and/or update component group. This method must be run in the event loop. @@ -273,7 +266,7 @@ class EntityComponent: ) ) - async def _async_reset(self): + async def _async_reset(self) -> None: """Remove entities and reset the entity component to initial values. This method must be run in the event loop. @@ -291,7 +284,7 @@ class EntityComponent: "group", "remove", dict(object_id=slugify(self.group_name)) ) - async def async_remove_entity(self, entity_id): + async def async_remove_entity(self, entity_id: str) -> None: """Remove an entity managed by one of the platforms.""" for platform in self._platforms.values(): if entity_id in platform.entities: diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 376a6e23e9a..9b82eb76dec 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -1,16 +1,16 @@ """Class to manage the entities for a single platform.""" import asyncio from contextvars import ContextVar +from datetime import datetime from typing import Optional from homeassistant.const import DEVICE_DEFAULT_NAME -from homeassistant.core import callback, valid_entity_id, split_entity_id +from homeassistant.core import callback, split_entity_id, valid_entity_id from homeassistant.exceptions import HomeAssistantError, PlatformNotReady from homeassistant.util.async_ import run_callback_threadsafe from .entity_registry import DISABLED_INTEGRATION -from .event import async_track_time_interval, async_call_later - +from .event import async_call_later, async_track_time_interval # mypy: allow-untyped-defs, no-check-untyped-defs @@ -65,14 +65,14 @@ class EntityPlatform: # which powers entity_component.add_entities if platform is None: self.parallel_updates = None - self.parallel_updates_semaphore = None + self.parallel_updates_semaphore: Optional[asyncio.Semaphore] = None return self.parallel_updates = getattr(platform, "PARALLEL_UPDATES", None) # semaphore will be created on demand self.parallel_updates_semaphore = None - def _get_parallel_updates_semaphore(self): + def _get_parallel_updates_semaphore(self) -> asyncio.Semaphore: """Get or create a semaphore for parallel updates.""" if self.parallel_updates_semaphore is None: self.parallel_updates_semaphore = asyncio.Semaphore( @@ -347,6 +347,9 @@ class EntityPlatform: device_id=device_id, known_object_ids=self.entities.keys(), disabled_by=disabled_by, + capabilities=entity.capability_attributes, + supported_features=entity.supported_features, + device_class=entity.device_class, ) entity.registry_entry = entry @@ -387,10 +390,16 @@ class EntityPlatform: # Make sure it is valid in case an entity set the value themselves if not valid_entity_id(entity.entity_id): raise HomeAssistantError(f"Invalid entity id: {entity.entity_id}") - if ( - entity.entity_id in self.entities - or entity.entity_id in self.hass.states.async_entity_ids(self.domain) - ): + + already_exists = entity.entity_id in self.entities + + if not already_exists: + existing = self.hass.states.get(entity.entity_id) + + if existing and not existing.attributes.get("restored"): + already_exists = True + + if already_exists: msg = f"Entity id already exists: {entity.entity_id}" if entity.unique_id is not None: msg += ". Platform {} does not generate unique IDs".format( @@ -407,7 +416,7 @@ class EntityPlatform: await entity.async_update_ha_state() - async def async_reset(self): + async def async_reset(self) -> None: """Remove all entities and reset data. This method must be run in the event loop. @@ -427,7 +436,7 @@ class EntityPlatform: self._async_unsub_polling() self._async_unsub_polling = None - async def async_remove_entity(self, entity_id): + async def async_remove_entity(self, entity_id: str) -> None: """Remove entity id from platform.""" await self.entities[entity_id].async_remove() @@ -438,7 +447,7 @@ class EntityPlatform: self._async_unsub_polling() self._async_unsub_polling = None - async def _update_entity_states(self, now): + async def _update_entity_states(self, now: datetime) -> None: """Update the states of all the polling entities. To protect from flooding the executor, we will update async entities @@ -450,7 +459,7 @@ class EntityPlatform: self._process_updates = asyncio.Lock() if self._process_updates.locked(): self.logger.warning( - "Updating %s %s took longer than the scheduled update " "interval %s", + "Updating %s %s took longer than the scheduled update interval %s", self.platform_name, self.domain, self.scan_interval, diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 08f29a9fb3e..77d8ccc00e0 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -11,10 +11,16 @@ import asyncio from collections import OrderedDict from itertools import chain import logging -from typing import Any, Dict, Iterable, List, Optional, cast +from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, cast import attr +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_SUPPORTED_FEATURES, + EVENT_HOMEASSISTANT_START, + STATE_UNAVAILABLE, +) from homeassistant.core import Event, callback, split_entity_id, valid_entity_id from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED from homeassistant.loader import bind_hass @@ -23,6 +29,8 @@ from homeassistant.util.yaml import load_yaml from .typing import HomeAssistantType +if TYPE_CHECKING: + from homeassistant.config_entries import ConfigEntry # noqa: F401 # mypy: allow-untyped-defs, no-check-untyped-defs @@ -37,6 +45,8 @@ DISABLED_HASS = "hass" DISABLED_USER = "user" DISABLED_INTEGRATION = "integration" +ATTR_RESTORED = "restored" + STORAGE_VERSION = 1 STORAGE_KEY = "core.entity_registry" @@ -49,7 +59,7 @@ class RegistryEntry: unique_id = attr.ib(type=str) platform = attr.ib(type=str) name = attr.ib(type=str, default=None) - device_id = attr.ib(type=str, default=None) + device_id: Optional[str] = attr.ib(default=None) config_entry_id: Optional[str] = attr.ib(default=None) disabled_by = attr.ib( type=Optional[str], @@ -64,6 +74,9 @@ class RegistryEntry: ) ), ) + capabilities: Optional[Dict[str, Any]] = attr.ib(default=None) + supported_features: int = attr.ib(default=0) + device_class: Optional[str] = attr.ib(default=None) domain = attr.ib(type=str, init=False, repr=False) @domain.default @@ -136,16 +149,22 @@ class EntityRegistry: @callback def async_get_or_create( self, - domain, - platform, - unique_id, + domain: str, + platform: str, + unique_id: str, *, - suggested_object_id=None, - config_entry=None, - device_id=None, - known_object_ids=None, - disabled_by=None, - ): + # To influence entity ID generation + suggested_object_id: Optional[str] = None, + known_object_ids: Optional[Iterable[str]] = None, + # To disable an entity if it gets created + disabled_by: Optional[str] = None, + # Data that we want entry to have + config_entry: Optional["ConfigEntry"] = None, + device_id: Optional[str] = None, + capabilities: Optional[Dict[str, Any]] = None, + supported_features: Optional[int] = None, + device_class: Optional[str] = None, + ) -> RegistryEntry: """Get entity. Create if it doesn't exist.""" config_entry_id = None if config_entry: @@ -154,10 +173,13 @@ class EntityRegistry: entity_id = self.async_get_entity_id(domain, platform, unique_id) if entity_id: - return self._async_update_entity( + return self._async_update_entity( # type: ignore entity_id, config_entry_id=config_entry_id or _UNDEF, device_id=device_id or _UNDEF, + capabilities=capabilities or _UNDEF, + supported_features=supported_features or _UNDEF, + device_class=device_class or _UNDEF, # When we changed our slugify algorithm, we invalidated some # stored entity IDs with either a __ or ending in _. # Fix introduced in 0.86 (Jan 23, 2019). Next line can be @@ -185,6 +207,9 @@ class EntityRegistry: unique_id=unique_id, platform=platform, disabled_by=disabled_by, + capabilities=capabilities, + supported_features=supported_features or 0, + device_class=device_class, ) self.entities[entity_id] = entity _LOGGER.info("Registered new %s.%s entity: %s", domain, platform, entity_id) @@ -229,12 +254,15 @@ class EntityRegistry: disabled_by=_UNDEF, ): """Update properties of an entity.""" - return self._async_update_entity( - entity_id, - name=name, - new_entity_id=new_entity_id, - new_unique_id=new_unique_id, - disabled_by=disabled_by, + return cast( # cast until we have _async_update_entity type hinted + RegistryEntry, + self._async_update_entity( + entity_id, + name=name, + new_entity_id=new_entity_id, + new_unique_id=new_unique_id, + disabled_by=disabled_by, + ), ) @callback @@ -248,6 +276,9 @@ class EntityRegistry: device_id=_UNDEF, new_unique_id=_UNDEF, disabled_by=_UNDEF, + capabilities=_UNDEF, + supported_features=_UNDEF, + device_class=_UNDEF, ): """Private facing update properties method.""" old = self.entities[entity_id] @@ -259,6 +290,9 @@ class EntityRegistry: ("config_entry_id", config_entry_id), ("device_id", device_id), ("disabled_by", disabled_by), + ("capabilities", capabilities), + ("supported_features", supported_features), + ("device_class", device_class), ): if value is not _UNDEF and value != getattr(old, attr_name): changes[attr_name] = value @@ -313,6 +347,8 @@ class EntityRegistry: async def async_load(self) -> None: """Load the entity registry.""" + async_setup_entity_restore(self.hass, self) + data = await self.hass.helpers.storage.async_migrator( self.hass.config.path(PATH_REGISTRY), self._store, @@ -331,6 +367,9 @@ class EntityRegistry: platform=entity["platform"], name=entity.get("name"), disabled_by=entity.get("disabled_by"), + capabilities=entity.get("capabilities") or {}, + supported_features=entity.get("supported_features", 0), + device_class=entity.get("device_class"), ) self.entities = entities @@ -354,6 +393,9 @@ class EntityRegistry: "platform": entry.platform, "name": entry.name, "disabled_by": entry.disabled_by, + "capabilities": entry.capabilities, + "supported_features": entry.supported_features, + "device_class": entry.device_class, } for entry in self.entities.values() ] @@ -411,3 +453,53 @@ async def _async_migrate(entities: Dict[str, Any]) -> Dict[str, List[Dict[str, A {"entity_id": entity_id, **info} for entity_id, info in entities.items() ] } + + +@callback +def async_setup_entity_restore( + hass: HomeAssistantType, registry: EntityRegistry +) -> None: + """Set up the entity restore mechanism.""" + + @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): + return + + hass.states.async_remove(event.data["entity_id"]) + + hass.bus.async_listen(EVENT_ENTITY_REGISTRY_UPDATED, cleanup_restored_states) + + if hass.is_running: + return + + @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()) + + 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: + attrs.update(entry.capabilities) + + if entry.supported_features: + attrs[ATTR_SUPPORTED_FEATURES] = entry.supported_features + + if entry.device_class: + attrs[ATTR_DEVICE_CLASS] = entry.device_class + + states.async_set(entry.entity_id, STATE_UNAVAILABLE, attrs) + + hass.bus.async_listen(EVENT_HOMEASSISTANT_START, _write_unavailable_states) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 715344a3969..b3c8af6f50c 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -5,23 +5,22 @@ from typing import Any, Callable, Dict, Iterable, Optional, Union, cast import attr -from homeassistant.loader import bind_hass -from homeassistant.helpers.sun import get_astral_event_next -from homeassistant.helpers.template import Template -from homeassistant.core import HomeAssistant, callback, CALLBACK_TYPE, Event, State from homeassistant.const import ( ATTR_NOW, + EVENT_CORE_CONFIG_UPDATE, EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL, SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET, - EVENT_CORE_CONFIG_UPDATE, ) +from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, State, callback +from homeassistant.helpers.sun import get_astral_event_next +from homeassistant.helpers.template import Template +from homeassistant.loader import bind_hass from homeassistant.util import dt as dt_util from homeassistant.util.async_ import run_callback_threadsafe - # PyLint does not like the use of threaded_listener_factory # pylint: disable=invalid-name diff --git a/homeassistant/helpers/icon.py b/homeassistant/helpers/icon.py index 96c3b7e08c1..b2a1d58717b 100644 --- a/homeassistant/helpers/icon.py +++ b/homeassistant/helpers/icon.py @@ -18,3 +18,14 @@ def icon_for_battery_level( elif 5 < battery_level < 95: icon += "-{}".format(int(round(battery_level / 10 - 0.01)) * 10) return icon + + +def icon_for_signal_level(signal_level: Optional[int] = None) -> str: + """Return a signal icon valid identifier.""" + if signal_level is None or signal_level == 0: + return "mdi:signal-cellular-outline" + if signal_level > 70: + return "mdi:signal-cellular-3" + if signal_level > 30: + return "mdi:signal-cellular-2" + return "mdi:signal-cellular-1" diff --git a/homeassistant/helpers/integration_platform.py b/homeassistant/helpers/integration_platform.py new file mode 100644 index 00000000000..01567c72c7b --- /dev/null +++ b/homeassistant/helpers/integration_platform.py @@ -0,0 +1,46 @@ +"""Helpers to help with integration platforms.""" +import asyncio +import logging +from typing import Any, Awaitable, Callable + +from homeassistant.core import Event, HomeAssistant +from homeassistant.loader import IntegrationNotFound, async_get_integration, bind_hass +from homeassistant.setup import ATTR_COMPONENT, EVENT_COMPONENT_LOADED + +_LOGGER = logging.getLogger(__name__) + + +@bind_hass +async def async_process_integration_platforms( + hass: HomeAssistant, + platform_name: str, + # Any = platform. + process_platform: Callable[[HomeAssistant, str, Any], Awaitable[None]], +) -> None: + """Process a specific platform for all current and future loaded integrations.""" + + async def _process(component_name: str) -> None: + """Process the intents of a component.""" + try: + integration = await async_get_integration(hass, component_name) + platform = integration.get_platform(platform_name) + except (IntegrationNotFound, ImportError): + return + + try: + await process_platform(hass, component_name, platform) + except Exception: # pylint: disable=broad-except + _LOGGER.exception( + "Error processing platform %s.%s", component_name, platform_name + ) + + async def async_component_loaded(event: Event) -> None: + """Handle a new component loaded.""" + await _process(event.data[ATTR_COMPONENT]) + + hass.bus.async_listen(EVENT_COMPONENT_LOADED, async_component_loaded) + + tasks = [_process(comp) for comp in hass.config.components] + + if tasks: + await asyncio.gather(*tasks) diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index dc48d825348..181d1baebc0 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -5,13 +5,12 @@ from typing import Any, Callable, Dict, Iterable, Optional import voluptuous as vol -from homeassistant.const import ATTR_SUPPORTED_FEATURES -from homeassistant.core import callback, State, T +from homeassistant.const import ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES +from homeassistant.core import Context, 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 -from homeassistant.const import ATTR_ENTITY_ID _LOGGER = logging.getLogger(__name__) _SlotsType = Dict[str, Any] @@ -53,6 +52,7 @@ async def async_handle( intent_type: str, slots: Optional[_SlotsType] = None, text_input: Optional[str] = None, + context: Optional[Context] = None, ) -> "IntentResponse": """Handle an intent.""" handler: IntentHandler = hass.data.get(DATA_KEY, {}).get(intent_type) @@ -60,7 +60,10 @@ async def async_handle( if handler is None: raise UnknownIntent(f"Unknown intent {intent_type}") - intent = Intent(hass, platform, intent_type, slots or {}, text_input) + if context is None: + context = Context() + + intent = Intent(hass, platform, intent_type, slots or {}, text_input, context) try: _LOGGER.info("Triggering intent handler %s", handler) @@ -196,7 +199,10 @@ class ServiceIntentHandler(IntentHandler): state = async_match_state(hass, slots["name"]["value"]) await hass.services.async_call( - self.domain, self.service, {ATTR_ENTITY_ID: state.entity_id} + self.domain, + self.service, + {ATTR_ENTITY_ID: state.entity_id}, + context=intent_obj.context, ) response = intent_obj.create_response() @@ -207,7 +213,7 @@ class ServiceIntentHandler(IntentHandler): class Intent: """Hold the intent.""" - __slots__ = ["hass", "platform", "intent_type", "slots", "text_input"] + __slots__ = ["hass", "platform", "intent_type", "slots", "text_input", "context"] def __init__( self, @@ -216,6 +222,7 @@ class Intent: intent_type: str, slots: _SlotsType, text_input: Optional[str], + context: Context, ) -> None: """Initialize an intent.""" self.hass = hass @@ -223,6 +230,7 @@ class Intent: self.intent_type = intent_type self.slots = slots self.text_input = text_input + self.context = context @callback def create_response(self) -> "IntentResponse": diff --git a/homeassistant/helpers/logging.py b/homeassistant/helpers/logging.py index dd9e3833801..0b274458045 100644 --- a/homeassistant/helpers/logging.py +++ b/homeassistant/helpers/logging.py @@ -1,9 +1,7 @@ """Helpers for logging allowing more advanced logging styles to be used.""" import inspect import logging - - -# mypy: allow-untyped-defs, no-check-untyped-defs +from typing import Any, Mapping, MutableMapping, Optional, Tuple class KeywordMessage: @@ -13,13 +11,13 @@ class KeywordMessage: Adapted from: https://stackoverflow.com/a/24683360/2267718 """ - def __init__(self, fmt, args, kwargs): - """Initialize a new BraceMessage object.""" + def __init__(self, fmt: Any, args: Any, kwargs: Mapping[str, Any]) -> None: + """Initialize a new KeywordMessage object.""" self._fmt = fmt self._args = args self._kwargs = kwargs - def __str__(self): + def __str__(self) -> str: """Convert the object to a string for logging.""" return str(self._fmt).format(*self._args, **self._kwargs) @@ -27,26 +25,30 @@ class KeywordMessage: class KeywordStyleAdapter(logging.LoggerAdapter): """Represents an adapter wrapping the logger allowing KeywordMessages.""" - def __init__(self, logger, extra=None): + def __init__( + self, logger: logging.Logger, extra: Optional[Mapping[str, Any]] = None + ) -> None: """Initialize a new StyleAdapter for the provided logger.""" super().__init__(logger, extra or {}) - def log(self, level, msg, *args, **kwargs): + def log(self, level: int, msg: Any, *args: Any, **kwargs: Any) -> None: """Log the message provided at the appropriate level.""" if self.isEnabledFor(level): msg, log_kwargs = self.process(msg, kwargs) - self.logger._log( # pylint: disable=protected-access + self.logger._log( # type: ignore # pylint: disable=protected-access level, KeywordMessage(msg, args, kwargs), (), **log_kwargs ) - def process(self, msg, kwargs): + def process( + self, msg: Any, kwargs: MutableMapping[str, Any] + ) -> Tuple[Any, MutableMapping[str, Any]]: """Process the keyward args in preparation for logging.""" return ( msg, { k: kwargs[k] for k in inspect.getfullargspec( - self.logger._log # pylint: disable=protected-access + self.logger._log # type: ignore # pylint: disable=protected-access ).args[1:] if k in kwargs }, diff --git a/homeassistant/helpers/network.py b/homeassistant/helpers/network.py index 671e7f1fa56..a446b575077 100644 --- a/homeassistant/helpers/network.py +++ b/homeassistant/helpers/network.py @@ -1,6 +1,6 @@ """Network helpers.""" -from typing import Optional, cast from ipaddress import ip_address +from typing import Optional, cast import yarl diff --git a/homeassistant/helpers/restore_state.py b/homeassistant/helpers/restore_state.py index 5d47f34b002..4a67193734e 100644 --- a/homeassistant/helpers/restore_state.py +++ b/homeassistant/helpers/restore_state.py @@ -1,24 +1,23 @@ """Support for restoring entity states on startup.""" import asyncio +from datetime import datetime, timedelta import logging -from datetime import timedelta, datetime -from typing import Any, Dict, List, Set, Optional +from typing import Any, Dict, List, Optional, Set +from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP from homeassistant.core import ( - HomeAssistant, - callback, - State, CoreState, + HomeAssistant, + State, + callback, valid_entity_id, ) -from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP -import homeassistant.util.dt as dt_util +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval -from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.json import JSONEncoder from homeassistant.helpers.storage import Store - +import homeassistant.util.dt as dt_util # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs # mypy: no-warn-return-any diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 1e65c24eaaf..8e0faa2ce4d 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -1,16 +1,16 @@ """Helpers to execute scripts.""" import asyncio -import logging from contextlib import suppress from datetime import datetime from itertools import islice -from typing import Optional, Sequence, Callable, Dict, List, Set, Tuple, Any +import logging +from typing import Any, Callable, Dict, List, Optional, Sequence, Set, Tuple import voluptuous as vol +from homeassistant import exceptions import homeassistant.components.device_automation as device_automation import homeassistant.components.scene as scene -from homeassistant.core import HomeAssistant, Context, callback, CALLBACK_TYPE from homeassistant.const import ( ATTR_ENTITY_ID, CONF_CONDITION, @@ -19,21 +19,20 @@ from homeassistant.const import ( CONF_TIMEOUT, SERVICE_TURN_ON, ) -from homeassistant import exceptions +from homeassistant.core import CALLBACK_TYPE, Context, HomeAssistant, callback from homeassistant.helpers import ( - service, condition, - template as template, config_validation as cv, + service, + template as template, ) from homeassistant.helpers.event import ( async_track_point_in_utc_time, async_track_template, ) from homeassistant.helpers.typing import ConfigType -import homeassistant.util.dt as date_util from homeassistant.util.async_ import run_callback_threadsafe - +import homeassistant.util.dt as date_util # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs @@ -231,7 +230,6 @@ class Script: Should only be called on exceptions raised by this scripts async_run. """ - # pylint: disable=protected-access step = self._exception_step action = self.sequence[step] action_type = _determine_action(action) @@ -281,7 +279,6 @@ class Script: @callback def async_script_delay(now): """Handle delay.""" - # pylint: disable=cell-var-from-loop with suppress(ValueError): self._async_listener.remove(unsub) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index e177c86c65c..5381f765993 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -7,7 +7,7 @@ from typing import Callable import voluptuous as vol from homeassistant.auth.permissions.const import CAT_ENTITIES, POLICY_CONTROL -from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, ATTR_AREA_ID +from homeassistant.const import ATTR_AREA_ID, ATTR_ENTITY_ID, ENTITY_MATCH_ALL import homeassistant.core as ha from homeassistant.exceptions import ( HomeAssistantError, @@ -16,12 +16,11 @@ from homeassistant.exceptions import ( UnknownUser, ) from homeassistant.helpers import template, typing +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import HomeAssistantType from homeassistant.loader import async_get_integration, bind_hass from homeassistant.util.yaml import load_yaml from homeassistant.util.yaml.loader import JSON_TYPE -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import HomeAssistantType - # mypy: allow-untyped-defs, no-check-untyped-defs @@ -260,19 +259,7 @@ async def entity_service_call( else: entity_perms = None - # Are we trying to target all entities - if ATTR_ENTITY_ID in call.data: - target_all_entities = call.data[ATTR_ENTITY_ID] == ENTITY_MATCH_ALL - else: - # Remove the service_name parameter along with this warning - _LOGGER.warning( - "Not passing an entity ID to a service to target all " - "entities is deprecated. Update your call to %s to be " - "instead: entity_id: %s", - service_name, - ENTITY_MATCH_ALL, - ) - target_all_entities = True + target_all_entities = call.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_ALL if not target_all_entities: # A set of entities we're trying to target. diff --git a/homeassistant/helpers/signal.py b/homeassistant/helpers/signal.py index 12792918742..53802a2a119 100644 --- a/homeassistant/helpers/signal.py +++ b/homeassistant/helpers/signal.py @@ -4,8 +4,8 @@ import signal import sys from types import FrameType -from homeassistant.core import callback, HomeAssistant from homeassistant.const import RESTART_EXIT_CODE +from homeassistant.core import HomeAssistant, callback from homeassistant.loader import bind_hass _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index abc97bf1f8a..60e6acc8797 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -1,13 +1,11 @@ """Helpers that help with state related things.""" import asyncio +from collections import defaultdict import datetime as dt import logging -from collections import defaultdict from types import ModuleType, TracebackType from typing import Dict, Iterable, List, Optional, Type, Union -from homeassistant.loader import bind_hass, async_get_integration, IntegrationNotFound -import homeassistant.util.dt as dt_util from homeassistant.components.sun import STATE_ABOVE_HORIZON, STATE_BELOW_HORIZON from homeassistant.const import ( STATE_CLOSED, @@ -21,6 +19,9 @@ from homeassistant.const import ( STATE_UNLOCKED, ) from homeassistant.core import Context, State +from homeassistant.loader import IntegrationNotFound, async_get_integration, bind_hass +import homeassistant.util.dt as dt_util + from .typing import HomeAssistantType _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index bd18eebfb25..aed6da37518 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -3,14 +3,13 @@ import asyncio from json import JSONEncoder import logging import os -from typing import Dict, List, Optional, Callable, Union, Any, Type +from typing import Any, Callable, Dict, List, Optional, Type, Union from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.core import HomeAssistant, callback, CALLBACK_TYPE +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.helpers.event import async_call_later from homeassistant.loader import bind_hass from homeassistant.util import json as json_util -from homeassistant.helpers.event import async_call_later - # mypy: allow-untyped-calls, allow-untyped-defs, no-warn-return-any # mypy: no-check-untyped-defs diff --git a/homeassistant/helpers/sun.py b/homeassistant/helpers/sun.py index 9fa6e074bdd..45ff06f16de 100644 --- a/homeassistant/helpers/sun.py +++ b/homeassistant/helpers/sun.py @@ -1,11 +1,12 @@ """Helpers for sun events.""" import datetime -from typing import Optional, Union, TYPE_CHECKING +from typing import TYPE_CHECKING, Optional, Union from homeassistant.const import SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET from homeassistant.core import callback -from homeassistant.util import dt as dt_util from homeassistant.loader import bind_hass +from homeassistant.util import dt as dt_util + from .typing import HomeAssistantType if TYPE_CHECKING: diff --git a/homeassistant/helpers/system_info.py b/homeassistant/helpers/system_info.py index b552a634d4b..7d1d6f2b3e7 100644 --- a/homeassistant/helpers/system_info.py +++ b/homeassistant/helpers/system_info.py @@ -6,6 +6,7 @@ from typing import Dict from homeassistant.const import __version__ as current_version from homeassistant.loader import bind_hass from homeassistant.util.package import is_virtual_env + from .typing import HomeAssistantType diff --git a/homeassistant/helpers/temperature.py b/homeassistant/helpers/temperature.py index 30b428a9e17..e0846d6f893 100644 --- a/homeassistant/helpers/temperature.py +++ b/homeassistant/helpers/temperature.py @@ -2,9 +2,9 @@ from numbers import Number from typing import Optional +from homeassistant.const import PRECISION_HALVES, PRECISION_TENTHS from homeassistant.core import HomeAssistant from homeassistant.util.temperature import convert as convert_temperature -from homeassistant.const import PRECISION_HALVES, PRECISION_TENTHS def display_temp( diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index aa17b2a1fba..b27120e1825 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -1,12 +1,12 @@ """Template helper methods for rendering strings with Home Assistant data.""" import base64 +from datetime import datetime +from functools import wraps import json import logging import math import random import re -from datetime import datetime -from functools import wraps from typing import Any, Dict, Iterable, List, Optional, Union import jinja2 @@ -30,7 +30,6 @@ 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 - # mypy: allow-untyped-calls, allow-untyped-defs # mypy: no-check-untyped-defs, no-warn-return-any @@ -69,7 +68,9 @@ def render_complex(value, variables=None): return [render_complex(item, variables) for item in value] if isinstance(value, dict): return {key: render_complex(item, variables) for key, item in value.items()} - return value.async_render(variables) + if isinstance(value, Template): + return value.async_render(variables) + return value def extract_entities( @@ -142,7 +143,7 @@ class RenderInfo: def result(self) -> str: """Results of the template computation.""" if self._exception is not None: - raise self._exception # pylint: disable=raising-bad-type + raise self._exception return self._result def _freeze(self) -> None: @@ -614,7 +615,7 @@ def distance(hass, *args): if latitude is None or longitude is None: _LOGGER.warning( - "Distance:Unable to process latitude and " "longitude: %s, %s", + "Distance:Unable to process latitude and longitude: %s, %s", value, value_2, ) @@ -669,6 +670,8 @@ def forgiving_round(value, precision=0, method="common"): value = math.ceil(float(value) * multiplier) / multiplier elif method == "floor": value = math.floor(float(value) * multiplier) / multiplier + elif method == "half": + value = round(float(value) * 2) / 2 else: # if method is common or something else, use common rounding value = round(float(value), precision) diff --git a/homeassistant/helpers/translation.py b/homeassistant/helpers/translation.py index b9fd24c95e0..fe254ab8907 100644 --- a/homeassistant/helpers/translation.py +++ b/homeassistant/helpers/translation.py @@ -3,11 +3,12 @@ import logging from typing import Any, Dict, Iterable, Optional from homeassistant.loader import ( + async_get_config_flows, async_get_integration, bind_hass, - async_get_config_flows, ) from homeassistant.util.json import load_json + from .typing import HomeAssistantType _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/helpers/typing.py b/homeassistant/helpers/typing.py index f084c5fddbe..6e31301066c 100644 --- a/homeassistant/helpers/typing.py +++ b/homeassistant/helpers/typing.py @@ -1,5 +1,5 @@ """Typing Helpers for Home Assistant.""" -from typing import Dict, Any, Tuple, Optional +from typing import Any, Dict, Optional, Tuple import homeassistant.core diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 9c8d7d2aecb..a14a5209840 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -13,20 +13,20 @@ import pathlib import sys from types import ModuleType from typing import ( + TYPE_CHECKING, + Any, + Callable, + Dict, + List, Optional, Set, - TYPE_CHECKING, - Callable, - Any, TypeVar, - List, - Dict, Union, cast, ) # Typing imports that create a circular dependency -# pylint: disable=using-constant-test,unused-import +# pylint: disable=unused-import if TYPE_CHECKING: from homeassistant.core import HomeAssistant @@ -195,22 +195,50 @@ class Integration: hass: "HomeAssistant", pkg_path: str, file_path: pathlib.Path, - manifest: Dict, + manifest: Dict[str, Any], ): """Initialize an integration.""" self.hass = hass self.pkg_path = pkg_path self.file_path = file_path - self.name: str = manifest["name"] - self.domain: str = manifest["domain"] - self.dependencies: List[str] = manifest["dependencies"] - self.after_dependencies: Optional[List[str]] = manifest.get( - "after_dependencies" - ) - self.requirements: List[str] = manifest["requirements"] - self.config_flow: bool = manifest.get("config_flow", False) + self.manifest = manifest _LOGGER.info("Loaded %s from %s", self.domain, pkg_path) + @property + def name(self) -> str: + """Return name.""" + return cast(str, self.manifest["name"]) + + @property + def domain(self) -> str: + """Return domain.""" + return cast(str, self.manifest["domain"]) + + @property + def dependencies(self) -> List[str]: + """Return dependencies.""" + return cast(List[str], self.manifest.get("dependencies", [])) + + @property + def after_dependencies(self) -> List[str]: + """Return after_dependencies.""" + return cast(List[str], self.manifest.get("after_dependencies", [])) + + @property + def requirements(self) -> List[str]: + """Return requirements.""" + return cast(List[str], self.manifest.get("requirements", [])) + + @property + def config_flow(self) -> bool: + """Return config_flow.""" + return cast(bool, self.manifest.get("config_flow", False)) + + @property + def documentation(self) -> Optional[str]: + """Return documentation.""" + return cast(str, self.manifest.get("documentation")) + @property def is_built_in(self) -> bool: """Test if package is a built-in integration.""" @@ -379,7 +407,7 @@ def _load_file( if str(err) not in white_listed_errors: _LOGGER.exception( - ("Error loading %s. Make sure all " "dependencies are installed"), + ("Error loading %s. Make sure all dependencies are installed"), path, ) diff --git a/homeassistant/monkey_patch.py b/homeassistant/monkey_patch.py deleted file mode 100644 index c6e2e66ab13..00000000000 --- a/homeassistant/monkey_patch.py +++ /dev/null @@ -1,73 +0,0 @@ -"""Monkey patch Python to work around issues causing segfaults. - -Under heavy threading operations that schedule calls into -the asyncio event loop, Task objects are created. Due to -a bug in Python, GC may have an issue when switching between -the threads and objects with __del__ (which various components -in HASS have). - -This monkey-patch removes the weakref.Weakset, and replaces it -with an object that ignores the only call utilizing it (the -Task.__init__ which calls _all_tasks.add(self)). It also removes -the __del__ which could trigger the future objects __del__ at -unpredictable times. - -The side-effect of this manipulation of the Task is that -Task.all_tasks() is no longer accurate, and there will be no -warning emitted if a Task is GC'd while in use. - -Related Python bugs: - - https://bugs.python.org/issue26617 -""" -import sys -from typing import Any - - -def patch_weakref_tasks() -> None: - """Replace weakref.WeakSet to address Python 3 bug.""" - # pylint: disable=no-self-use, protected-access - import asyncio.tasks - - class IgnoreCalls: - """Ignore add calls.""" - - def add(self, other: Any) -> None: - """No-op add.""" - return - - asyncio.tasks.Task._all_tasks = IgnoreCalls() # type: ignore - try: - del asyncio.tasks.Task.__del__ - except: # noqa: E722 pylint: disable=bare-except - pass - - -def disable_c_asyncio() -> None: - """Disable using C implementation of asyncio. - - Required to be able to apply the weakref monkey patch. - - Requires Python 3.6+. - """ - - class AsyncioImportFinder: - """Finder that blocks C version of asyncio being loaded.""" - - PATH_TRIGGER = "_asyncio" - - def __init__(self, path_entry: str) -> None: - if path_entry != self.PATH_TRIGGER: - raise ImportError() - - def find_module(self, fullname: str, path: Any = None) -> None: - """Find a module.""" - if fullname == self.PATH_TRIGGER: - raise ModuleNotFoundError() - - sys.path_hooks.append(AsyncioImportFinder) - sys.path.insert(0, AsyncioImportFinder.PATH_TRIGGER) - - try: - import _asyncio # noqa: F401 - except ImportError: - pass diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 72605a3475e..8347567246e 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -6,29 +6,29 @@ astral==1.10.1 async_timeout==3.0.1 attrs==19.3.0 bcrypt==3.1.7 -certifi>=2019.9.11 -contextvars==2.4;python_version<"3.7" +certifi>=2019.11.28 cryptography==2.8 +defusedxml==0.6.0 distro==1.4.0 -hass-nabucasa==0.29 -home-assistant-frontend==20191115.0 -importlib-metadata==0.23 +hass-nabucasa==0.30 +home-assistant-frontend==20191204.1 +importlib-metadata==1.3.0 jinja2>=2.10.3 netdisco==2.6.0 pip>=8.0.3 python-slugify==4.0.0 pytz>=2019.03 -pyyaml==5.1.2 +pyyaml==5.2.0 requests==2.22.0 ruamel.yaml==0.15.100 -sqlalchemy==1.3.11 +sqlalchemy==1.3.12 voluptuous-serialize==2.3.0 voluptuous==0.11.7 -zeroconf==0.23.0 +zeroconf==0.24.4 pycryptodome>=3.6.6 -# Breaks Python 3.6 and is not needed for our supported Python versions +# Not needed for our supported Python versions enum34==1000000000.0.0 # This is a old unmaintained library and is replaced with pycryptodome diff --git a/homeassistant/requirements.py b/homeassistant/requirements.py index a0eec0f442b..7b2d3fe9bc3 100644 --- a/homeassistant/requirements.py +++ b/homeassistant/requirements.py @@ -1,20 +1,24 @@ """Module to handle installing requirements.""" import asyncio -from pathlib import Path import logging import os -from typing import Any, Dict, List, Optional +from pathlib import Path +from typing import Any, Dict, Iterable, List, Optional, Set -from homeassistant.exceptions import HomeAssistantError -import homeassistant.util.package as pkg_util from homeassistant.core import HomeAssistant -from homeassistant.loader import async_get_integration, Integration +from homeassistant.exceptions import HomeAssistantError +from homeassistant.loader import Integration, async_get_integration +import homeassistant.util.package as pkg_util DATA_PIP_LOCK = "pip_lock" DATA_PKG_CACHE = "pkg_cache" CONSTRAINT_FILE = "package_constraints.txt" PROGRESS_FILE = ".pip_progress" _LOGGER = logging.getLogger(__name__) +DISCOVERY_INTEGRATIONS: Dict[str, Iterable[str]] = { + "ssdp": ("ssdp",), + "zeroconf": ("zeroconf", "homekit"), +} class RequirementsNotFound(HomeAssistantError): @@ -28,16 +32,19 @@ class RequirementsNotFound(HomeAssistantError): async def async_get_integration_with_requirements( - hass: HomeAssistant, domain: str + hass: HomeAssistant, domain: str, done: Set[str] = None ) -> Integration: - """Get an integration with installed requirements. + """Get an integration with all requirements installed, including the dependencies. This can raise IntegrationNotFound if manifest or integration is invalid, RequirementNotFound if there was some type of failure to install requirements. - - Does not handle circular dependencies. """ + if done is None: + done = {domain} + else: + done.add(domain) + integration = await async_get_integration(hass, domain) if hass.config.skip_pip: @@ -48,11 +55,26 @@ async def async_get_integration_with_requirements( hass, integration.domain, integration.requirements ) - deps = integration.dependencies + (integration.after_dependencies or []) + deps_to_check = [ + dep + for dep in integration.dependencies + integration.after_dependencies + if dep not in done + ] - if deps: + for check_domain, to_check in DISCOVERY_INTEGRATIONS.items(): + if ( + check_domain not in done + and check_domain not in deps_to_check + and any(check in integration.manifest for check in to_check) + ): + deps_to_check.append(check_domain) + + if deps_to_check: await asyncio.gather( - *[async_get_integration_with_requirements(hass, dep) for dep in deps] + *[ + async_get_integration_with_requirements(hass, dep, done) + for dep in deps_to_check + ] ) return integration diff --git a/homeassistant/scripts/__init__.py b/homeassistant/scripts/__init__.py index ecac61895c5..b47450ab9dd 100644 --- a/homeassistant/scripts/__init__.py +++ b/homeassistant/scripts/__init__.py @@ -10,8 +10,7 @@ from typing import List, Optional, Sequence, Text from homeassistant.bootstrap import async_mount_local_lib_path from homeassistant.config import get_default_config_dir from homeassistant.requirements import pip_kwargs -from homeassistant.util.package import install_package, is_virtual_env, is_installed - +from homeassistant.util.package import install_package, is_installed, is_virtual_env # mypy: allow-untyped-defs, no-warn-return-any diff --git a/homeassistant/scripts/auth.py b/homeassistant/scripts/auth.py index e0a8b5cf117..66baf555306 100644 --- a/homeassistant/scripts/auth.py +++ b/homeassistant/scripts/auth.py @@ -6,9 +6,8 @@ import os from homeassistant.auth import auth_manager_from_config from homeassistant.auth.providers import homeassistant as hass_auth -from homeassistant.core import HomeAssistant from homeassistant.config import get_default_config_dir - +from homeassistant.core import HomeAssistant # mypy: allow-untyped-calls, allow-untyped-defs diff --git a/homeassistant/scripts/benchmark/__init__.py b/homeassistant/scripts/benchmark/__init__.py index 480f6fc9fde..58125bc4829 100644 --- a/homeassistant/scripts/benchmark/__init__.py +++ b/homeassistant/scripts/benchmark/__init__.py @@ -11,7 +11,6 @@ from homeassistant import core from homeassistant.const import ATTR_NOW, EVENT_STATE_CHANGED, EVENT_TIME_CHANGED from homeassistant.util import dt as dt_util - # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs # mypy: no-warn-return-any diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index 3ac023115a1..46724fc7c10 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -1,19 +1,18 @@ """Script to check the configuration file.""" import argparse -import logging -import os from collections import OrderedDict from glob import glob -from typing import Dict, List, Sequence, Any, Tuple, Callable +import logging +import os +from typing import Any, Callable, Dict, List, Sequence, Tuple from unittest.mock import patch from homeassistant import bootstrap, 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 import homeassistant.util.yaml.loader as yaml_loader -from homeassistant.exceptions import HomeAssistantError - # mypy: allow-untyped-calls, allow-untyped-defs diff --git a/homeassistant/scripts/credstash.py b/homeassistant/scripts/credstash.py index e77ae326cd7..f90ab5f793e 100644 --- a/homeassistant/scripts/credstash.py +++ b/homeassistant/scripts/credstash.py @@ -4,7 +4,6 @@ import getpass from homeassistant.util.yaml import _SECRET_NAMESPACE - # mypy: allow-untyped-defs REQUIREMENTS = ["credstash==1.15.0"] diff --git a/homeassistant/scripts/ensure_config.py b/homeassistant/scripts/ensure_config.py index c5cf69283e6..0b5d1104997 100644 --- a/homeassistant/scripts/ensure_config.py +++ b/homeassistant/scripts/ensure_config.py @@ -2,9 +2,8 @@ import argparse import os -from homeassistant.core import HomeAssistant import homeassistant.config as config_util - +from homeassistant.core import HomeAssistant # mypy: allow-untyped-calls, allow-untyped-defs @@ -12,9 +11,7 @@ import homeassistant.config as config_util def run(args): """Handle ensure config commandline script.""" parser = argparse.ArgumentParser( - description=( - "Ensure a Home Assistant config exists, " "creates one if necessary." - ) + description=("Ensure a Home Assistant config exists, creates one if necessary.") ) parser.add_argument( "-c", diff --git a/homeassistant/scripts/keyring.py b/homeassistant/scripts/keyring.py index 0c5623a50ad..594d897ee4c 100644 --- a/homeassistant/scripts/keyring.py +++ b/homeassistant/scripts/keyring.py @@ -5,10 +5,8 @@ import os from homeassistant.util.yaml import _SECRET_NAMESPACE - # mypy: allow-untyped-defs - -REQUIREMENTS = ["keyring==19.2.0", "keyrings.alt==3.1.1"] +REQUIREMENTS = ["keyring==20.0.0", "keyrings.alt==3.4.0"] def run(args): diff --git a/homeassistant/scripts/macos/__init__.py b/homeassistant/scripts/macos/__init__.py index ceb3609dbdb..9d9c5cd8248 100644 --- a/homeassistant/scripts/macos/__init__.py +++ b/homeassistant/scripts/macos/__init__.py @@ -2,7 +2,6 @@ import os import time - # mypy: allow-untyped-calls, allow-untyped-defs diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 314938feeed..f0d1e492b99 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -2,16 +2,14 @@ import asyncio import logging.handlers from timeit import default_timer as timer - from types import ModuleType -from typing import Awaitable, Callable, Optional, Dict, List +from typing import Awaitable, Callable, Dict, List, Optional -from homeassistant import requirements, core, loader, config as conf_util +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 - _LOGGER = logging.getLogger(__name__) ATTR_COMPONENT = "component" @@ -77,7 +75,7 @@ async def _async_process_dependencies( if failed: _LOGGER.error( - "Unable to set up dependencies of %s. " "Setup failed for dependencies: %s", + "Unable to set up dependencies of %s. Setup failed for dependencies: %s", name, ", ".join(failed), ) @@ -94,7 +92,7 @@ async def _async_setup_component( This method is a coroutine. """ - def log_error(msg: str, link: bool = True) -> None: + def log_error(msg: str, link: Optional[str] = None) -> None: """Log helper.""" _LOGGER.error("Setup failed for %s: %s", domain, msg) async_notify_setup_error(hass, domain, link) @@ -102,7 +100,7 @@ async def _async_setup_component( try: integration = await loader.async_get_integration(hass, domain) except loader.IntegrationNotFound: - log_error("Integration not found.", False) + log_error("Integration not found.") return False # Validate all dependencies exist and there are no circular dependencies @@ -110,14 +108,14 @@ async def _async_setup_component( await loader.async_component_dependencies(hass, domain) except loader.IntegrationNotFound as err: _LOGGER.error( - "Not setting up %s because we are unable to resolve " "(sub)dependency %s", + "Not setting up %s because we are unable to resolve (sub)dependency %s", domain, err.domain, ) return False except loader.CircularDependency as err: _LOGGER.error( - "Not setting up %s because it contains a circular dependency: " "%s -> %s", + "Not setting up %s because it contains a circular dependency: %s -> %s", domain, err.from_domain, err.to_domain, @@ -129,7 +127,7 @@ async def _async_setup_component( try: await async_process_deps_reqs(hass, config, integration) except HomeAssistantError as err: - log_error(str(err)) + log_error(str(err), integration.documentation) return False # Some integrations fail on import because they call functions incorrectly. @@ -137,7 +135,7 @@ async def _async_setup_component( try: component = integration.get_component() except ImportError: - log_error("Unable to import component", False) + log_error("Unable to import component", integration.documentation) return False except Exception: # pylint: disable=broad-except _LOGGER.exception("Setup failed for %s: unknown error", domain) @@ -148,7 +146,7 @@ async def _async_setup_component( ) if processed_config is None: - log_error("Invalid config.") + log_error("Invalid config.", integration.documentation) return False start = timer() @@ -180,7 +178,7 @@ async def _async_setup_component( return False except Exception: # pylint: disable=broad-except _LOGGER.exception("Error during setup of component %s", domain) - async_notify_setup_error(hass, domain, True) + async_notify_setup_error(hass, domain, integration.documentation) return False finally: end = timer() @@ -288,8 +286,8 @@ async def async_process_deps_reqs( raise HomeAssistantError("Could not set up all dependencies.") if not hass.config.skip_pip and integration.requirements: - await requirements.async_process_requirements( - hass, integration.domain, integration.requirements + await requirements.async_get_integration_with_requirements( + hass, integration.domain ) processed.add(integration.domain) diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py index 408d1e370d4..f39fa5f1e55 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -1,23 +1,23 @@ """Helper methods for various modules.""" import asyncio from datetime import datetime, timedelta -import threading -import re import enum -import socket -import random -import string from functools import wraps +import random +import re +import socket +import string +import threading from types import MappingProxyType from typing import ( Any, + Callable, + Coroutine, + Iterable, + KeysView, Optional, TypeVar, - Callable, - KeysView, Union, - Iterable, - Coroutine, ) import slugify as unicode_slug diff --git a/homeassistant/util/aiohttp.py b/homeassistant/util/aiohttp.py index 1e36d2d4875..69911986f57 100644 --- a/homeassistant/util/aiohttp.py +++ b/homeassistant/util/aiohttp.py @@ -1,7 +1,7 @@ """Utilities to help with aiohttp.""" import json -from urllib.parse import parse_qsl from typing import Any, Dict, Optional +from urllib.parse import parse_qsl from multidict import CIMultiDict, MultiDict diff --git a/homeassistant/util/async_.py b/homeassistant/util/async_.py index 64bedfe2501..212c2bff910 100644 --- a/homeassistant/util/async_.py +++ b/homeassistant/util/async_.py @@ -1,35 +1,14 @@ """Asyncio backports for Python 3.6 compatibility.""" -import concurrent.futures -import threading -import logging -from asyncio import coroutines +from asyncio import coroutines, ensure_future from asyncio.events import AbstractEventLoop - -import asyncio -from asyncio import ensure_future -from typing import Any, Coroutine, Callable, TypeVar, Awaitable +import concurrent.futures +import logging +import threading +from typing import Any, Callable, Coroutine _LOGGER = logging.getLogger(__name__) -try: - # pylint: disable=invalid-name - asyncio_run = asyncio.run # type: ignore -except AttributeError: - _T = TypeVar("_T") - - def asyncio_run(main: Awaitable[_T], *, debug: bool = False) -> _T: - """Minimal re-implementation of asyncio.run (since 3.7).""" - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - loop.set_debug(debug) - try: - return loop.run_until_complete(main) - finally: - asyncio.set_event_loop(None) - loop.close() - - def fire_coroutine_threadsafe(coro: Coroutine, loop: AbstractEventLoop) -> None: """Submit a coroutine object to a given event loop. diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py index 837a0e77cd7..b56ecbbaa89 100644 --- a/homeassistant/util/color.py +++ b/homeassistant/util/color.py @@ -1,8 +1,8 @@ """Color util methods.""" -import math import colorsys +import math +from typing import List, Optional, Tuple -from typing import Tuple, List, Optional import attr # Official CSS3 colors from w3.org: @@ -107,7 +107,7 @@ COLORS = { "mediumslateblue": (123, 104, 238), "mediumspringgreen": (0, 250, 154), "mediumturquoise": (72, 209, 204), - "mediumvioletredred": (199, 21, 133), + "mediumvioletred": (199, 21, 133), "midnightblue": (25, 25, 112), "mintcream": (245, 255, 250), "mistyrose": (255, 228, 225), diff --git a/homeassistant/util/distance.py b/homeassistant/util/distance.py index b9cce45cb5b..4fdc40bde2f 100644 --- a/homeassistant/util/distance.py +++ b/homeassistant/util/distance.py @@ -4,12 +4,12 @@ import logging from numbers import Number from homeassistant.const import ( - LENGTH_KILOMETERS, - LENGTH_MILES, - LENGTH_FEET, - LENGTH_METERS, - UNIT_NOT_RECOGNIZED_TEMPLATE, LENGTH, + LENGTH_FEET, + LENGTH_KILOMETERS, + LENGTH_METERS, + LENGTH_MILES, + UNIT_NOT_RECOGNIZED_TEMPLATE, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index 1abb4294398..49f9d7d5f99 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -1,7 +1,7 @@ """Helper methods to handle the time in Home Assistant.""" import datetime as dt import re -from typing import Any, Union, Optional, Tuple, List, cast, Dict +from typing import Any, Dict, List, Optional, Tuple, Union, cast import pytz import pytz.exceptions as pytzexceptions @@ -253,7 +253,7 @@ def find_next_time_expression_time( including daylight saving time. """ if not seconds or not minutes or not hours: - raise ValueError("Cannot find a next time: Time expression never " "matches!") + raise ValueError("Cannot find a next time: Time expression never matches!") def _lower_bound(arr: List[int], cmp: int) -> Optional[int]: """Return the first value in arr greater or equal to cmp. diff --git a/homeassistant/util/json.py b/homeassistant/util/json.py index 5b2ee316376..e975c878672 100644 --- a/homeassistant/util/json.py +++ b/homeassistant/util/json.py @@ -1,10 +1,9 @@ """JSON utility functions.""" -import logging -from typing import Union, List, Dict, Optional, Type - import json +import logging import os import tempfile +from typing import Dict, List, Optional, Type, Union from homeassistant.exceptions import HomeAssistantError diff --git a/homeassistant/util/location.py b/homeassistant/util/location.py index b572b3025a0..a617eba50f9 100644 --- a/homeassistant/util/location.py +++ b/homeassistant/util/location.py @@ -6,7 +6,7 @@ detect_location_info and elevation are mocked by default during tests. import asyncio import collections import math -from typing import Any, Optional, Tuple, Dict +from typing import Any, Dict, Optional, Tuple import aiohttp diff --git a/homeassistant/util/package.py b/homeassistant/util/package.py index 58a3db31bd3..24cf8309228 100644 --- a/homeassistant/util/package.py +++ b/homeassistant/util/package.py @@ -2,15 +2,14 @@ import asyncio import logging import os +from pathlib import Path from subprocess import PIPE, Popen import sys from typing import Optional from urllib.parse import urlparse -from pathlib import Path +from importlib_metadata import PackageNotFoundError, version import pkg_resources -from importlib_metadata import version, PackageNotFoundError - _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/util/pil.py b/homeassistant/util/pil.py new file mode 100644 index 00000000000..80c11c9c410 --- /dev/null +++ b/homeassistant/util/pil.py @@ -0,0 +1,47 @@ +"""PIL utilities. + +Can only be used by integrations that have pillow in their requirements. +""" +from typing import Tuple + +from PIL import ImageDraw + + +def draw_box( + draw: ImageDraw, + box: Tuple[float, float, float, float], + img_width: int, + img_height: int, + text: str = "", + color: Tuple[int, int, int] = (255, 255, 0), +) -> None: + """ + Draw a bounding box on and image. + + The bounding box is defined by the tuple (y_min, x_min, y_max, x_max) + where the coordinates are floats in the range [0.0, 1.0] and + relative to the width and height of the image. + + For example, if an image is 100 x 200 pixels (height x width) and the bounding + box is `(0.1, 0.2, 0.5, 0.9)`, the upper-left and bottom-right coordinates of + the bounding box will be `(40, 10)` to `(180, 50)` (in (x,y) coordinates). + """ + + line_width = 3 + font_height = 8 + y_min, x_min, y_max, x_max = box + (left, right, top, bottom) = ( + x_min * img_width, + x_max * img_width, + y_min * img_height, + y_max * img_height, + ) + draw.line( + [(left, top), (left, bottom), (right, bottom), (right, top), (left, top)], + width=line_width, + fill=color, + ) + if text: + draw.text( + (left + line_width, abs(top - line_width - font_height)), text, fill=color + ) diff --git a/homeassistant/util/pressure.py b/homeassistant/util/pressure.py index e394076800c..df791fd0235 100644 --- a/homeassistant/util/pressure.py +++ b/homeassistant/util/pressure.py @@ -4,13 +4,13 @@ import logging from numbers import Number from homeassistant.const import ( - PRESSURE_PA, + PRESSURE, PRESSURE_HPA, - PRESSURE_MBAR, PRESSURE_INHG, + PRESSURE_MBAR, + PRESSURE_PA, PRESSURE_PSI, UNIT_NOT_RECOGNIZED_TEMPLATE, - PRESSURE, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/util/ruamel_yaml.py b/homeassistant/util/ruamel_yaml.py index b7e8927888c..e5ffbb94afe 100644 --- a/homeassistant/util/ruamel_yaml.py +++ b/homeassistant/util/ruamel_yaml.py @@ -1,18 +1,18 @@ """ruamel.yaml utility functions.""" +from collections import OrderedDict import logging import os from os import O_CREAT, O_TRUNC, O_WRONLY, stat_result -from collections import OrderedDict -from typing import Union, List, Dict, Optional +from typing import Dict, List, Optional, Union import ruamel.yaml from ruamel.yaml import YAML +from ruamel.yaml.compat import StringIO from ruamel.yaml.constructor import SafeConstructor from ruamel.yaml.error import YAMLError -from ruamel.yaml.compat import StringIO -from homeassistant.util.yaml import secret_yaml from homeassistant.exceptions import HomeAssistantError +from homeassistant.util.yaml import secret_yaml _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/util/temperature.py b/homeassistant/util/temperature.py index 909cdac60d9..0b3edc6ef57 100644 --- a/homeassistant/util/temperature.py +++ b/homeassistant/util/temperature.py @@ -2,8 +2,8 @@ from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, - UNIT_NOT_RECOGNIZED_TEMPLATE, TEMPERATURE, + UNIT_NOT_RECOGNIZED_TEMPLATE, ) diff --git a/homeassistant/util/unit_system.py b/homeassistant/util/unit_system.py index 23ac8f05025..a79c022be45 100644 --- a/homeassistant/util/unit_system.py +++ b/homeassistant/util/unit_system.py @@ -1,35 +1,37 @@ """Unit system helper class and methods.""" import logging -from typing import Optional from numbers import Number +from typing import Optional from homeassistant.const import ( - TEMP_CELSIUS, - TEMP_FAHRENHEIT, - LENGTH_MILES, + CONF_UNIT_SYSTEM_IMPERIAL, + CONF_UNIT_SYSTEM_METRIC, + LENGTH, LENGTH_KILOMETERS, - PRESSURE_PA, - PRESSURE_PSI, - VOLUME_LITERS, - VOLUME_GALLONS, + LENGTH_MILES, + MASS, MASS_GRAMS, MASS_KILOGRAMS, MASS_OUNCES, MASS_POUNDS, - CONF_UNIT_SYSTEM_METRIC, - CONF_UNIT_SYSTEM_IMPERIAL, - LENGTH, - MASS, PRESSURE, - VOLUME, + PRESSURE_PA, + PRESSURE_PSI, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, TEMPERATURE, UNIT_NOT_RECOGNIZED_TEMPLATE, + VOLUME, + VOLUME_GALLONS, + VOLUME_LITERS, +) +from homeassistant.util import ( + distance as distance_util, + pressure as pressure_util, + temperature as temperature_util, + volume as volume_util, ) -from homeassistant.util import temperature as temperature_util -from homeassistant.util import distance as distance_util -from homeassistant.util import pressure as pressure_util -from homeassistant.util import volume as volume_util _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/util/volume.py b/homeassistant/util/volume.py index 5a05b663522..2e033beb35c 100644 --- a/homeassistant/util/volume.py +++ b/homeassistant/util/volume.py @@ -2,13 +2,14 @@ import logging from numbers import Number + from homeassistant.const import ( + UNIT_NOT_RECOGNIZED_TEMPLATE, + VOLUME, + VOLUME_FLUID_OUNCE, + VOLUME_GALLONS, VOLUME_LITERS, VOLUME_MILLILITERS, - VOLUME_GALLONS, - VOLUME_FLUID_OUNCE, - VOLUME, - UNIT_NOT_RECOGNIZED_TEMPLATE, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/util/yaml/__init__.py b/homeassistant/util/yaml/__init__.py index 9cf33a401d3..106bdbf8ef5 100644 --- a/homeassistant/util/yaml/__init__.py +++ b/homeassistant/util/yaml/__init__.py @@ -1,9 +1,8 @@ """YAML utility functions.""" -from .const import SECRET_YAML, _SECRET_NAMESPACE +from .const import _SECRET_NAMESPACE, SECRET_YAML from .dumper import dump, save_yaml from .loader import clear_secret_cache, load_yaml, secret_yaml - __all__ = [ "SECRET_YAML", "_SECRET_NAMESPACE", diff --git a/homeassistant/util/yaml/dumper.py b/homeassistant/util/yaml/dumper.py index a53dc0cdd02..ffcd4917363 100644 --- a/homeassistant/util/yaml/dumper.py +++ b/homeassistant/util/yaml/dumper.py @@ -1,10 +1,10 @@ """Custom dumper and representers.""" from collections import OrderedDict + import yaml from .objects import NodeListClass - # mypy: allow-untyped-calls, no-warn-return-any diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py index 47635cd66c3..6b921ade961 100644 --- a/homeassistant/util/yaml/loader.py +++ b/homeassistant/util/yaml/loader.py @@ -1,13 +1,18 @@ """Custom loader.""" +from collections import OrderedDict +import fnmatch import logging import os import sys -import fnmatch -from collections import OrderedDict -from typing import Union, List, Dict, Iterator, overload, TypeVar +from typing import Dict, Iterator, List, TypeVar, Union, overload import yaml +from homeassistant.exceptions import HomeAssistantError + +from .const import _SECRET_NAMESPACE, SECRET_YAML +from .objects import NodeListClass, NodeStrClass + try: import keyring except ImportError: @@ -18,11 +23,6 @@ try: except ImportError: credstash = None -from homeassistant.exceptions import HomeAssistantError - -from .const import _SECRET_NAMESPACE, SECRET_YAML -from .objects import NodeListClass, NodeStrClass - # mypy: allow-untyped-calls, no-warn-return-any @@ -68,7 +68,6 @@ def load_yaml(fname: str) -> JSON_TYPE: raise HomeAssistantError(exc) -# pylint: disable=pointless-statement @overload def _add_reference( obj: Union[list, NodeListClass], loader: yaml.SafeLoader, node: yaml.nodes.Node @@ -211,7 +210,7 @@ def _ordered_dict(loader: SafeLineLoader, node: yaml.nodes.MappingNode) -> Order if key in seen: fname = getattr(loader.stream, "name", "") - _LOGGER.error( + _LOGGER.warning( 'YAML file %s contains duplicate key "%s". ' "Check lines %d and %d.", fname, key, @@ -259,7 +258,7 @@ def _load_secret_yaml(secret_path: str) -> JSON_TYPE: _LOGGER.setLevel(logging.DEBUG) else: _LOGGER.error( - "secrets.yaml: 'logger: debug' expected," " but 'logger: %s' found", + "secrets.yaml: 'logger: debug' expected, but 'logger: %s' found", logger, ) del secrets["logger"] @@ -277,7 +276,7 @@ def secret_yaml(loader: SafeLineLoader, node: yaml.nodes.Node) -> JSON_TYPE: if node.value in secrets: _LOGGER.debug( - "Secret %s retrieved from secrets.yaml in " "folder %s", + "Secret %s retrieved from secrets.yaml in folder %s", node.value, secret_path, ) diff --git a/pylintrc b/pylintrc index 7794f61e500..0ffbb138f9e 100644 --- a/pylintrc +++ b/pylintrc @@ -24,6 +24,8 @@ good-names=id,i,j,k,ex,Run,_,fp # inconsistent-return-statements - doesn't handle raise # unnecessary-pass - readability for functions which only contain pass # import-outside-toplevel - TODO +# too-many-ancestors - it's too strict. +# wrong-import-order - isort guards this disable= format, abstract-class-little-used, @@ -37,6 +39,7 @@ disable= not-context-manager, redefined-variable-type, too-few-public-methods, + too-many-ancestors, too-many-arguments, too-many-branches, too-many-instance-attributes, @@ -47,7 +50,10 @@ disable= too-many-statements, too-many-boolean-expressions, unnecessary-pass, - unused-argument + unused-argument, + wrong-import-order +enable= + use-symbolic-message-instead [REPORTS] score=no @@ -60,4 +66,4 @@ ignored-classes=_CountingAttr expected-line-ending-format=LF [EXCEPTIONS] -overgeneral-exceptions=Exception,HomeAssistantError +overgeneral-exceptions=BaseException,Exception,HomeAssistantError diff --git a/pyproject.toml b/pyproject.toml index 7a75060c8e9..7c0c5eeb433 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,3 @@ [tool.black] -target-version = ["py36", "py37", "py38"] +target-version = ["py37", "py38"] exclude = 'generated' diff --git a/requirements_all.txt b/requirements_all.txt index e4fa1aa04d8..d2ed4e0e1e2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -4,16 +4,15 @@ astral==1.10.1 async_timeout==3.0.1 attrs==19.3.0 bcrypt==3.1.7 -certifi>=2019.9.11 -contextvars==2.4;python_version<"3.7" -importlib-metadata==0.23 +certifi>=2019.11.28 +importlib-metadata==1.3.0 jinja2>=2.10.3 PyJWT==1.7.1 cryptography==2.8 pip>=8.0.3 python-slugify==4.0.0 pytz>=2019.03 -pyyaml==5.1.2 +pyyaml==5.2.0 requests==2.22.0 ruamel.yaml==0.15.100 voluptuous==0.11.7 @@ -100,7 +99,7 @@ TwitterAPI==2.5.10 # VL53L1X2==0.1.5 # homeassistant.components.waze_travel_time -WazeRouteCalculator==0.10 +WazeRouteCalculator==0.12 # homeassistant.components.yessssms YesssSMS==0.4.1 @@ -115,7 +114,7 @@ adafruit-blinka==1.2.1 adafruit-circuitpython-mcp230xx==1.1.2 # homeassistant.components.androidtv -adb-shell==0.0.8 +adb-shell==0.1.0 # homeassistant.components.adguard adguardhome==0.3.0 @@ -126,8 +125,14 @@ afsapi==0.0.4 # homeassistant.components.geonetnz_quakes aio_geojson_geonetnz_quakes==0.11 +# homeassistant.components.geonetnz_volcano +aio_geojson_geonetnz_volcano==0.5 + +# homeassistant.components.nsw_rural_fire_service_feed +aio_geojson_nsw_rfs_incidents==0.1 + # homeassistant.components.ambient_station -aioambient==0.3.2 +aioambient==1.0.2 # homeassistant.components.asuswrt aioasuswrt==1.1.22 @@ -142,7 +147,7 @@ aiobotocore==0.10.4 aiodns==2.0.0 # homeassistant.components.esphome -aioesphomeapi==2.5.0 +aioesphomeapi==2.6.1 # homeassistant.components.freebox aiofreepybox==0.0.8 @@ -158,7 +163,7 @@ aioharmony==0.1.13 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==1.9.2 +aiohue==1.10.1 # homeassistant.components.imap aioimaplib==0.7.15 @@ -181,6 +186,9 @@ aionotion==1.1.0 # homeassistant.components.hunterdouglas_powerview aiopvapi==1.6.14 +# homeassistant.components.webostv +aiopylgtv==0.2.4 + # homeassistant.components.switcher_kis aioswitcher==2019.4.26 @@ -197,7 +205,7 @@ airly==0.0.2 aladdin_connect==0.3 # homeassistant.components.alarmdecoder -alarmdecoder==1.13.2 +alarmdecoder==1.13.9 # homeassistant.components.alpha_vantage alpha_vantage==2.1.2 @@ -209,7 +217,7 @@ ambiclimate==0.2.1 amcrest==1.5.3 # homeassistant.components.androidtv -androidtv==0.0.34 +androidtv==0.0.36 # homeassistant.components.anel_pwrctrl anel_pwrctrl-homeassistant==0.0.1.dev2 @@ -224,7 +232,7 @@ apcaccess==0.0.13 apns2==0.3.0 # homeassistant.components.apprise -apprise==0.8.1 +apprise==0.8.2 # homeassistant.components.aprs aprslib==0.6.46 @@ -245,6 +253,9 @@ asterisk_mbox==0.5.0 # homeassistant.components.upnp async-upnp-client==0.14.12 +# homeassistant.components.aten_pe +atenpdu==0.3.0 + # homeassistant.components.aurora_abb_powerone aurorapy==0.2.6 @@ -279,13 +290,13 @@ batinfo==0.4.2 # beacontools[scan]==1.2.3 # homeassistant.components.scrape -beautifulsoup4==4.8.1 +beautifulsoup4==4.8.2 # homeassistant.components.beewi_smartclim beewi_smartclim==0.0.7 # homeassistant.components.zha -bellows-homeassistant==0.11.0 +bellows-homeassistant==0.12.0 # homeassistant.components.bmw_connected_drive bimmer_connected==0.6.2 @@ -391,7 +402,7 @@ crimereports==1.0.1 datadog==0.15.0 # homeassistant.components.metoffice -datapoint==0.4.3 +datapoint==0.9.5 # homeassistant.components.decora # decora==0.6 @@ -402,6 +413,7 @@ datapoint==0.4.3 # homeassistant.components.ihc # homeassistant.components.namecheapdns # homeassistant.components.ohmconnect +# homeassistant.components.ssdp defusedxml==0.6.0 # homeassistant.components.deluge @@ -417,7 +429,7 @@ directpy==0.5 discogs_client==2.2.2 # homeassistant.components.discord -discord.py==1.2.4 +discord.py==1.2.5 # homeassistant.components.updater distro==1.4.0 @@ -449,6 +461,9 @@ ecoaliface==0.4.0 # homeassistant.components.ee_brightbox eebrightbox==0.0.4 +# homeassistant.components.elgato +elgato==0.1.0 + # homeassistant.components.eliqonline eliqonline==1.2.2 @@ -462,7 +477,7 @@ emulated_roku==0.1.8 enocean==0.50 # homeassistant.components.entur_public_transport -enturclient==0.2.0 +enturclient==0.2.1 # homeassistant.components.environment_canada env_canada==0.0.30 @@ -471,7 +486,7 @@ env_canada==0.0.30 # envirophat==0.0.6 # homeassistant.components.enphase_envoy -envoy_reader==0.8.6 +envoy_reader==0.11.0 # homeassistant.components.season ephem==3.7.7.0 @@ -483,7 +498,7 @@ epson-projector==0.1.3 epsonprinter==0.0.9 # homeassistant.components.netgear_lte -eternalegypt==0.0.10 +eternalegypt==0.0.11 # homeassistant.components.keyboard_remote # evdev==1.1.2 @@ -546,7 +561,6 @@ geizhals==0.0.9 geniushub-client==0.6.30 # homeassistant.components.geo_json_events -# homeassistant.components.nsw_rural_fire_service_feed # homeassistant.components.usgs_earthquakes_feed geojson_client==0.4 @@ -554,7 +568,7 @@ geojson_client==0.4 geopy==1.19.0 # homeassistant.components.geo_rss_events -georss_generic_client==0.2 +georss_generic_client==0.3 # homeassistant.components.ign_sismologia georss_ign_sismologia_client==0.2 @@ -567,6 +581,9 @@ georss_qld_bushfire_alert_client==0.3 # homeassistant.components.nmap_tracker getmac==0.8.1 +# homeassistant.components.gios +gios==0.0.3 + # homeassistant.components.gitter gitterpy==0.1.7 @@ -625,7 +642,7 @@ habitipy==0.2.0 hangups==0.4.9 # homeassistant.components.cloud -hass-nabucasa==0.29 +hass-nabucasa==0.30 # homeassistant.components.mqtt hbmqtt==0.9.5 @@ -634,10 +651,10 @@ hbmqtt==0.9.5 hdate==0.9.3 # homeassistant.components.heatmiser -heatmiserV3==0.9.1 +heatmiserV3==1.1.18 # homeassistant.components.here_travel_time -herepy==0.6.3.1 +herepy==2.0.0 # homeassistant.components.hikvisioncam hikvision==0.4 @@ -652,19 +669,19 @@ hlk-sw16==0.0.7 hole==0.5.0 # homeassistant.components.workday -holidays==0.9.11 +holidays==0.9.12 # homeassistant.components.frontend -home-assistant-frontend==20191115.0 +home-assistant-frontend==20191204.1 # homeassistant.components.zwave -homeassistant-pyozw==0.1.4 +homeassistant-pyozw==0.1.7 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 # homeassistant.components.homematicip_cloud -homematicip==0.10.13 +homematicip==0.10.15 # homeassistant.components.horizon horimote==0.4.1 @@ -674,7 +691,7 @@ horimote==0.4.1 httplib2==0.10.3 # homeassistant.components.huawei_lte -huawei-lte-api==1.4.3 +huawei-lte-api==1.4.4 # homeassistant.components.hydrawise hydrawiser==0.1.1 @@ -697,7 +714,7 @@ ibmiotf==0.3.4 iglo==1.2.7 # homeassistant.components.ihc -ihcsdk==2.3.0 +ihcsdk==2.4.0 # homeassistant.components.incomfort incomfort-client==0.4.0 @@ -715,7 +732,7 @@ iperf3==0.1.11 ipify==1.0.0 # homeassistant.components.verisure -jsonpath==0.75 +jsonpath==0.82 # homeassistant.components.kodi jsonrpc-async==0.6 @@ -727,13 +744,13 @@ jsonrpc-websocket==0.6 kaiterra-async-client==0.0.2 # homeassistant.components.keba -keba-kecontact==0.2.0 +keba-kecontact==1.0.0 # homeassistant.scripts.keyring -keyring==19.2.0 +keyring==20.0.0 # homeassistant.scripts.keyring -keyrings.alt==3.1.1 +keyrings.alt==3.4.0 # homeassistant.components.kiwi kiwiki-client==0.1.1 @@ -745,13 +762,13 @@ konnected==0.1.5 lakeside==0.12 # homeassistant.components.dyson -libpurecool==0.5.0 +libpurecool==0.6.0 # homeassistant.components.foscam libpyfoscam==1.0 # homeassistant.components.vivotek -libpyvivotek==0.2.2 +libpyvivotek==0.4.0 # homeassistant.components.mikrotik librouteros==2.3.0 @@ -769,7 +786,7 @@ liffylights==0.9.4 lightify==1.0.7.2 # homeassistant.components.lightwave -lightwave==0.15 +lightwave==0.17 # homeassistant.components.limitlessled limitlessled==1.1.3 @@ -778,7 +795,7 @@ limitlessled==1.1.3 linode-api==4.1.9b1 # homeassistant.components.liveboxplaytv -liveboxplaytv==2.0.2 +liveboxplaytv==2.0.3 # homeassistant.components.lametric lmnotify==0.0.4 @@ -796,7 +813,7 @@ london-tube-status==0.2 luftdaten==0.6.3 # homeassistant.components.lupusec -lupupy==0.0.17 +lupupy==0.0.18 # homeassistant.components.lw12wifi lw12==0.9.2 @@ -808,7 +825,7 @@ lyft_rides==0.2 magicseaweed==1.0.3 # homeassistant.components.matrix -matrix-client==0.2.0 +matrix-client==0.3.2 # homeassistant.components.maxcube maxcube-api==0.1.0 @@ -844,7 +861,7 @@ mitemp_bt==0.0.3 motorparts==1.1.0 # homeassistant.components.tts -mutagen==1.42.0 +mutagen==1.43.0 # homeassistant.components.mychevy mychevy==1.2.0 @@ -859,7 +876,7 @@ n26==0.2.7 nad_receiver==0.0.11 # homeassistant.components.keenetic_ndms2 -ndms2_client==0.0.10 +ndms2_client==0.0.11 # homeassistant.components.ness_alarm nessclient==0.9.15 @@ -893,7 +910,7 @@ nuheat==0.3.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.17.3 +numpy==1.17.4 # homeassistant.components.oasa_telematics oasatelematics==0.3 @@ -911,13 +928,13 @@ onkyo-eiscp==1.2.7 onvif-zeep-async==0.2.0 # homeassistant.components.opencv -# opencv-python-headless==4.1.1.26 +# opencv-python-headless==4.1.2.30 # homeassistant.components.openevse openevsewifi==0.4 # homeassistant.components.openhome -openhomedevice==0.4.2 +openhomedevice==0.6.3 # homeassistant.components.opensensemap opensensemap-api==0.1.5 @@ -971,9 +988,11 @@ piglow==1.2.4 # homeassistant.components.pilight pilight==0.1.1 -# homeassistant.components.image_processing +# homeassistant.components.doods # homeassistant.components.proxy # homeassistant.components.qrcode +# homeassistant.components.seven_segments +# homeassistant.components.tensorflow pillow==6.2.1 # homeassistant.components.dominos @@ -986,7 +1005,7 @@ plexapi==3.3.0 plexauth==0.0.5 # homeassistant.components.plex -plexwebsocket==0.0.5 +plexwebsocket==0.0.6 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 @@ -1019,8 +1038,11 @@ prometheus_client==0.7.1 # homeassistant.components.tensorflow protobuf==3.6.1 +# homeassistant.components.proxmoxve +proxmoxer==1.0.3 + # homeassistant.components.systemmonitor -psutil==5.6.5 +psutil==5.6.7 # homeassistant.components.ptvsd ptvsd==4.2.8 @@ -1069,13 +1091,13 @@ pyHS100==0.3.5 pyMetno==0.4.6 # homeassistant.components.rfxtrx -pyRFXtrx==0.23 +pyRFXtrx==0.24 # homeassistant.components.switchmate # pySwitchmate==0.4.6 # homeassistant.components.tibber -pyTibber==0.11.7 +pyTibber==0.12.0 # homeassistant.components.dlink pyW215==0.6.0 @@ -1111,7 +1133,7 @@ pyalmond==0.0.2 pyarlo==0.2.3 # homeassistant.components.netatmo -pyatmo==2.3.3 +pyatmo==3.1.0 # homeassistant.components.atome pyatome==0.1.1 @@ -1165,7 +1187,7 @@ pydaikin==1.6.1 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==64 +pydeconz==65 # homeassistant.components.delijn pydelijn==0.5.1 @@ -1218,6 +1240,9 @@ pyflexit==0.3 # homeassistant.components.flic pyflic-homeassistant==0.4.dev0 +# homeassistant.components.flume +pyflume==0.2.4 + # homeassistant.components.flunearyou pyflunearyou==1.0.3 @@ -1256,7 +1281,7 @@ pyhik==0.2.5 pyhiveapi==0.2.19.3 # homeassistant.components.homematic -pyhomematic==0.1.61 +pyhomematic==0.1.62 # homeassistant.components.homeworks pyhomeworks==0.0.6 @@ -1267,6 +1292,9 @@ pyialarm==0.3 # homeassistant.components.icloud pyicloud==0.9.1 +# homeassistant.components.intesishome +pyintesishome==1.5 + # homeassistant.components.ipma pyipma==1.2.1 @@ -1300,9 +1328,6 @@ pylaunches==0.2.0 # homeassistant.components.lg_netcast pylgnetcast-homeassistant==0.2.0.dev0 -# homeassistant.components.webostv -pylgtv==0.1.9 - # homeassistant.components.linky pylinky==0.4.0 @@ -1313,7 +1338,7 @@ pylitejet==0.1 pyloopenergy==0.1.3 # homeassistant.components.lutron_caseta -pylutron-caseta==0.5.0 +pylutron-caseta==0.5.1 # homeassistant.components.lutron pylutron==0.2.5 @@ -1325,7 +1350,7 @@ pymailgunner==1.4 pymediaroom==0.6.4 # homeassistant.components.somfy -pymfy==0.6.1 +pymfy==0.7.1 # homeassistant.components.xiaomi_tv pymitv==1.4.3 @@ -1407,8 +1432,11 @@ pyowlet==1.0.3 # homeassistant.components.openweathermap pyowm==2.10.0 +# homeassistant.components.onewire +pyownet==0.10.0.post1 + # homeassistant.components.elv -pypca==0.0.5 +pypca==0.0.7 # homeassistant.components.lcn pypck==0.6.3 @@ -1420,7 +1448,7 @@ pypjlink2==1.2.0 pypoint==1.1.2 # homeassistant.components.ps4 -pyps4-2ndscreen==1.0.1 +pyps4-2ndscreen==1.0.4 # homeassistant.components.qwikswitch pyqwikswitch==0.93 @@ -1461,6 +1489,9 @@ pysesame2==1.0.1 # homeassistant.components.goalfeed pysher==1.0.1 +# homeassistant.components.signal_messenger +pysignalclirestapi==0.1.4 + # homeassistant.components.sma pysma==0.3.4 @@ -1468,7 +1499,7 @@ pysma==0.3.4 pysmartapp==0.3.2 # homeassistant.components.smartthings -pysmartthings==0.6.9 +pysmartthings==0.7.0 # homeassistant.components.smarty pysmarty==0.8 @@ -1501,7 +1532,7 @@ pysyncthru==0.5.0 pytautulli==0.5.0 # homeassistant.components.liveboxplaytv -pyteleloisirs==3.5 +pyteleloisirs==3.6 # homeassistant.components.tfiac pytfiac==0.4 @@ -1549,13 +1580,13 @@ python-izone==1.1.1 python-join-api==0.0.4 # homeassistant.components.juicenet -python-juicenet==0.1.5 +python-juicenet==0.1.6 # homeassistant.components.lirc # python-lirc==1.2.3 # homeassistant.components.xiaomi_miio -python-miio==0.4.7 +python-miio==0.4.8 # homeassistant.components.mpd python-mpd2==1.0.0 @@ -1573,7 +1604,7 @@ python-nmap==0.6.1 python-pushover==0.4 # homeassistant.components.qbittorrent -python-qbittorrent==0.3.1 +python-qbittorrent==0.4.1 # homeassistant.components.ripple python-ripple-api==0.0.3 @@ -1600,7 +1631,7 @@ python-telnet-vlc==1.0.4 python-twitch-client==0.6.0 # homeassistant.components.velbus -python-velbus==2.0.27 +python-velbus==2.0.32 # homeassistant.components.vlc python-vlc==1.1.2 @@ -1615,7 +1646,7 @@ python-wink==1.10.5 python_awair==0.0.4 # homeassistant.components.swiss_public_transport -python_opendata_transport==0.1.4 +python_opendata_transport==0.2.1 # homeassistant.components.egardia pythonegardia==1.0.40 @@ -1649,7 +1680,10 @@ pyuptimerobot==0.0.5 # pyuserinput==0.1.11 # homeassistant.components.vera -pyvera==0.3.6 +pyvera==0.3.7 + +# homeassistant.components.versasense +pyversasense==0.0.6 # homeassistant.components.vesync pyvesync==1.1.0 @@ -1658,7 +1692,7 @@ pyvesync==1.1.0 pyvizio==0.0.7 # homeassistant.components.velux -pyvlx==0.2.11 +pyvlx==0.2.12 # homeassistant.components.html5 pywebpush==1.9.2 @@ -1709,10 +1743,10 @@ restrictedpython==5.0 rfk101py==0.0.1 # homeassistant.components.rflink -rflink==0.0.46 +rflink==0.0.50 # homeassistant.components.ring -ring_doorbell==0.2.3 +ring_doorbell==0.2.8 # homeassistant.components.fleetgo ritassist==0.9.2 @@ -1724,10 +1758,10 @@ rjpl==0.3.5 rocketchat-API==0.6.1 # homeassistant.components.roku -roku==3.1 +roku==4.0.0 # homeassistant.components.roomba -roombapy==1.3.1 +roombapy==1.4.2 # homeassistant.components.rova rova==0.1.0 @@ -1769,13 +1803,13 @@ sense_energy==0.7.0 sharp_aquos_rc==0.3.2 # homeassistant.components.shodan -shodan==1.19.1 +shodan==1.21.1 # homeassistant.components.simplepush simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==5.2.0 +simplisafe-python==5.3.6 # homeassistant.components.sisyphus sisyphus-control==2.2.1 @@ -1844,10 +1878,13 @@ spotipy-homeassistant==2.4.4.dev1 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.3.11 +sqlalchemy==1.3.12 + +# homeassistant.components.starline +starline==0.1.3 # homeassistant.components.starlingbank -starlingbank==3.1 +starlingbank==3.2 # homeassistant.components.statsd statsd==3.2.1 @@ -1877,7 +1914,7 @@ swisshydrodata==0.0.3 synology-srm==0.0.7 # homeassistant.components.tahoma -tahoma-api==0.0.14 +tahoma-api==0.0.16 # homeassistant.components.tank_utility tank_utility==1.4.0 @@ -1904,7 +1941,7 @@ temperusb==1.5.3 # tensorflow==1.13.2 # homeassistant.components.tesla -teslajsonpy==0.2.0 +teslajsonpy==0.2.1 # homeassistant.components.thermoworks_smoke thermoworks_smoke==0.1.8 @@ -1931,7 +1968,7 @@ tp-connected==0.0.4 transmissionrpc==0.11 # homeassistant.components.tuya -tuyaha==0.0.4 +tuyaha==0.0.5 # homeassistant.components.twentemilieu twentemilieu==0.1.0 @@ -1943,7 +1980,7 @@ twilio==6.32.0 unifiled==0.11 # homeassistant.components.upcloud -upcloud-api==0.4.3 +upcloud-api==0.4.5 # homeassistant.components.huawei_lte url-normalize==1.4.1 @@ -1958,7 +1995,7 @@ uvcclient==0.11.0 vallox-websocket-api==2.2.0 # homeassistant.components.venstar -venstarcolortouch==0.9 +venstarcolortouch==0.12 # homeassistant.components.meteo_france vigilancemeteo==3.0.0 @@ -1970,7 +2007,7 @@ volkszaehler==0.1.2 volvooncall==0.8.7 # homeassistant.components.verisure -vsure==1.5.2 +vsure==1.5.4 # homeassistant.components.vasttrafik vtjp==0.1.14 @@ -1998,9 +2035,6 @@ webexteamssdk==1.1.1 # homeassistant.components.gpmdp websocket-client==0.54.0 -# homeassistant.components.webostv -websockets==6.0 - # homeassistant.components.wirelesstag wirelesstagpy==0.4.0 @@ -2051,16 +2085,16 @@ yeelight==0.5.0 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2019.11.05 +youtube_dl==2020.01.01 # homeassistant.components.zengge zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.23.0 +zeroconf==0.24.4 # homeassistant.components.zha -zha-quirks==0.0.28 +zha-quirks==0.0.30 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 @@ -2072,10 +2106,10 @@ ziggo-mediabox-xl==1.1.0 zigpy-deconz==0.7.0 # homeassistant.components.zha -zigpy-homeassistant==0.11.0 +zigpy-homeassistant==0.12.0 # homeassistant.components.zha -zigpy-xbee-homeassistant==0.7.0 +zigpy-xbee-homeassistant==0.8.0 # homeassistant.components.zha zigpy-zigate==0.5.0 diff --git a/requirements_docs.txt b/requirements_docs.txt index b3dd4616f49..a27f3a4a306 100644 --- a/requirements_docs.txt +++ b/requirements_docs.txt @@ -1,3 +1,3 @@ -Sphinx==2.1.2 -sphinx-autodoc-typehints==1.6.0 +Sphinx==2.3.1 +sphinx-autodoc-typehints==1.10.3 sphinx-autodoc-annotation==1.0.post1 \ No newline at end of file diff --git a/requirements_test.txt b/requirements_test.txt index dae60b74653..37268e70726 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -6,7 +6,7 @@ asynctest==0.13.0 codecov==2.0.15 mock-open==1.3.1 -mypy==0.740 +mypy==0.761 pre-commit==1.20.0 pylint==2.4.4 astroid==2.3.3 @@ -14,6 +14,6 @@ pytest-aiohttp==0.3.0 pytest-cov==2.8.1 pytest-sugar==0.9.2 pytest-timeout==1.3.3 -pytest==5.2.3 +pytest==5.3.2 requests_mock==1.7.0 responses==0.10.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bfd97c58fd8..ff1ded4434c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -29,7 +29,7 @@ YesssSMS==0.4.1 abodepy==0.16.7 # homeassistant.components.androidtv -adb-shell==0.0.8 +adb-shell==0.1.0 # homeassistant.components.adguard adguardhome==0.3.0 @@ -37,8 +37,14 @@ adguardhome==0.3.0 # homeassistant.components.geonetnz_quakes aio_geojson_geonetnz_quakes==0.11 +# homeassistant.components.geonetnz_volcano +aio_geojson_geonetnz_volcano==0.5 + +# homeassistant.components.nsw_rural_fire_service_feed +aio_geojson_nsw_rfs_incidents==0.1 + # homeassistant.components.ambient_station -aioambient==0.3.2 +aioambient==1.0.2 # homeassistant.components.asuswrt aioasuswrt==1.1.22 @@ -50,18 +56,21 @@ aioautomatic==0.6.5 aiobotocore==0.10.4 # homeassistant.components.esphome -aioesphomeapi==2.5.0 +aioesphomeapi==2.6.1 # homeassistant.components.emulated_hue # homeassistant.components.http aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==1.9.2 +aiohue==1.10.1 # homeassistant.components.notion aionotion==1.1.0 +# homeassistant.components.webostv +aiopylgtv==0.2.4 + # homeassistant.components.switcher_kis aioswitcher==2019.4.26 @@ -78,13 +87,13 @@ airly==0.0.2 ambiclimate==0.2.1 # homeassistant.components.androidtv -androidtv==0.0.34 +androidtv==0.0.36 # homeassistant.components.apns apns2==0.3.0 # homeassistant.components.apprise -apprise==0.8.1 +apprise==0.8.2 # homeassistant.components.aprs aprslib==0.6.46 @@ -103,7 +112,7 @@ av==6.1.2 axis==25 # homeassistant.components.zha -bellows-homeassistant==0.11.0 +bellows-homeassistant==0.12.0 # homeassistant.components.bom bomradarloop==0.1.3 @@ -137,6 +146,7 @@ datadog==0.15.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns # homeassistant.components.ohmconnect +# homeassistant.components.ssdp defusedxml==0.6.0 # homeassistant.components.directv @@ -151,6 +161,9 @@ dsmr_parser==0.12 # homeassistant.components.ee_brightbox eebrightbox==0.0.4 +# homeassistant.components.elgato +elgato==0.1.0 + # homeassistant.components.emulated_roku emulated_roku==0.1.8 @@ -167,7 +180,6 @@ foobot_async==0.3.1 gTTS-token==1.1.3 # homeassistant.components.geo_json_events -# homeassistant.components.nsw_rural_fire_service_feed # homeassistant.components.usgs_earthquakes_feed geojson_client==0.4 @@ -175,7 +187,7 @@ geojson_client==0.4 geopy==1.19.0 # homeassistant.components.geo_rss_events -georss_generic_client==0.2 +georss_generic_client==0.3 # homeassistant.components.ign_sismologia georss_ign_sismologia_client==0.2 @@ -188,6 +200,9 @@ georss_qld_bushfire_alert_client==0.3 # homeassistant.components.nmap_tracker getmac==0.8.1 +# homeassistant.components.gios +gios==0.0.3 + # homeassistant.components.glances glances_api==0.2.0 @@ -204,7 +219,7 @@ ha-ffmpeg==2.0 hangups==0.4.9 # homeassistant.components.cloud -hass-nabucasa==0.29 +hass-nabucasa==0.30 # homeassistant.components.mqtt hbmqtt==0.9.5 @@ -213,32 +228,32 @@ hbmqtt==0.9.5 hdate==0.9.3 # homeassistant.components.here_travel_time -herepy==0.6.3.1 +herepy==2.0.0 # homeassistant.components.pi_hole hole==0.5.0 # homeassistant.components.workday -holidays==0.9.11 +holidays==0.9.12 # homeassistant.components.frontend -home-assistant-frontend==20191115.0 +home-assistant-frontend==20191204.1 # homeassistant.components.zwave -homeassistant-pyozw==0.1.4 +homeassistant-pyozw==0.1.7 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 # homeassistant.components.homematicip_cloud -homematicip==0.10.13 +homematicip==0.10.15 # homeassistant.components.google # homeassistant.components.remember_the_milk httplib2==0.10.3 # homeassistant.components.huawei_lte -huawei-lte-api==1.4.3 +huawei-lte-api==1.4.4 # homeassistant.components.iaqualink iaqualink==0.3.0 @@ -247,16 +262,16 @@ iaqualink==0.3.0 influxdb==5.2.3 # homeassistant.components.verisure -jsonpath==0.75 +jsonpath==0.82 # homeassistant.scripts.keyring -keyring==19.2.0 +keyring==20.0.0 # homeassistant.scripts.keyring -keyrings.alt==3.1.1 +keyrings.alt==3.4.0 # homeassistant.components.dyson -libpurecool==0.5.0 +libpurecool==0.6.0 # homeassistant.components.soundtouch libsoundtouch==0.7.2 @@ -277,7 +292,7 @@ mficlient==0.3.0 minio==4.0.9 # homeassistant.components.tts -mutagen==1.42.0 +mutagen==1.43.0 # homeassistant.components.ness_alarm nessclient==0.9.15 @@ -296,7 +311,7 @@ nuheat==0.3.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.17.3 +numpy==1.17.4 # homeassistant.components.google oauth2client==4.0.0 @@ -314,11 +329,6 @@ pexpect==4.6.0 # homeassistant.components.pilight pilight==0.1.1 -# homeassistant.components.image_processing -# homeassistant.components.proxy -# homeassistant.components.qrcode -pillow==6.2.1 - # homeassistant.components.plex plexapi==3.3.0 @@ -326,7 +336,7 @@ plexapi==3.3.0 plexauth==0.0.5 # homeassistant.components.plex -plexwebsocket==0.0.5 +plexwebsocket==0.0.6 # homeassistant.components.mhz19 # homeassistant.components.serial_pm @@ -367,7 +377,7 @@ pyHS100==0.3.5 pyMetno==0.4.6 # homeassistant.components.rfxtrx -pyRFXtrx==0.23 +pyRFXtrx==0.24 # homeassistant.components.nextbus py_nextbusnext==0.1.4 @@ -397,7 +407,7 @@ pycoolmasternet==0.0.4 pydaikin==1.6.1 # homeassistant.components.deconz -pydeconz==64 +pydeconz==65 # homeassistant.components.zwave pydispatcher==2.0.5 @@ -421,7 +431,10 @@ pyhaversion==3.1.0 pyheos==0.6.0 # homeassistant.components.homematic -pyhomematic==0.1.61 +pyhomematic==0.1.62 + +# homeassistant.components.icloud +pyicloud==0.9.1 # homeassistant.components.ipma pyipma==1.2.1 @@ -432,9 +445,6 @@ pyiqvia==0.2.1 # homeassistant.components.kira pykira==0.1.1 -# homeassistant.components.webostv -pylgtv==0.1.9 - # homeassistant.components.linky pylinky==0.4.0 @@ -445,7 +455,7 @@ pylitejet==0.1 pymailgunner==1.4 # homeassistant.components.somfy -pymfy==0.6.1 +pymfy==0.7.1 # homeassistant.components.mochad pymochad==0.2.0 @@ -477,7 +487,7 @@ pyotp==2.3.0 pypoint==1.1.2 # homeassistant.components.ps4 -pyps4-2ndscreen==1.0.1 +pyps4-2ndscreen==1.0.4 # homeassistant.components.qwikswitch pyqwikswitch==0.93 @@ -489,7 +499,7 @@ pysma==0.3.4 pysmartapp==0.3.2 # homeassistant.components.smartthings -pysmartthings==0.6.9 +pysmartthings==0.7.0 # homeassistant.components.soma pysoma==0.0.10 @@ -510,13 +520,13 @@ python-forecastio==1.4.0 python-izone==1.1.1 # homeassistant.components.xiaomi_miio -python-miio==0.4.7 +python-miio==0.4.8 # homeassistant.components.nest python-nest==4.1.0 # homeassistant.components.velbus -python-velbus==2.0.27 +python-velbus==2.0.32 # homeassistant.components.awair python_awair==0.0.4 @@ -527,6 +537,9 @@ pytraccar==0.9.0 # homeassistant.components.tradfri pytradfri[async]==6.4.0 +# homeassistant.components.vera +pyvera==0.3.7 + # homeassistant.components.vesync pyvesync==1.1.0 @@ -540,10 +553,10 @@ regenmaschine==1.5.1 restrictedpython==5.0 # homeassistant.components.rflink -rflink==0.0.46 +rflink==0.0.50 # homeassistant.components.ring -ring_doorbell==0.2.3 +ring_doorbell==0.2.8 # homeassistant.components.yamaha rxv==0.6.0 @@ -552,7 +565,7 @@ rxv==0.6.0 samsungctl[websocket]==0.7.1 # homeassistant.components.simplisafe -simplisafe-python==5.2.0 +simplisafe-python==5.3.6 # homeassistant.components.sleepiq sleepyq==0.7 @@ -568,7 +581,10 @@ somecomfort==0.5.2 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.3.11 +sqlalchemy==1.3.12 + +# homeassistant.components.starline +starline==0.1.3 # homeassistant.components.statsd statsd==3.2.1 @@ -585,6 +601,9 @@ sunwatcher==0.2.1 # homeassistant.components.tellduslive tellduslive==0.10.10 +# homeassistant.components.tesla +teslajsonpy==0.2.1 + # homeassistant.components.toon toonapilib==3.2.4 @@ -604,7 +623,7 @@ url-normalize==1.4.1 uvcclient==0.11.0 # homeassistant.components.verisure -vsure==1.5.2 +vsure==1.5.4 # homeassistant.components.vultr vultr==0.1.2 @@ -617,9 +636,6 @@ wakeonlan==1.1.6 # homeassistant.components.folder_watcher watchdog==0.8.3 -# homeassistant.components.webostv -websockets==6.0 - # homeassistant.components.withings withings-api==2.1.3 @@ -640,19 +656,19 @@ ya_ma==0.3.8 yahooweather==0.10 # homeassistant.components.zeroconf -zeroconf==0.23.0 +zeroconf==0.24.4 # homeassistant.components.zha -zha-quirks==0.0.28 +zha-quirks==0.0.30 # homeassistant.components.zha zigpy-deconz==0.7.0 # homeassistant.components.zha -zigpy-homeassistant==0.11.0 +zigpy-homeassistant==0.12.0 # homeassistant.components.zha -zigpy-xbee-homeassistant==0.7.0 +zigpy-xbee-homeassistant==0.8.0 # homeassistant.components.zha zigpy-zigate==0.5.0 diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 29380ca7cd2..7a20962ff7c 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -1,6 +1,8 @@ # Automatically generated from .pre-commit-config-all.yaml by gen_requirements_all.py, do not edit +bandit==1.6.2 black==19.10b0 flake8-docstrings==1.5.0 flake8==3.7.9 -pydocstyle==4.0.1 +isort==v4.3.21 +pydocstyle==5.0.1 diff --git a/script/__init__.py b/script/__init__.py new file mode 100644 index 00000000000..e71d5b3763b --- /dev/null +++ b/script/__init__.py @@ -0,0 +1 @@ +"""Home Assistant scripts.""" diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 9bbe7d379ec..f40a89a9d9a 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -8,10 +8,10 @@ import pkgutil import re import sys -from homeassistant.util.yaml.loader import load_yaml - from script.hassfest.model import Integration +from homeassistant.util.yaml.loader import load_yaml + COMMENT_REQUIREMENTS = ( "Adafruit_BBIO", "Adafruit-DHT", @@ -58,13 +58,15 @@ CONSTRAINT_PATH = os.path.join( CONSTRAINT_BASE = """ pycryptodome>=3.6.6 -# Breaks Python 3.6 and is not needed for our supported Python versions +# Not needed for our supported Python versions enum34==1000000000.0.0 # This is a old unmaintained library and is replaced with pycryptodome pycrypto==1000000000.0.0 """ +IGNORE_PRE_COMMIT_HOOK_ID = ("check-json",) + def has_tests(module: str): """Test if a module has tests. @@ -256,8 +258,9 @@ def requirements_pre_commit_output(): reqs = [] for repo in (x for x in pre_commit_conf["repos"] if x.get("rev")): for hook in repo["hooks"]: - reqs.append(f"{hook['id']}=={repo['rev']}") - reqs.extend(x for x in hook.get("additional_dependencies", ())) + if hook["id"] not in IGNORE_PRE_COMMIT_HOOK_ID: + reqs.append(f"{hook['id']}=={repo['rev']}") + reqs.extend(x for x in hook.get("additional_dependencies", ())) output = [ f"# Automatically generated " f"from {source} by {Path(__file__).name}, do not edit", diff --git a/script/hassfest/__main__.py b/script/hassfest/__main__.py index a1168b15f7d..99e32e57f43 100644 --- a/script/hassfest/__main__.py +++ b/script/hassfest/__main__.py @@ -2,10 +2,28 @@ import pathlib import sys -from .model import Integration, Config -from . import codeowners, config_flow, dependencies, manifest, services, ssdp, zeroconf +from . import ( + codeowners, + config_flow, + dependencies, + json, + manifest, + services, + ssdp, + zeroconf, +) +from .model import Config, Integration -PLUGINS = [codeowners, config_flow, dependencies, manifest, services, ssdp, zeroconf] +PLUGINS = [ + json, + codeowners, + config_flow, + dependencies, + manifest, + services, + ssdp, + zeroconf, +] def get_config() -> Config: diff --git a/script/hassfest/codeowners.py b/script/hassfest/codeowners.py index 6f63fab3fdb..cfbd112100a 100644 --- a/script/hassfest/codeowners.py +++ b/script/hassfest/codeowners.py @@ -1,7 +1,7 @@ """Generate CODEOWNERS.""" from typing import Dict -from .model import Integration, Config +from .model import Config, Integration BASE = """ # This file is generated by script/hassfest/codeowners.py @@ -66,7 +66,7 @@ def validate(integrations: Dict[str, Integration], config: Config): if fp.read().strip() != content: config.add_error( "codeowners", - "File CODEOWNERS is not up to date. " "Run python3 -m script.hassfest", + "File CODEOWNERS is not up to date. Run python3 -m script.hassfest", fixable=True, ) return diff --git a/script/hassfest/config_flow.py b/script/hassfest/config_flow.py index 4384399f4db..83d495e1bf2 100644 --- a/script/hassfest/config_flow.py +++ b/script/hassfest/config_flow.py @@ -2,7 +2,7 @@ import json from typing import Dict -from .model import Integration, Config +from .model import Config, Integration BASE = """ \"\"\"Automatically generated by hassfest. diff --git a/script/hassfest/dependencies.py b/script/hassfest/dependencies.py index e9933995715..8500e9d897d 100644 --- a/script/hassfest/dependencies.py +++ b/script/hassfest/dependencies.py @@ -1,24 +1,91 @@ """Validate dependencies.""" -import pathlib -import re -from typing import Set, Dict +import ast +from pathlib import Path +from typing import Dict, Set + +from homeassistant.requirements import DISCOVERY_INTEGRATIONS from .model import Integration -def grep_dir(path: pathlib.Path, glob_pattern: str, search_pattern: str) -> Set[str]: - """Recursively go through a dir and it's children and find the regex.""" - pattern = re.compile(search_pattern) - found = set() +class ImportCollector(ast.NodeVisitor): + """Collect all integrations referenced.""" - for fil in path.glob(glob_pattern): - if not fil.is_file(): - continue + def __init__(self, integration: Integration): + """Initialize the import collector.""" + self.integration = integration + self.referenced: Dict[Path, Set[str]] = {} - for match in pattern.finditer(fil.read_text()): - found.add(match.groups()[0]) + # Current file or dir we're inspecting + self._cur_fil_dir = None - return found + def collect(self) -> None: + """Collect imports from a source file.""" + for fil in self.integration.path.glob("**/*.py"): + if not fil.is_file(): + continue + + self._cur_fil_dir = fil.relative_to(self.integration.path) + self.referenced[self._cur_fil_dir] = set() + self.visit(ast.parse(fil.read_text())) + self._cur_fil_dir = None + + def _add_reference(self, reference_domain: str): + """Add a reference.""" + self.referenced[self._cur_fil_dir].add(reference_domain) + + def visit_ImportFrom(self, node): + """Visit ImportFrom node.""" + if node.module is None: + return + + if node.module.startswith("homeassistant.components."): + # from homeassistant.components.alexa.smart_home import EVENT_ALEXA_SMART_HOME + # from homeassistant.components.logbook import bla + self._add_reference(node.module.split(".")[2]) + + elif node.module == "homeassistant.components": + # from homeassistant.components import sun + for name_node in node.names: + self._add_reference(name_node.name) + + def visit_Import(self, node): + """Visit Import node.""" + # import homeassistant.components.hue as hue + for name_node in node.names: + if name_node.name.startswith("homeassistant.components."): + self._add_reference(name_node.name.split(".")[2]) + + def visit_Attribute(self, node): + """Visit Attribute node.""" + # hass.components.hue.async_create() + # Name(id=hass) + # .Attribute(attr=hue) + # .Attribute(attr=async_create) + + # self.hass.components.hue.async_create() + # Name(id=self) + # .Attribute(attr=hass) + # .Attribute(attr=hue) + # .Attribute(attr=async_create) + if ( + isinstance(node.value, ast.Attribute) + and node.value.attr == "components" + and ( + ( + isinstance(node.value.value, ast.Name) + and node.value.value.id == "hass" + ) + or ( + isinstance(node.value.value, ast.Attribute) + and node.value.value.attr == "hass" + ) + ) + ): + self._add_reference(node.attr) + else: + # Have it visit other kids + self.generic_visit(node) ALLOWED_USED_COMPONENTS = { @@ -30,28 +97,135 @@ ALLOWED_USED_COMPONENTS = { "hassio", "system_health", "websocket_api", + "automation", + "device_automation", + "zone", + "homeassistant", + "system_log", + "person", + # Discovery + "discovery", + # Other + "mjpeg", # base class, has no reqs or component to load. + "stream", # Stream cannot install on all systems, can be imported without reqs. } +IGNORE_VIOLATIONS = [ + # Has same requirement, gets defaults. + ("sql", "recorder"), + # Sharing a base class + ("openalpr_cloud", "openalpr_local"), + ("lutron_caseta", "lutron"), + ("ffmpeg_noise", "ffmpeg_motion"), + # Demo + ("demo", "manual"), + ("demo", "openalpr_local"), + # This should become a helper method that integrations can submit data to + ("websocket_api", "lovelace"), + # Expose HA to external systems + "homekit", + "alexa", + "google_assistant", + "emulated_hue", + "prometheus", + "conversation", + "logbook", + "mobile_app", + # These should be extracted to external package + "pvoutput", + "dwd_weather_warnings", + # Should be rewritten to use own data fetcher + "scrape", +] -def validate_dependencies(integration: Integration): + +def calc_allowed_references(integration: Integration) -> Set[str]: + """Return a set of allowed references.""" + allowed_references = ( + ALLOWED_USED_COMPONENTS + | set(integration.manifest["dependencies"]) + | set(integration.manifest.get("after_dependencies", [])) + ) + + # Discovery requirements are ok if referenced in manifest + for check_domain, to_check in DISCOVERY_INTEGRATIONS.items(): + if any(check in integration.manifest for check in to_check): + allowed_references.add(check_domain) + + return allowed_references + + +def find_non_referenced_integrations( + integrations: Dict[str, Integration], + integration: Integration, + references: Dict[Path, Set[str]], +): + """Find intergrations that are not allowed to be referenced.""" + allowed_references = calc_allowed_references(integration) + referenced = set() + for path, refs in references.items(): + if len(path.parts) == 1: + # climate.py is stored as climate + cur_fil_dir = path.stem + else: + # climate/__init__.py is stored as climate + cur_fil_dir = path.parts[0] + + is_platform_other_integration = cur_fil_dir in integrations + + for ref in refs: + # We are always allowed to import from ourselves + if ref == integration.domain: + continue + + # These references are approved based on the manifest + if ref in allowed_references: + continue + + # Some violations are whitelisted + if (integration.domain, ref) in IGNORE_VIOLATIONS: + continue + + # If it's a platform for another integration, the other integration is ok + if is_platform_other_integration and cur_fil_dir == ref: + continue + + # These have a platform specified in this integration + if not is_platform_other_integration and ( + (integration.path / f"{ref}.py").is_file() + # Platform dir + or (integration.path / ref).is_dir() + ): + continue + + referenced.add(ref) + + return referenced + + +def validate_dependencies( + integrations: Dict[str, Integration], integration: Integration +): """Validate all dependencies.""" - # Find usage of hass.components - referenced = grep_dir(integration.path, "**/*.py", r"hass\.components\.(\w+)") - referenced -= ALLOWED_USED_COMPONENTS - referenced -= set(integration.manifest["dependencies"]) - referenced -= set(integration.manifest.get("after_dependencies", [])) + # Some integrations are allowed to have violations. + if integration.domain in IGNORE_VIOLATIONS: + return - if referenced: - for domain in sorted(referenced): - print( - "Warning: {} references integration {} but it's not a " - "dependency".format(integration.domain, domain) - ) - # Not enforced yet. - # integration.add_error( - # 'dependencies', - # "Using component {} but it's not a dependency".format(domain) - # ) + # Find usage of hass.components + collector = ImportCollector(integration) + collector.collect() + + for domain in sorted( + find_non_referenced_integrations( + integrations, integration, collector.referenced + ) + ): + integration.add_error( + "dependencies", + "Using component {} but it's not in 'dependencies' or 'after_dependencies'".format( + domain + ), + ) def validate(integrations: Dict[str, Integration], config): @@ -61,7 +235,7 @@ def validate(integrations: Dict[str, Integration], config): if not integration.manifest: continue - validate_dependencies(integration) + validate_dependencies(integrations, integration) # check that all referenced dependencies exist for dep in integration.manifest["dependencies"]: diff --git a/script/hassfest/json.py b/script/hassfest/json.py new file mode 100644 index 00000000000..73b6c372b4f --- /dev/null +++ b/script/hassfest/json.py @@ -0,0 +1,29 @@ +"""Validate integration JSON files.""" +import json +from typing import Dict + +from .model import Integration + + +def validate_json_files(integration: Integration): + """Validate JSON files for integration.""" + for json_file in integration.path.glob("**/*.json"): + if not json_file.is_file(): + continue + + try: + json.loads(json_file.read_text()) + except json.JSONDecodeError: + relative_path = json_file.relative_to(integration.path) + integration.add_error("json", f"Invalid JSON file {relative_path}") + + return + + +def validate(integrations: Dict[str, Integration], config): + """Handle JSON files inside integrations.""" + for integration in integrations.values(): + if not integration.manifest: + continue + + validate_json_files(integration) diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 16f8b77b5d3..acc5e9af832 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -6,7 +6,6 @@ from voluptuous.humanize import humanize_error from .model import Integration - MANIFEST_SCHEMA = vol.Schema( { vol.Required("domain"): str, diff --git a/script/hassfest/manifest_helper.py b/script/hassfest/manifest_helper.py index 251a2939807..0c2a1456ec6 100644 --- a/script/hassfest/manifest_helper.py +++ b/script/hassfest/manifest_helper.py @@ -2,7 +2,6 @@ import json import pathlib - component_dir = pathlib.Path("homeassistant/components") diff --git a/script/hassfest/model.py b/script/hassfest/model.py index 77683d65961..faa1c26262c 100644 --- a/script/hassfest/model.py +++ b/script/hassfest/model.py @@ -1,8 +1,8 @@ """Models for manifest validator.""" -import json -from typing import List, Dict, Any -import pathlib import importlib +import json +import pathlib +from typing import Any, Dict, List import attr diff --git a/script/hassfest/services.py b/script/hassfest/services.py index 801ee10e43a..08cde60f5b5 100644 --- a/script/hassfest/services.py +++ b/script/hassfest/services.py @@ -1,8 +1,8 @@ """Validate dependencies.""" import pathlib +import re from typing import Dict -import re import voluptuous as vol from voluptuous.humanize import humanize_error diff --git a/script/hassfest/ssdp.py b/script/hassfest/ssdp.py index d2dd724605e..5ee2076ecf4 100644 --- a/script/hassfest/ssdp.py +++ b/script/hassfest/ssdp.py @@ -3,7 +3,7 @@ from collections import OrderedDict, defaultdict import json from typing import Dict -from .model import Integration, Config +from .model import Config, Integration BASE = """ \"\"\"Automatically generated by hassfest. @@ -68,7 +68,7 @@ def validate(integrations: Dict[str, Integration], config: Config): if fp.read().strip() != content: config.add_error( "ssdp", - "File ssdp.py is not up to date. " "Run python3 -m script.hassfest", + "File ssdp.py is not up to date. Run python3 -m script.hassfest", fixable=True, ) return diff --git a/script/hassfest/zeroconf.py b/script/hassfest/zeroconf.py index 3d93d363086..2a1bb936871 100644 --- a/script/hassfest/zeroconf.py +++ b/script/hassfest/zeroconf.py @@ -3,7 +3,7 @@ from collections import OrderedDict, defaultdict import json from typing import Dict -from .model import Integration, Config +from .model import Config, Integration BASE = """ \"\"\"Automatically generated by hassfest. @@ -122,7 +122,7 @@ def validate(integrations: Dict[str, Integration], config: Config): if current != content: config.add_error( "zeroconf", - "File zeroconf.py is not up to date. " "Run python3 -m script.hassfest", + "File zeroconf.py is not up to date. Run python3 -m script.hassfest", fixable=True, ) return diff --git a/script/inspect_schemas.py b/script/inspect_schemas.py index e46165bfaa2..4f305a4fb9c 100755 --- a/script/inspect_schemas.py +++ b/script/inspect_schemas.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """Inspect all component SCHEMAS.""" -import os import importlib +import os import pkgutil from homeassistant.config import _identify_config_schema diff --git a/script/lazytox.py b/script/lazytox.py index ca8a160e7dc..d1b09618998 100755 --- a/script/lazytox.py +++ b/script/lazytox.py @@ -4,12 +4,12 @@ Lazy 'tox' to quickly check if branch is up to PR standards. This is NOT a tox replacement, only a quick check during development. """ -import os import asyncio -import sys +from collections import namedtuple +import os import re import shlex -from collections import namedtuple +import sys try: from colorlog.escape_codes import escape_codes diff --git a/script/scaffold/__main__.py b/script/scaffold/__main__.py index 78490b84ba3..94ac009fd9c 100644 --- a/script/scaffold/__main__.py +++ b/script/scaffold/__main__.py @@ -4,10 +4,9 @@ from pathlib import Path import subprocess import sys -from . import gather_info, generate, error, docs +from . import docs, error, gather_info, generate from .const import COMPONENT_DIR - TEMPLATES = [ p.name for p in (Path(__file__).parent / "templates").glob("*") if p.is_dir() ] diff --git a/script/scaffold/docs.py b/script/scaffold/docs.py index 5df663fec0b..8186b857e80 100644 --- a/script/scaffold/docs.py +++ b/script/scaffold/docs.py @@ -1,7 +1,6 @@ """Print links to relevant docs.""" from .model import Info - DATA = { "config_flow": { "title": "Config Flow", diff --git a/script/scaffold/gather_info.py b/script/scaffold/gather_info.py index 6a69040a6d7..48d0a20ea73 100644 --- a/script/scaffold/gather_info.py +++ b/script/scaffold/gather_info.py @@ -4,9 +4,8 @@ import json from homeassistant.util import slugify from .const import COMPONENT_DIR -from .model import Info from .error import ExitApp - +from .model import Info CHECK_EMPTY = ["Cannot be empty", lambda value: value] diff --git a/script/scaffold/generate.py b/script/scaffold/generate.py index a04cdb3ef5e..b2f669006a9 100644 --- a/script/scaffold/generate.py +++ b/script/scaffold/generate.py @@ -92,7 +92,7 @@ def _custom_tasks(template, info) -> None: info.update_strings( device_automation={ **info.strings().get("device_automation", {}), - "condtion_type": { + "condition_type": { "is_on": "{entity_name} is on", "is_off": "{entity_name} is off", }, @@ -154,7 +154,7 @@ def _custom_tasks(template, info) -> None: "pick_implementation": {"title": "Pick Authentication Method"} }, "abort": { - "missing_configuration": "The Somfy component is not configured. Please follow the documentation." + "missing_configuration": "The {info.name} component is not configured. Please follow the documentation." }, "create_entry": { "default": f"Successfully authenticated with {info.name}." diff --git a/script/scaffold/templates/config_flow/integration/__init__.py b/script/scaffold/templates/config_flow/integration/__init__.py index 04b908952d1..4a206981c3c 100644 --- a/script/scaffold/templates/config_flow/integration/__init__.py +++ b/script/scaffold/templates/config_flow/integration/__init__.py @@ -3,8 +3,8 @@ import asyncio import voluptuous as vol -from homeassistant.core import HomeAssistant from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant from .const import DOMAIN diff --git a/script/scaffold/templates/config_flow/integration/config_flow.py b/script/scaffold/templates/config_flow/integration/config_flow.py index e08851f47a0..e2452b5324d 100644 --- a/script/scaffold/templates/config_flow/integration/config_flow.py +++ b/script/scaffold/templates/config_flow/integration/config_flow.py @@ -3,7 +3,7 @@ import logging import voluptuous as vol -from homeassistant import core, config_entries, exceptions +from homeassistant import config_entries, core, exceptions from .const import DOMAIN # pylint:disable=unused-import 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 35d8a96ab2b..b68adc897bb 100644 --- a/script/scaffold/templates/config_flow/tests/test_config_flow.py +++ b/script/scaffold/templates/config_flow/tests/test_config_flow.py @@ -2,8 +2,8 @@ from unittest.mock import patch from homeassistant import config_entries, setup -from homeassistant.components.NEW_DOMAIN.const import DOMAIN from homeassistant.components.NEW_DOMAIN.config_flow import CannotConnect, InvalidAuth +from homeassistant.components.NEW_DOMAIN.const import DOMAIN from tests.common import mock_coro diff --git a/script/scaffold/templates/config_flow_discovery/integration/__init__.py b/script/scaffold/templates/config_flow_discovery/integration/__init__.py index 04b908952d1..4a206981c3c 100644 --- a/script/scaffold/templates/config_flow_discovery/integration/__init__.py +++ b/script/scaffold/templates/config_flow_discovery/integration/__init__.py @@ -3,8 +3,8 @@ import asyncio import voluptuous as vol -from homeassistant.core import HomeAssistant from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant from .const import DOMAIN 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 16d13aaa99f..db5f719ce3d 100644 --- a/script/scaffold/templates/config_flow_discovery/integration/config_flow.py +++ b/script/scaffold/templates/config_flow_discovery/integration/config_flow.py @@ -1,8 +1,9 @@ """Config flow for NEW_NAME.""" import my_pypi_dependency -from homeassistant.helpers import config_entry_flow from homeassistant import config_entries +from homeassistant.helpers import config_entry_flow + from .const import DOMAIN diff --git a/script/scaffold/templates/config_flow_oauth2/integration/__init__.py b/script/scaffold/templates/config_flow_oauth2/integration/__init__.py index 30e7ad97810..d561f284caf 100644 --- a/script/scaffold/templates/config_flow_oauth2/integration/__init__.py +++ b/script/scaffold/templates/config_flow_oauth2/integration/__init__.py @@ -3,17 +3,17 @@ import asyncio import voluptuous as vol -from homeassistant.core import HomeAssistant -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET -from homeassistant.helpers import ( - config_validation as cv, - config_entry_oauth2_flow, - aiohttp_client, -) 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, +) -from .const import DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN from . import api, config_flow +from .const import DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN CONFIG_SCHEMA = vol.Schema( { diff --git a/script/scaffold/templates/config_flow_oauth2/integration/api.py b/script/scaffold/templates/config_flow_oauth2/integration/api.py index c5aa4a81ebe..f1a1f6a7ec4 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 asyncio import run_coroutine_threadsafe from aiohttp import ClientSession import my_pypi_package -from homeassistant import core, config_entries +from homeassistant import config_entries, core from homeassistant.helpers import config_entry_oauth2_flow # TODO the following two API examples are based on our suggested best practices diff --git a/script/scaffold/templates/config_flow_oauth2/integration/config_flow.py b/script/scaffold/templates/config_flow_oauth2/integration/config_flow.py index 1112a404e60..2343e1d79f8 100644 --- a/script/scaffold/templates/config_flow_oauth2/integration/config_flow.py +++ b/script/scaffold/templates/config_flow_oauth2/integration/config_flow.py @@ -3,6 +3,7 @@ import logging from homeassistant import config_entries from homeassistant.helpers import config_entry_oauth2_flow + from .const import DOMAIN _LOGGER = logging.getLogger(__name__) 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 7e61bcbfb1b..ec332de13e2 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 @@ -1,5 +1,5 @@ """Test the NEW_NAME config flow.""" -from homeassistant import config_entries, setup, data_entry_flow +from homeassistant import config_entries, setup from homeassistant.components.NEW_DOMAIN.const import ( DOMAIN, OAUTH2_AUTHORIZE, @@ -17,11 +17,7 @@ async def test_full_flow(hass, aiohttp_client, aioclient_mock): hass, "NEW_DOMAIN", { - "NEW_DOMAIN": { - "type": "oauth2", - "client_id": CLIENT_ID, - "client_secret": CLIENT_SECRET, - }, + "NEW_DOMAIN": {"client_id": CLIENT_ID, "client_secret": CLIENT_SECRET}, "http": {"base_url": "https://example.com"}, }, ) @@ -31,7 +27,6 @@ async def test_full_flow(hass, aiohttp_client, aioclient_mock): ) state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]}) - 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" @@ -56,5 +51,3 @@ async def test_full_flow(hass, aiohttp_client, aioclient_mock): result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - entry = hass.config_entries.async_entries(DOMAIN)[0] - assert entry.data["type"] == "oauth2" diff --git a/script/scaffold/templates/device_action/integration/device_action.py b/script/scaffold/templates/device_action/integration/device_action.py index d5674f01b2d..3861ee8ebe9 100644 --- a/script/scaffold/templates/device_action/integration/device_action.py +++ b/script/scaffold/templates/device_action/integration/device_action.py @@ -1,19 +1,21 @@ """Provides device automations for NEW_NAME.""" -from typing import Optional, List +from typing import List, Optional + import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, - CONF_DOMAIN, - CONF_TYPE, CONF_DEVICE_ID, + CONF_DOMAIN, CONF_ENTITY_ID, - SERVICE_TURN_ON, + CONF_TYPE, SERVICE_TURN_OFF, + SERVICE_TURN_ON, ) -from homeassistant.core import HomeAssistant, Context +from homeassistant.core import Context, HomeAssistant from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv + from . import DOMAIN # TODO specify your supported action types. 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 b65c8257531..3c7c7bb71a4 100644 --- a/script/scaffold/templates/device_action/tests/test_device_action.py +++ b/script/scaffold/templates/device_action/tests/test_device_action.py @@ -2,17 +2,17 @@ import pytest from homeassistant.components.NEW_DOMAIN import DOMAIN -from homeassistant.setup import async_setup_component import homeassistant.components.automation as automation from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, assert_lists_same, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, - async_get_device_automations, ) diff --git a/script/scaffold/templates/device_condition/integration/device_condition.py b/script/scaffold/templates/device_condition/integration/device_condition.py index 4b7baf68a37..1414636474d 100644 --- a/script/scaffold/templates/device_condition/integration/device_condition.py +++ b/script/scaffold/templates/device_condition/integration/device_condition.py @@ -1,21 +1,23 @@ """Provide the device automations for NEW_NAME.""" from typing import Dict, List + import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, CONF_CONDITION, - CONF_DOMAIN, - CONF_TYPE, CONF_DEVICE_ID, + CONF_DOMAIN, CONF_ENTITY_ID, + CONF_TYPE, STATE_OFF, STATE_ON, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import condition, config_validation as cv, entity_registry -from homeassistant.helpers.typing import ConfigType, TemplateVarsType from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA +from homeassistant.helpers.typing import ConfigType, TemplateVarsType + from . import DOMAIN # TODO specify your supported condition types. 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 1ae4df5f1b7..d58957030dc 100644 --- a/script/scaffold/templates/device_condition/tests/test_device_condition.py +++ b/script/scaffold/templates/device_condition/tests/test_device_condition.py @@ -2,18 +2,18 @@ import pytest from homeassistant.components.NEW_DOMAIN import DOMAIN -from homeassistant.const import STATE_ON, STATE_OFF -from homeassistant.setup import async_setup_component import homeassistant.components.automation as automation +from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, assert_lists_same, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, - async_get_device_automations, ) diff --git a/script/scaffold/templates/device_trigger/integration/device_trigger.py b/script/scaffold/templates/device_trigger/integration/device_trigger.py index e0741734d5f..a4f918684dc 100644 --- a/script/scaffold/templates/device_trigger/integration/device_trigger.py +++ b/script/scaffold/templates/device_trigger/integration/device_trigger.py @@ -1,21 +1,23 @@ """Provides device automations for NEW_NAME.""" from typing import List + import voluptuous as vol +from homeassistant.components.automation import AutomationActionType, state +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA from homeassistant.const import ( - CONF_DOMAIN, - CONF_TYPE, - CONF_PLATFORM, CONF_DEVICE_ID, + CONF_DOMAIN, CONF_ENTITY_ID, - STATE_ON, + CONF_PLATFORM, + CONF_TYPE, STATE_OFF, + STATE_ON, ) -from homeassistant.core import HomeAssistant, CALLBACK_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 homeassistant.components.automation import state, AutomationActionType -from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA + from . import DOMAIN # TODO specify your supported trigger types. 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 99e1f8937af..0ea584f474d 100644 --- a/script/scaffold/templates/device_trigger/tests/test_device_trigger.py +++ b/script/scaffold/templates/device_trigger/tests/test_device_trigger.py @@ -2,18 +2,18 @@ import pytest from homeassistant.components.NEW_DOMAIN import DOMAIN -from homeassistant.const import STATE_ON, STATE_OFF -from homeassistant.setup import async_setup_component import homeassistant.components.automation as automation +from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, assert_lists_same, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, - async_get_device_automations, ) diff --git a/script/scaffold/templates/integration/integration/__init__.py b/script/scaffold/templates/integration/integration/__init__.py index c2ae59aaad4..0ab65cb7da8 100644 --- a/script/scaffold/templates/integration/integration/__init__.py +++ b/script/scaffold/templates/integration/integration/__init__.py @@ -5,7 +5,6 @@ from homeassistant.core import HomeAssistant from .const import DOMAIN - CONFIG_SCHEMA = vol.Schema({vol.Optional(DOMAIN): {}}, extra=vol.ALLOW_EXTRA) diff --git a/script/scaffold/templates/integration/integration/manifest.json b/script/scaffold/templates/integration/integration/manifest.json index 0bc54519ce9..a95991abef8 100644 --- a/script/scaffold/templates/integration/integration/manifest.json +++ b/script/scaffold/templates/integration/integration/manifest.json @@ -4,7 +4,8 @@ "config_flow": false, "documentation": "https://www.home-assistant.io/integrations/NEW_DOMAIN", "requirements": [], - "ssdp": {}, + "ssdp": [], + "zeroconf": [], "homekit": {}, "dependencies": [], "codeowners": [] diff --git a/script/scaffold/templates/reproduce_state/integration/reproduce_state.py b/script/scaffold/templates/reproduce_state/integration/reproduce_state.py index 3449009818b..871142a5b00 100644 --- a/script/scaffold/templates/reproduce_state/integration/reproduce_state.py +++ b/script/scaffold/templates/reproduce_state/integration/reproduce_state.py @@ -5,10 +5,10 @@ from typing import Iterable, Optional from homeassistant.const import ( ATTR_ENTITY_ID, - STATE_ON, - STATE_OFF, SERVICE_TURN_OFF, SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, ) from homeassistant.core import Context, State from homeassistant.helpers.typing import HomeAssistantType diff --git a/script/translations_download_split.py b/script/translations_download_split.py index 375a9490e4e..86944ec9f90 100755 --- a/script/translations_download_split.py +++ b/script/translations_download_split.py @@ -4,7 +4,7 @@ import glob import json import os import re -from typing import Union, List, Dict +from typing import Dict, List, Union FILENAME_FORMAT = re.compile(r"strings\.(?P\w+)\.json") diff --git a/script/translations_upload_merge.py b/script/translations_upload_merge.py index c44727f690f..86de1f1842b 100755 --- a/script/translations_upload_merge.py +++ b/script/translations_upload_merge.py @@ -5,7 +5,7 @@ import itertools import json import os import re -from typing import Union, List, Dict +from typing import Dict, List, Union FILENAME_FORMAT = re.compile(r"strings\.(?P\w+)\.json") diff --git a/script/version_bump.py b/script/version_bump.py index de6638df30b..13dfe499f5e 100755 --- a/script/version_bump.py +++ b/script/version_bump.py @@ -1,9 +1,9 @@ #!/usr/bin/env python3 """Helper script to bump the current version.""" import argparse +from datetime import datetime import re import subprocess -from datetime import datetime from packaging.version import Version diff --git a/setup.cfg b/setup.cfg index bb2b1652ffa..f9e9852812c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -11,7 +11,6 @@ classifier = Intended Audience :: Developers License :: OSI Approved :: Apache Software License Operating System :: OS Independent - Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Topic :: Home Automation @@ -57,7 +56,7 @@ forced_separate = tests combine_as_imports = true [mypy] -python_version = 3.6 +python_version = 3.7 ignore_errors = true follow_imports = silent ignore_missing_imports = true @@ -65,7 +64,7 @@ warn_incomplete_stub = true warn_redundant_casts = true warn_unused_configs = true -[mypy-homeassistant.bootstrap,homeassistant.components,homeassistant.config_entries,homeassistant.config,homeassistant.const,homeassistant.core,homeassistant.data_entry_flow,homeassistant.exceptions,homeassistant.loader,homeassistant.__main__,homeassistant.monkey_patch,homeassistant.requirements,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.image_processing.*,homeassistant.components.integration.*,homeassistant.components.light.*,homeassistant.components.lock.*,homeassistant.components.mailbox.*,homeassistant.components.media_player.*,homeassistant.components.notify.*,homeassistant.components.persistent_notification.*,homeassistant.components.proximity.*,homeassistant.components.remote.*,homeassistant.components.scene.*,homeassistant.components.sensor.*,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.helpers.*,homeassistant.scripts.*,homeassistant.util.*] +[mypy-homeassistant.bootstrap,homeassistant.components,homeassistant.config_entries,homeassistant.config,homeassistant.const,homeassistant.core,homeassistant.data_entry_flow,homeassistant.exceptions,homeassistant.loader,homeassistant.__main__,homeassistant.requirements,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.image_processing.*,homeassistant.components.integration.*,homeassistant.components.light.*,homeassistant.components.lock.*,homeassistant.components.mailbox.*,homeassistant.components.media_player.*,homeassistant.components.notify.*,homeassistant.components.persistent_notification.*,homeassistant.components.proximity.*,homeassistant.components.remote.*,homeassistant.components.scene.*,homeassistant.components.sensor.*,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.helpers.*,homeassistant.scripts.*,homeassistant.util.*] ignore_errors = false check_untyped_defs = true disallow_incomplete_defs = true diff --git a/setup.py b/setup.py index 99195ee2f56..cf84577b558 100755 --- a/setup.py +++ b/setup.py @@ -1,7 +1,8 @@ #!/usr/bin/env python3 """Home Assistant setup script.""" from datetime import datetime as dt -from setuptools import setup, find_packages + +from setuptools import find_packages, setup import homeassistant.const as hass_const @@ -36,9 +37,8 @@ REQUIRES = [ "async_timeout==3.0.1", "attrs==19.3.0", "bcrypt==3.1.7", - "certifi>=2019.9.11", - 'contextvars==2.4;python_version<"3.7"', - "importlib-metadata==0.23", + "certifi>=2019.11.28", + "importlib-metadata==1.3.0", "jinja2>=2.10.3", "PyJWT==1.7.1", # PyJWT has loose dependency. We want the latest one. @@ -46,7 +46,7 @@ REQUIRES = [ "pip>=8.0.3", "python-slugify==4.0.0", "pytz>=2019.03", - "pyyaml==5.1.2", + "pyyaml==5.2.0", "requests==2.22.0", "ruamel.yaml==0.15.100", "voluptuous==0.11.7", diff --git a/tests/auth/mfa_modules/test_insecure_example.py b/tests/auth/mfa_modules/test_insecure_example.py index dc233cb53ff..5384ebee4bd 100644 --- a/tests/auth/mfa_modules/test_insecure_example.py +++ b/tests/auth/mfa_modules/test_insecure_example.py @@ -2,6 +2,7 @@ from homeassistant import auth, data_entry_flow from homeassistant.auth.mfa_modules import auth_mfa_module_from_config from homeassistant.auth.models import Credentials + from tests.common import MockUser diff --git a/tests/auth/mfa_modules/test_notify.py b/tests/auth/mfa_modules/test_notify.py index 20ed104b375..bc4ecaab712 100644 --- a/tests/auth/mfa_modules/test_notify.py +++ b/tests/auth/mfa_modules/test_notify.py @@ -3,9 +3,10 @@ import asyncio from unittest.mock import patch from homeassistant import data_entry_flow -from homeassistant.auth import models as auth_models, auth_manager_from_config +from homeassistant.auth import auth_manager_from_config, models as auth_models from homeassistant.auth.mfa_modules import auth_mfa_module_from_config from homeassistant.components.notify import NOTIFY_SERVICE_SCHEMA + from tests.common import MockUser, async_mock_service MOCK_CODE = "123456" diff --git a/tests/auth/mfa_modules/test_totp.py b/tests/auth/mfa_modules/test_totp.py index e53e3030cd2..d0a4f3cf3ac 100644 --- a/tests/auth/mfa_modules/test_totp.py +++ b/tests/auth/mfa_modules/test_totp.py @@ -3,8 +3,9 @@ import asyncio from unittest.mock import patch from homeassistant import data_entry_flow -from homeassistant.auth import models as auth_models, auth_manager_from_config +from homeassistant.auth import auth_manager_from_config, models as auth_models from homeassistant.auth.mfa_modules import auth_mfa_module_from_config + from tests.common import MockUser MOCK_CODE = "123456" diff --git a/tests/auth/permissions/test_entities.py b/tests/auth/permissions/test_entities.py index c2305b5c203..a929984d152 100644 --- a/tests/auth/permissions/test_entities.py +++ b/tests/auth/permissions/test_entities.py @@ -3,14 +3,14 @@ import pytest import voluptuous as vol from homeassistant.auth.permissions.entities import ( - compile_entities, ENTITY_POLICY_SCHEMA, + compile_entities, ) from homeassistant.auth.permissions.models import PermissionLookup -from homeassistant.helpers.entity_registry import RegistryEntry from homeassistant.helpers.device_registry import DeviceEntry +from homeassistant.helpers.entity_registry import RegistryEntry -from tests.common import mock_registry, mock_device_registry +from tests.common import mock_device_registry, mock_registry def test_entities_none(): diff --git a/tests/auth/permissions/test_system_policies.py b/tests/auth/permissions/test_system_policies.py index 2710fdd68c3..58515e2ad2a 100644 --- a/tests/auth/permissions/test_system_policies.py +++ b/tests/auth/permissions/test_system_policies.py @@ -1,8 +1,8 @@ """Test system policies.""" from homeassistant.auth.permissions import ( + POLICY_SCHEMA, PolicyPermissions, system_policies, - POLICY_SCHEMA, ) diff --git a/tests/auth/providers/test_command_line.py b/tests/auth/providers/test_command_line.py index 33b1735457f..abcf124b9c4 100644 --- a/tests/auth/providers/test_command_line.py +++ b/tests/auth/providers/test_command_line.py @@ -1,13 +1,13 @@ """Tests for the command_line auth provider.""" -from unittest.mock import Mock import os +from unittest.mock import Mock import uuid import pytest from homeassistant import data_entry_flow -from homeassistant.auth import auth_store, models as auth_models, AuthManager +from homeassistant.auth import AuthManager, auth_store, models as auth_models from homeassistant.auth.providers import command_line from homeassistant.const import CONF_TYPE diff --git a/tests/auth/providers/test_insecure_example.py b/tests/auth/providers/test_insecure_example.py index 43f5aff290e..c5b3a8db038 100644 --- a/tests/auth/providers/test_insecure_example.py +++ b/tests/auth/providers/test_insecure_example.py @@ -4,7 +4,7 @@ import uuid import pytest -from homeassistant.auth import auth_store, models as auth_models, AuthManager +from homeassistant.auth import AuthManager, auth_store, models as auth_models from homeassistant.auth.providers import insecure_example from tests.common import mock_coro diff --git a/tests/auth/test_init.py b/tests/auth/test_init.py index 73b383fb0fe..aa047a12d54 100644 --- a/tests/auth/test_init.py +++ b/tests/auth/test_init.py @@ -7,11 +7,12 @@ import pytest import voluptuous as vol from homeassistant import auth, data_entry_flow -from homeassistant.auth import models as auth_models, auth_store, const as auth_const +from homeassistant.auth import auth_store, const as auth_const, models as auth_models from homeassistant.auth.const import MFA_SESSION_EXPIRATION from homeassistant.core import callback from homeassistant.util import dt as dt_util -from tests.common import MockUser, ensure_auth_manager_loaded, flush_store, CLIENT_ID + +from tests.common import CLIENT_ID, MockUser, ensure_auth_manager_loaded, flush_store @pytest.fixture diff --git a/tests/bandit.yaml b/tests/bandit.yaml new file mode 100644 index 00000000000..79812cba56f --- /dev/null +++ b/tests/bandit.yaml @@ -0,0 +1,11 @@ +# https://bandit.readthedocs.io/en/latest/config.html + +tests: + - B313 + - B314 + - B315 + - B316 + - B317 + - B318 + - B319 + - B320 diff --git a/tests/common.py b/tests/common.py index e652e10cc54..e57710c46cc 100644 --- a/tests/common.py +++ b/tests/common.py @@ -1,32 +1,32 @@ """Test the helper method for writing tests.""" import asyncio import collections -import functools as ft -import json -import logging -import os -import uuid -import sys -import threading - from collections import OrderedDict from contextlib import contextmanager from datetime import timedelta +import functools as ft from io import StringIO +import json +import logging +import os +import sys +import threading from unittest.mock import MagicMock, Mock, patch - -import homeassistant.util.dt as date_util -import homeassistant.util.yaml.loader as yaml_loader +import uuid from homeassistant import auth, config_entries, core as ha, loader from homeassistant.auth import ( - models as auth_models, auth_store, - providers as auth_providers, + models as auth_models, permissions as auth_permissions, + providers as auth_providers, ) from homeassistant.auth.permissions import system_policies from homeassistant.components import mqtt, recorder +from homeassistant.components.device_automation import ( # noqa: F401 + _async_get_device_automation_capabilities as async_get_device_automation_capabilities, + _async_get_device_automations as async_get_device_automations, +) from homeassistant.components.mqtt.models import Message from homeassistant.config import async_process_component_config from homeassistant.const import ( @@ -38,8 +38,8 @@ from homeassistant.const import ( EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, SERVER_PORT, - STATE_ON, STATE_OFF, + STATE_ON, ) from homeassistant.core import State from homeassistant.helpers import ( @@ -54,12 +54,10 @@ from homeassistant.helpers import ( ) from homeassistant.helpers.json import JSONEncoder from homeassistant.setup import async_setup_component, setup_component -from homeassistant.util.unit_system import METRIC_SYSTEM from homeassistant.util.async_ import run_callback_threadsafe -from homeassistant.components.device_automation import ( # noqa: F401 - _async_get_device_automations as async_get_device_automations, - _async_get_device_automation_capabilities as async_get_device_automation_capabilities, -) +import homeassistant.util.dt as date_util +from homeassistant.util.unit_system import METRIC_SYSTEM +import homeassistant.util.yaml.loader as yaml_loader _TEST_INSTANCE_PORT = SERVER_PORT _LOGGER = logging.getLogger(__name__) @@ -673,6 +671,7 @@ class MockConfigEntry(config_entries.ConfigEntry): options={}, system_options={}, connection_class=config_entries.CONN_CLASS_UNKNOWN, + unique_id=None, ): """Initialize a mock config entry.""" kwargs = { @@ -684,6 +683,7 @@ class MockConfigEntry(config_entries.ConfigEntry): "version": version, "title": title, "connection_class": connection_class, + "unique_id": unique_id, } if source is not None: kwargs["source"] = source @@ -906,6 +906,11 @@ class MockEntity(entity.Entity): """Return the unique ID of the entity.""" return self._handle("unique_id") + @property + def state(self): + """Return the state of the entity.""" + return self._handle("state") + @property def available(self): """Return True if entity is available.""" @@ -916,6 +921,21 @@ class MockEntity(entity.Entity): """Info how it links to a device.""" return self._handle("device_info") + @property + def device_class(self): + """Info how device should be classified.""" + return self._handle("device_class") + + @property + def capability_attributes(self): + """Info about capabilities.""" + return self._handle("capability_attributes") + + @property + def supported_features(self): + """Info about supported features.""" + return self._handle("supported_features") + @property def entity_registry_enabled_default(self): """Return if the entity should be enabled when first added to the entity registry.""" diff --git a/tests/components/abode/test_config_flow.py b/tests/components/abode/test_config_flow.py index c3f5d170767..5e32c923245 100644 --- a/tests/components/abode/test_config_flow.py +++ b/tests/components/abode/test_config_flow.py @@ -6,6 +6,7 @@ from abodepy.exceptions import AbodeAuthenticationException from homeassistant import data_entry_flow from homeassistant.components.abode import config_flow from homeassistant.const import CONF_PASSWORD, CONF_USERNAME + from tests.common import MockConfigEntry CONF_POLLING = "polling" diff --git a/tests/components/airly/test_config_flow.py b/tests/components/airly/test_config_flow.py index 8b615b34c2a..a5ca3981a5a 100644 --- a/tests/components/airly/test_config_flow.py +++ b/tests/components/airly/test_config_flow.py @@ -5,11 +5,11 @@ from airly.exceptions import AirlyError from asynctest import patch from homeassistant import data_entry_flow -from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.components.airly import config_flow from homeassistant.components.airly.const import DOMAIN +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME -from tests.common import load_fixture, MockConfigEntry +from tests.common import MockConfigEntry, load_fixture CONFIG = { CONF_NAME: "abcd", diff --git a/tests/components/alarm_control_panel/common.py b/tests/components/alarm_control_panel/common.py index 216f226ef14..ce0bde0517c 100644 --- a/tests/components/alarm_control_panel/common.py +++ b/tests/components/alarm_control_panel/common.py @@ -7,17 +7,18 @@ from homeassistant.components.alarm_control_panel import DOMAIN from homeassistant.const import ( ATTR_CODE, ATTR_ENTITY_ID, - SERVICE_ALARM_TRIGGER, - SERVICE_ALARM_DISARM, - SERVICE_ALARM_ARM_HOME, + ENTITY_MATCH_ALL, SERVICE_ALARM_ARM_AWAY, - SERVICE_ALARM_ARM_NIGHT, SERVICE_ALARM_ARM_CUSTOM_BYPASS, + SERVICE_ALARM_ARM_HOME, + SERVICE_ALARM_ARM_NIGHT, + SERVICE_ALARM_DISARM, + SERVICE_ALARM_TRIGGER, ) from homeassistant.loader import bind_hass -async def async_alarm_disarm(hass, code=None, entity_id=None): +async def async_alarm_disarm(hass, code=None, entity_id=ENTITY_MATCH_ALL): """Send the alarm the command for disarm.""" data = {} if code: @@ -29,7 +30,7 @@ async def async_alarm_disarm(hass, code=None, entity_id=None): @bind_hass -def alarm_disarm(hass, code=None, entity_id=None): +def alarm_disarm(hass, code=None, entity_id=ENTITY_MATCH_ALL): """Send the alarm the command for disarm.""" data = {} if code: @@ -40,7 +41,7 @@ def alarm_disarm(hass, code=None, entity_id=None): hass.services.call(DOMAIN, SERVICE_ALARM_DISARM, data) -async def async_alarm_arm_home(hass, code=None, entity_id=None): +async def async_alarm_arm_home(hass, code=None, entity_id=ENTITY_MATCH_ALL): """Send the alarm the command for disarm.""" data = {} if code: @@ -52,7 +53,7 @@ async def async_alarm_arm_home(hass, code=None, entity_id=None): @bind_hass -def alarm_arm_home(hass, code=None, entity_id=None): +def alarm_arm_home(hass, code=None, entity_id=ENTITY_MATCH_ALL): """Send the alarm the command for arm home.""" data = {} if code: @@ -63,7 +64,7 @@ def alarm_arm_home(hass, code=None, entity_id=None): hass.services.call(DOMAIN, SERVICE_ALARM_ARM_HOME, data) -async def async_alarm_arm_away(hass, code=None, entity_id=None): +async def async_alarm_arm_away(hass, code=None, entity_id=ENTITY_MATCH_ALL): """Send the alarm the command for disarm.""" data = {} if code: @@ -75,7 +76,7 @@ async def async_alarm_arm_away(hass, code=None, entity_id=None): @bind_hass -def alarm_arm_away(hass, code=None, entity_id=None): +def alarm_arm_away(hass, code=None, entity_id=ENTITY_MATCH_ALL): """Send the alarm the command for arm away.""" data = {} if code: @@ -86,7 +87,7 @@ def alarm_arm_away(hass, code=None, entity_id=None): hass.services.call(DOMAIN, SERVICE_ALARM_ARM_AWAY, data) -async def async_alarm_arm_night(hass, code=None, entity_id=None): +async def async_alarm_arm_night(hass, code=None, entity_id=ENTITY_MATCH_ALL): """Send the alarm the command for disarm.""" data = {} if code: @@ -98,7 +99,7 @@ async def async_alarm_arm_night(hass, code=None, entity_id=None): @bind_hass -def alarm_arm_night(hass, code=None, entity_id=None): +def alarm_arm_night(hass, code=None, entity_id=ENTITY_MATCH_ALL): """Send the alarm the command for arm night.""" data = {} if code: @@ -109,7 +110,7 @@ def alarm_arm_night(hass, code=None, entity_id=None): hass.services.call(DOMAIN, SERVICE_ALARM_ARM_NIGHT, data) -async def async_alarm_trigger(hass, code=None, entity_id=None): +async def async_alarm_trigger(hass, code=None, entity_id=ENTITY_MATCH_ALL): """Send the alarm the command for disarm.""" data = {} if code: @@ -121,7 +122,7 @@ async def async_alarm_trigger(hass, code=None, entity_id=None): @bind_hass -def alarm_trigger(hass, code=None, entity_id=None): +def alarm_trigger(hass, code=None, entity_id=ENTITY_MATCH_ALL): """Send the alarm the command for trigger.""" data = {} if code: @@ -132,7 +133,7 @@ def alarm_trigger(hass, code=None, entity_id=None): hass.services.call(DOMAIN, SERVICE_ALARM_TRIGGER, data) -async def async_alarm_arm_custom_bypass(hass, code=None, entity_id=None): +async def async_alarm_arm_custom_bypass(hass, code=None, entity_id=ENTITY_MATCH_ALL): """Send the alarm the command for disarm.""" data = {} if code: @@ -146,7 +147,7 @@ async def async_alarm_arm_custom_bypass(hass, code=None, entity_id=None): @bind_hass -def alarm_arm_custom_bypass(hass, code=None, entity_id=None): +def alarm_arm_custom_bypass(hass, code=None, entity_id=ENTITY_MATCH_ALL): """Send the alarm the command for arm custom bypass.""" data = {} if code: diff --git a/tests/components/alarm_control_panel/test_device_action.py b/tests/components/alarm_control_panel/test_device_action.py index c2dfcbd78b9..72754c3c96f 100644 --- a/tests/components/alarm_control_panel/test_device_action.py +++ b/tests/components/alarm_control_panel/test_device_action.py @@ -2,6 +2,7 @@ import pytest from homeassistant.components.alarm_control_panel import DOMAIN +import homeassistant.components.automation as automation from homeassistant.const import ( CONF_PLATFORM, STATE_ALARM_ARMED_AWAY, @@ -11,17 +12,16 @@ from homeassistant.const import ( STATE_ALARM_TRIGGERED, STATE_UNKNOWN, ) -from homeassistant.setup import async_setup_component -import homeassistant.components.automation as automation from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, assert_lists_same, + async_get_device_automation_capabilities, + async_get_device_automations, mock_device_registry, mock_registry, - async_get_device_automations, - async_get_device_automation_capabilities, ) @@ -46,6 +46,9 @@ async def test_get_actions(hass, device_reg, entity_reg): 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} + ) expected_actions = [ { "domain": DOMAIN, @@ -82,6 +85,36 @@ async def test_get_actions(hass, device_reg, entity_reg): assert_lists_same(actions, expected_actions) +async def test_get_actions_arm_night_only(hass, device_reg, entity_reg): + """Test we get the expected actions from a 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": 4} + ) + expected_actions = [ + { + "domain": DOMAIN, + "type": "arm_night", + "device_id": device_entry.id, + "entity_id": "alarm_control_panel.test_5678", + }, + { + "domain": DOMAIN, + "type": "disarm", + "device_id": device_entry.id, + "entity_id": "alarm_control_panel.test_5678", + }, + ] + actions = await async_get_device_automations(hass, "action", device_entry.id) + assert_lists_same(actions, expected_actions) + + async def test_get_action_capabilities(hass, device_reg, entity_reg): """Test we get the expected capabilities from a sensor trigger.""" platform = getattr(hass.components, f"test.{DOMAIN}") diff --git a/tests/components/alarm_control_panel/test_device_trigger.py b/tests/components/alarm_control_panel/test_device_trigger.py new file mode 100644 index 00000000000..ec14cefc291 --- /dev/null +++ b/tests/components/alarm_control_panel/test_device_trigger.py @@ -0,0 +1,257 @@ +"""The tests for Alarm control panel device triggers.""" +import pytest + +from homeassistant.components.alarm_control_panel import DOMAIN +import homeassistant.components.automation as automation +from homeassistant.const import ( + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, + STATE_ALARM_PENDING, + STATE_ALARM_TRIGGERED, +) +from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component + +from tests.common import ( + MockConfigEntry, + assert_lists_same, + async_get_device_automations, + async_mock_service, + mock_device_registry, + mock_registry, +) + + +@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 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} + ) + expected_triggers = [ + { + "platform": "device", + "domain": DOMAIN, + "type": "disarmed", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "platform": "device", + "domain": DOMAIN, + "type": "triggered", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "platform": "device", + "domain": DOMAIN, + "type": "armed_home", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "platform": "device", + "domain": DOMAIN, + "type": "armed_away", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "platform": "device", + "domain": DOMAIN, + "type": "armed_night", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + ] + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert_lists_same(triggers, expected_triggers) + + +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) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "alarm_control_panel.entity", + "type": "triggered", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "triggered - {{ trigger.platform}} - " + "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " + "{{ trigger.to_state.state}} - {{ trigger.for }}" + ) + }, + }, + }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "alarm_control_panel.entity", + "type": "disarmed", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "disarmed - {{ trigger.platform}} - " + "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " + "{{ trigger.to_state.state}} - {{ trigger.for }}" + ) + }, + }, + }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "alarm_control_panel.entity", + "type": "armed_home", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "armed_home - {{ trigger.platform}} - " + "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " + "{{ trigger.to_state.state}} - {{ trigger.for }}" + ) + }, + }, + }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "alarm_control_panel.entity", + "type": "armed_away", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "armed_away - {{ trigger.platform}} - " + "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " + "{{ trigger.to_state.state}} - {{ trigger.for }}" + ) + }, + }, + }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "alarm_control_panel.entity", + "type": "armed_night", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "armed_night - {{ trigger.platform}} - " + "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " + "{{ trigger.to_state.state}} - {{ trigger.for }}" + ) + }, + }, + }, + ] + }, + ) + + # Fake that the entity is triggered. + hass.states.async_set("alarm_control_panel.entity", STATE_ALARM_TRIGGERED) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data[ + "some" + ] == "triggered - device - {} - pending - triggered - None".format( + "alarm_control_panel.entity" + ) + + # Fake that the entity is disarmed. + hass.states.async_set("alarm_control_panel.entity", STATE_ALARM_DISARMED) + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data[ + "some" + ] == "disarmed - device - {} - triggered - disarmed - None".format( + "alarm_control_panel.entity" + ) + + # 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 - {} - pending - armed_home - None".format( + "alarm_control_panel.entity" + ) + + # 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 - {} - pending - armed_away - None".format( + "alarm_control_panel.entity" + ) + + # 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 - {} - pending - armed_night - None".format( + "alarm_control_panel.entity" + ) diff --git a/tests/components/alert/test_init.py b/tests/components/alert/test_init.py index bcdb2058a42..55a3112c32f 100644 --- a/tests/components/alert/test_init.py +++ b/tests/components/alert/test_init.py @@ -1,26 +1,26 @@ """The tests for the Alert component.""" -import unittest - # pylint: disable=protected-access from copy import deepcopy +import unittest import homeassistant.components.alert as alert -import homeassistant.components.notify as notify from homeassistant.components.alert import DOMAIN +import homeassistant.components.notify as notify from homeassistant.const import ( ATTR_ENTITY_ID, CONF_ENTITY_ID, - STATE_IDLE, CONF_NAME, CONF_STATE, SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, - STATE_ON, + STATE_IDLE, STATE_OFF, + STATE_ON, ) from homeassistant.core import callback from homeassistant.setup import setup_component + from tests.common import get_test_home_assistant NAME = "alert_test" diff --git a/tests/components/alexa/__init__.py b/tests/components/alexa/__init__.py index 0fa1961ad61..473a3c6e12b 100644 --- a/tests/components/alexa/__init__.py +++ b/tests/components/alexa/__init__.py @@ -1,8 +1,8 @@ """Tests for the Alexa integration.""" from uuid import uuid4 -from homeassistant.core import Context from homeassistant.components.alexa import config, smart_home +from homeassistant.core import Context from tests.common import async_mock_service @@ -13,7 +13,12 @@ TEST_TOKEN_URL = "https://api.amazon.com/auth/o2/token" class MockConfig(config.AbstractConfig): """Mock Alexa config.""" - entity_config = {"binary_sensor.test_doorbell": {"display_categories": "DOORBELL"}} + entity_config = { + "binary_sensor.test_doorbell": {"display_categories": "DOORBELL"}, + "binary_sensor.test_contact_forced": {"display_categories": "CONTACT_SENSOR"}, + "binary_sensor.test_motion_forced": {"display_categories": "MOTION_SENSOR"}, + "binary_sensor.test_motion_camera_event": {"display_categories": "CAMERA"}, + } @property def supports_auth(self): diff --git a/tests/components/alexa/test_auth.py b/tests/components/alexa/test_auth.py index ecd8f1eb4b3..9d14fffe7e4 100644 --- a/tests/components/alexa/test_auth.py +++ b/tests/components/alexa/test_auth.py @@ -1,5 +1,6 @@ """Test Alexa auth endpoints.""" from homeassistant.components.alexa.auth import Auth + from . import TEST_TOKEN_URL diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py index d2f6cddc522..9c086e1fc50 100644 --- a/tests/components/alexa/test_capabilities.py +++ b/tests/components/alexa/test_capabilities.py @@ -1,37 +1,38 @@ """Test Alexa capabilities.""" import pytest -from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, - TEMP_CELSIUS, - STATE_LOCKED, - STATE_UNLOCKED, - STATE_UNKNOWN, - STATE_UNAVAILABLE, - STATE_ALARM_DISARMED, - STATE_ALARM_ARMED_AWAY, - STATE_ALARM_ARMED_CUSTOM_BYPASS, - STATE_ALARM_ARMED_HOME, - STATE_ALARM_ARMED_NIGHT, -) -from homeassistant.components.climate import const as climate from homeassistant.components.alexa import smart_home from homeassistant.components.alexa.errors import UnsupportedProperty +from homeassistant.components.climate import const as climate from homeassistant.components.media_player.const import ( SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_STOP, ) -from tests.common import async_mock_service +from homeassistant.const import ( + ATTR_UNIT_OF_MEASUREMENT, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_CUSTOM_BYPASS, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, + STATE_LOCKED, + STATE_UNAVAILABLE, + STATE_UNKNOWN, + STATE_UNLOCKED, + TEMP_CELSIUS, +) from . import ( DEFAULT_CONFIG, - get_new_request, assert_request_calls_service, assert_request_fails, + get_new_request, reported_properties, ) +from tests.common import async_mock_service + @pytest.mark.parametrize("result,adjust", [(25, "-5"), (35, "5"), (0, "-80")]) async def test_api_adjust_brightness(hass, result, adjust): @@ -410,14 +411,14 @@ async def test_report_fan_direction(hass): properties.assert_not_has_property("Alexa.ModeController", "mode") properties = await reported_properties(hass, "fan.reverse") - properties.assert_equal("Alexa.ModeController", "mode", "reverse") + properties.assert_equal("Alexa.ModeController", "mode", "direction.reverse") properties = await reported_properties(hass, "fan.forward") - properties.assert_equal("Alexa.ModeController", "mode", "forward") + properties.assert_equal("Alexa.ModeController", "mode", "direction.forward") -async def test_report_cover_percentage_state(hass): - """Test PercentageController reports cover percentage correctly.""" +async def test_report_cover_range_value(hass): + """Test RangeController reports cover position correctly.""" hass.states.async_set( "cover.fully_open", "open", @@ -447,13 +448,13 @@ async def test_report_cover_percentage_state(hass): ) properties = await reported_properties(hass, "cover.fully_open") - properties.assert_equal("Alexa.PercentageController", "percentage", 100) + properties.assert_equal("Alexa.RangeController", "rangeValue", 100) properties = await reported_properties(hass, "cover.half_open") - properties.assert_equal("Alexa.PercentageController", "percentage", 50) + properties.assert_equal("Alexa.RangeController", "rangeValue", 50) properties = await reported_properties(hass, "cover.closed") - properties.assert_equal("Alexa.PercentageController", "percentage", 0) + properties.assert_equal("Alexa.RangeController", "rangeValue", 0) async def test_report_climate_state(hass): @@ -666,3 +667,45 @@ async def test_report_playback_state(hass): properties.assert_equal( "Alexa.PlaybackStateReporter", "playbackState", {"state": "STOPPED"} ) + + +async def test_report_image_processing(hass): + """Test EventDetectionSensor implements humanPresenceDetectionState property.""" + hass.states.async_set( + "image_processing.test_face", + 0, + { + "friendly_name": "Test face", + "device_class": "face", + "faces": [], + "total_faces": 0, + }, + ) + + properties = await reported_properties(hass, "image_processing#test_face") + properties.assert_equal( + "Alexa.EventDetectionSensor", + "humanPresenceDetectionState", + {"value": "NOT_DETECTED"}, + ) + + hass.states.async_set( + "image_processing.test_classifier", + 3, + { + "friendly_name": "Test classifier", + "device_class": "face", + "faces": [ + {"confidence": 98.34, "name": "Hans", "age": 16.0, "gender": "male"}, + {"name": "Helena", "age": 28.0, "gender": "female"}, + {"confidence": 62.53, "name": "Luna"}, + ], + "total_faces": 3, + }, + ) + properties = await reported_properties(hass, "image_processing#test_classifier") + properties.assert_equal( + "Alexa.EventDetectionSensor", + "humanPresenceDetectionState", + {"value": "DETECTED"}, + ) diff --git a/tests/components/alexa/test_entities.py b/tests/components/alexa/test_entities.py index 7436306fe25..8cae4fb9b77 100644 --- a/tests/components/alexa/test_entities.py +++ b/tests/components/alexa/test_entities.py @@ -1,6 +1,7 @@ """Test Alexa entity representation.""" from homeassistant.components.alexa import smart_home -from . import get_new_request, DEFAULT_CONFIG + +from . import DEFAULT_CONFIG, get_new_request async def test_unsupported_domain(hass): diff --git a/tests/components/alexa/test_flash_briefings.py b/tests/components/alexa/test_flash_briefings.py index 4c94046d41a..d3fe28d227d 100644 --- a/tests/components/alexa/test_flash_briefings.py +++ b/tests/components/alexa/test_flash_briefings.py @@ -1,14 +1,13 @@ """The tests for the Alexa component.""" # pylint: disable=protected-access -import asyncio import datetime import pytest -from homeassistant.core import callback -from homeassistant.setup import async_setup_component from homeassistant.components import alexa from homeassistant.components.alexa import const +from homeassistant.core import callback +from homeassistant.setup import async_setup_component SESSION_ID = "amzn1.echo-api.session.0000000-0000-0000-0000-00000000000" APPLICATION_ID = "amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00ebe" @@ -67,21 +66,19 @@ def _flash_briefing_req(client, briefing_id): return client.get("/api/alexa/flash_briefings/{}".format(briefing_id)) -@asyncio.coroutine -def test_flash_briefing_invalid_id(alexa_client): +async def test_flash_briefing_invalid_id(alexa_client): """Test an invalid Flash Briefing ID.""" - req = yield from _flash_briefing_req(alexa_client, 10000) + req = await _flash_briefing_req(alexa_client, 10000) assert req.status == 404 - text = yield from req.text() + text = await req.text() assert text == "" -@asyncio.coroutine -def test_flash_briefing_date_from_str(alexa_client): +async def test_flash_briefing_date_from_str(alexa_client): """Test the response has a valid date parsed from string.""" - req = yield from _flash_briefing_req(alexa_client, "weather") + req = await _flash_briefing_req(alexa_client, "weather") assert req.status == 200 - data = yield from req.json() + data = await req.json() assert isinstance( datetime.datetime.strptime( data[0].get(const.ATTR_UPDATE_DATE), const.DATE_FORMAT @@ -90,8 +87,7 @@ def test_flash_briefing_date_from_str(alexa_client): ) -@asyncio.coroutine -def test_flash_briefing_valid(alexa_client): +async def test_flash_briefing_valid(alexa_client): """Test the response is valid.""" data = [ { @@ -104,9 +100,9 @@ def test_flash_briefing_valid(alexa_client): } ] - req = yield from _flash_briefing_req(alexa_client, "news_audio") + req = await _flash_briefing_req(alexa_client, "news_audio") assert req.status == 200 - json = yield from req.json() + json = await req.json() assert isinstance( datetime.datetime.strptime( json[0].get(const.ATTR_UPDATE_DATE), const.DATE_FORMAT diff --git a/tests/components/alexa/test_intent.py b/tests/components/alexa/test_intent.py index 0b7fc5f22c8..962ba677403 100644 --- a/tests/components/alexa/test_intent.py +++ b/tests/components/alexa/test_intent.py @@ -1,14 +1,13 @@ """The tests for the Alexa component.""" # pylint: disable=protected-access -import asyncio import json import pytest -from homeassistant.core import callback -from homeassistant.setup import async_setup_component from homeassistant.components import alexa from homeassistant.components.alexa import intent +from homeassistant.core import callback +from homeassistant.setup import async_setup_component SESSION_ID = "amzn1.echo-api.session.0000000-0000-0000-0000-00000000000" APPLICATION_ID = "amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00ebe" @@ -115,8 +114,7 @@ def _intent_req(client, data=None): ) -@asyncio.coroutine -def test_intent_launch_request(alexa_client): +async def test_intent_launch_request(alexa_client): """Test the launch of a request.""" data = { "version": "1.0", @@ -133,15 +131,14 @@ def test_intent_launch_request(alexa_client): "timestamp": "2015-05-13T12:34:56Z", }, } - req = yield from _intent_req(alexa_client, data) + req = await _intent_req(alexa_client, data) assert req.status == 200 - data = yield from req.json() + data = await req.json() text = data.get("response", {}).get("outputSpeech", {}).get("text") assert text == "LaunchRequest has been received." -@asyncio.coroutine -def test_intent_launch_request_not_configured(alexa_client): +async def test_intent_launch_request_not_configured(alexa_client): """Test the launch of a request.""" data = { "version": "1.0", @@ -160,15 +157,14 @@ def test_intent_launch_request_not_configured(alexa_client): "timestamp": "2015-05-13T12:34:56Z", }, } - req = yield from _intent_req(alexa_client, data) + req = await _intent_req(alexa_client, data) assert req.status == 200 - data = yield from req.json() + data = await req.json() text = data.get("response", {}).get("outputSpeech", {}).get("text") assert text == "This intent is not yet configured within Home Assistant." -@asyncio.coroutine -def test_intent_request_with_slots(alexa_client): +async def test_intent_request_with_slots(alexa_client): """Test a request with slots.""" data = { "version": "1.0", @@ -195,15 +191,14 @@ def test_intent_request_with_slots(alexa_client): }, }, } - req = yield from _intent_req(alexa_client, data) + req = await _intent_req(alexa_client, data) assert req.status == 200 - data = yield from req.json() + data = await req.json() text = data.get("response", {}).get("outputSpeech", {}).get("text") assert text == "You told us your sign is virgo." -@asyncio.coroutine -def test_intent_request_with_slots_and_synonym_resolution(alexa_client): +async def test_intent_request_with_slots_and_synonym_resolution(alexa_client): """Test a request with slots and a name synonym.""" data = { "version": "1.0", @@ -249,15 +244,14 @@ def test_intent_request_with_slots_and_synonym_resolution(alexa_client): }, }, } - req = yield from _intent_req(alexa_client, data) + req = await _intent_req(alexa_client, data) assert req.status == 200 - data = yield from req.json() + data = await req.json() text = data.get("response", {}).get("outputSpeech", {}).get("text") assert text == "You told us your sign is Virgo." -@asyncio.coroutine -def test_intent_request_with_slots_and_multi_synonym_resolution(alexa_client): +async def test_intent_request_with_slots_and_multi_synonym_resolution(alexa_client): """Test a request with slots and multiple name synonyms.""" data = { "version": "1.0", @@ -303,15 +297,14 @@ def test_intent_request_with_slots_and_multi_synonym_resolution(alexa_client): }, }, } - req = yield from _intent_req(alexa_client, data) + req = await _intent_req(alexa_client, data) assert req.status == 200 - data = yield from req.json() + data = await req.json() text = data.get("response", {}).get("outputSpeech", {}).get("text") assert text == "You told us your sign is V zodiac." -@asyncio.coroutine -def test_intent_request_with_slots_but_no_value(alexa_client): +async def test_intent_request_with_slots_but_no_value(alexa_client): """Test a request with slots but no value.""" data = { "version": "1.0", @@ -338,15 +331,14 @@ def test_intent_request_with_slots_but_no_value(alexa_client): }, }, } - req = yield from _intent_req(alexa_client, data) + req = await _intent_req(alexa_client, data) assert req.status == 200 - data = yield from req.json() + data = await req.json() text = data.get("response", {}).get("outputSpeech", {}).get("text") assert text == "You told us your sign is ." -@asyncio.coroutine -def test_intent_request_without_slots(hass, alexa_client): +async def test_intent_request_without_slots(hass, alexa_client): """Test a request without slots.""" data = { "version": "1.0", @@ -370,9 +362,9 @@ def test_intent_request_without_slots(hass, alexa_client): "intent": {"name": "WhereAreWeIntent"}, }, } - req = yield from _intent_req(alexa_client, data) + req = await _intent_req(alexa_client, data) assert req.status == 200 - json = yield from req.json() + json = await req.json() text = json.get("response", {}).get("outputSpeech", {}).get("text") assert text == "Anne Therese is at unknown and Paulus is at unknown" @@ -380,15 +372,14 @@ def test_intent_request_without_slots(hass, alexa_client): hass.states.async_set("device_tracker.paulus", "home") hass.states.async_set("device_tracker.anne_therese", "home") - req = yield from _intent_req(alexa_client, data) + req = await _intent_req(alexa_client, data) assert req.status == 200 - json = yield from req.json() + json = await req.json() text = json.get("response", {}).get("outputSpeech", {}).get("text") assert text == "You are both home, you silly" -@asyncio.coroutine -def test_intent_request_calling_service(alexa_client): +async def test_intent_request_calling_service(alexa_client): """Test a request for calling a service.""" data = { "version": "1.0", @@ -410,7 +401,7 @@ def test_intent_request_calling_service(alexa_client): }, } call_count = len(calls) - req = yield from _intent_req(alexa_client, data) + req = await _intent_req(alexa_client, data) assert req.status == 200 assert call_count + 1 == len(calls) call = calls[-1] @@ -419,15 +410,14 @@ def test_intent_request_calling_service(alexa_client): assert call.data.get("entity_id") == ["switch.test"] assert call.data.get("hello") == "virgo" - data = yield from req.json() + data = await req.json() assert data["response"]["card"]["title"] == "Card title for virgo" assert data["response"]["card"]["content"] == "Card content: virgo" assert data["response"]["outputSpeech"]["type"] == "PlainText" assert data["response"]["outputSpeech"]["text"] == "Service called for virgo" -@asyncio.coroutine -def test_intent_session_ended_request(alexa_client): +async def test_intent_session_ended_request(alexa_client): """Test the request for ending the session.""" data = { "version": "1.0", @@ -452,14 +442,13 @@ def test_intent_session_ended_request(alexa_client): }, } - req = yield from _intent_req(alexa_client, data) + req = await _intent_req(alexa_client, data) assert req.status == 200 - text = yield from req.text() + text = await req.text() assert text == "" -@asyncio.coroutine -def test_intent_from_built_in_intent_library(alexa_client): +async def test_intent_from_built_in_intent_library(alexa_client): """Test intents from the Built-in Intent Library.""" data = { "request": { @@ -490,8 +479,8 @@ def test_intent_from_built_in_intent_library(alexa_client): "application": {"applicationId": APPLICATION_ID}, }, } - req = yield from _intent_req(alexa_client, data) + req = await _intent_req(alexa_client, data) assert req.status == 200 - data = yield from req.json() + data = await req.json() text = data.get("response", {}).get("outputSpeech", {}).get("text") assert text == "Playing the shins." diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 9b901288f26..37301c3555e 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -1,9 +1,7 @@ """Test for smart home alexa support.""" import pytest -from homeassistant.core import Context, callback -from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT -from homeassistant.components.alexa import smart_home, messages +from homeassistant.components.alexa import messages, smart_home from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, @@ -11,6 +9,7 @@ from homeassistant.components.media_player.const import ( SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, + SUPPORT_SELECT_SOUND_MODE, SUPPORT_SELECT_SOURCE, SUPPORT_STOP, SUPPORT_TURN_OFF, @@ -18,22 +17,24 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, ) +from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.core import Context, callback from homeassistant.helpers import entityfilter -from tests.common import async_mock_service - from . import ( - get_new_request, - MockConfig, DEFAULT_CONFIG, - assert_request_calls_service, - assert_request_fails, + MockConfig, ReportedProperties, assert_power_controller_works, + assert_request_calls_service, + assert_request_fails, assert_scene_controller_works, + get_new_request, reported_properties, ) +from tests.common import async_mock_service + @pytest.fixture def events(hass): @@ -126,10 +127,12 @@ async def discovery_test(device, hass, expected_endpoints=1): return None -def get_capability(capabilities, capability_name): +def get_capability(capabilities, capability_name, instance=None): """Search a set of capabilities for a specific one.""" for capability in capabilities: - if capability["interface"] == capability_name: + if instance and capability["instance"] == instance: + return capability + elif capability["interface"] == capability_name: return capability return None @@ -157,7 +160,7 @@ async def test_switch(hass, events): assert appliance["displayCategories"][0] == "SWITCH" assert appliance["friendlyName"] == "Test switch" assert_endpoint_capabilities( - appliance, "Alexa.PowerController", "Alexa.EndpointHealth" + appliance, "Alexa.PowerController", "Alexa.EndpointHealth", "Alexa" ) await assert_power_controller_works( @@ -168,6 +171,23 @@ async def test_switch(hass, events): properties.assert_equal("Alexa.PowerController", "powerState", "ON") +async def test_outlet(hass, events): + """Test switch with device class outlet discovery.""" + device = ( + "switch.test", + "on", + {"friendly_name": "Test switch", "device_class": "outlet"}, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "switch#test" + assert appliance["displayCategories"][0] == "SMARTPLUG" + assert appliance["friendlyName"] == "Test switch" + assert_endpoint_capabilities( + appliance, "Alexa", "Alexa.PowerController", "Alexa.EndpointHealth" + ) + + async def test_light(hass): """Test light discovery.""" device = ("light.test_1", "on", {"friendly_name": "Test light 1"}) @@ -177,7 +197,7 @@ async def test_light(hass): assert appliance["displayCategories"][0] == "LIGHT" assert appliance["friendlyName"] == "Test light 1" assert_endpoint_capabilities( - appliance, "Alexa.PowerController", "Alexa.EndpointHealth" + appliance, "Alexa.PowerController", "Alexa.EndpointHealth", "Alexa" ) await assert_power_controller_works( @@ -203,6 +223,7 @@ async def test_dimmable_light(hass): "Alexa.BrightnessController", "Alexa.PowerController", "Alexa.EndpointHealth", + "Alexa", ) properties = await reported_properties(hass, "light#test_2") @@ -245,6 +266,7 @@ async def test_color_light(hass): "Alexa.ColorController", "Alexa.ColorTemperatureController", "Alexa.EndpointHealth", + "Alexa", ) # IncreaseColorTemperature and DecreaseColorTemperature have their own @@ -260,8 +282,11 @@ async def test_script(hass): assert appliance["displayCategories"][0] == "ACTIVITY_TRIGGER" assert appliance["friendlyName"] == "Test script" - (capability,) = assert_endpoint_capabilities(appliance, "Alexa.SceneController") - assert not capability["supportsDeactivation"] + capabilities = assert_endpoint_capabilities( + appliance, "Alexa.SceneController", "Alexa" + ) + scene_capability = get_capability(capabilities, "Alexa.SceneController") + assert not scene_capability["supportsDeactivation"] await assert_scene_controller_works("script#test", "script.turn_on", None, hass) @@ -276,8 +301,11 @@ async def test_cancelable_script(hass): appliance = await discovery_test(device, hass) assert appliance["endpointId"] == "script#test_2" - (capability,) = assert_endpoint_capabilities(appliance, "Alexa.SceneController") - assert capability["supportsDeactivation"] + capabilities = assert_endpoint_capabilities( + appliance, "Alexa.SceneController", "Alexa" + ) + scene_capability = get_capability(capabilities, "Alexa.SceneController") + assert scene_capability["supportsDeactivation"] await assert_scene_controller_works( "script#test_2", "script.turn_on", "script.turn_off", hass @@ -293,7 +321,7 @@ async def test_input_boolean(hass): assert appliance["displayCategories"][0] == "OTHER" assert appliance["friendlyName"] == "Test input boolean" assert_endpoint_capabilities( - appliance, "Alexa.PowerController", "Alexa.EndpointHealth" + appliance, "Alexa.PowerController", "Alexa.EndpointHealth", "Alexa" ) await assert_power_controller_works( @@ -310,8 +338,11 @@ async def test_scene(hass): assert appliance["displayCategories"][0] == "SCENE_TRIGGER" assert appliance["friendlyName"] == "Test scene" - (capability,) = assert_endpoint_capabilities(appliance, "Alexa.SceneController") - assert not capability["supportsDeactivation"] + capabilities = assert_endpoint_capabilities( + appliance, "Alexa.SceneController", "Alexa" + ) + scene_capability = get_capability(capabilities, "Alexa.SceneController") + assert not scene_capability["supportsDeactivation"] await assert_scene_controller_works("scene#test", "scene.turn_on", None, hass) @@ -325,7 +356,7 @@ async def test_fan(hass): assert appliance["displayCategories"][0] == "FAN" assert appliance["friendlyName"] == "Test fan 1" capabilities = assert_endpoint_capabilities( - appliance, "Alexa.PowerController", "Alexa.EndpointHealth" + appliance, "Alexa.PowerController", "Alexa.EndpointHealth", "Alexa" ) power_capability = get_capability(capabilities, "Alexa.PowerController") @@ -361,6 +392,7 @@ async def test_variable_fan(hass): "Alexa.PowerLevelController", "Alexa.RangeController", "Alexa.EndpointHealth", + "Alexa", ) range_capability = get_capability(capabilities, "Alexa.RangeController") @@ -391,9 +423,29 @@ async def test_variable_fan(hass): ) assert call.data["speed"] == "medium" + call, _ = await assert_request_calls_service( + "Alexa.PercentageController", + "SetPercentage", + "fan#test_2", + "fan.set_speed", + hass, + payload={"percentage": "33"}, + ) + assert call.data["speed"] == "low" + + call, _ = await assert_request_calls_service( + "Alexa.PercentageController", + "SetPercentage", + "fan#test_2", + "fan.set_speed", + hass, + payload={"percentage": "100"}, + ) + assert call.data["speed"] == "high" + await assert_percentage_changes( hass, - [("high", "-5"), ("off", "5"), ("low", "-80")], + [("high", "-5"), ("off", "5"), ("low", "-80"), ("medium", "-34")], "Alexa.PercentageController", "AdjustPercentage", "fan#test_2", @@ -402,6 +454,16 @@ async def test_variable_fan(hass): "speed", ) + call, _ = await assert_request_calls_service( + "Alexa.PowerLevelController", + "SetPowerLevel", + "fan#test_2", + "fan.set_speed", + hass, + payload={"powerLevel": "20"}, + ) + assert call.data["speed"] == "low" + call, _ = await assert_request_calls_service( "Alexa.PowerLevelController", "SetPowerLevel", @@ -412,6 +474,16 @@ async def test_variable_fan(hass): ) assert call.data["speed"] == "medium" + call, _ = await assert_request_calls_service( + "Alexa.PowerLevelController", + "SetPowerLevel", + "fan#test_2", + "fan.set_speed", + hass, + payload={"powerLevel": "99"}, + ) + assert call.data["speed"] == "high" + await assert_percentage_changes( hass, [("high", "-5"), ("medium", "-50"), ("low", "-80")], @@ -444,6 +516,7 @@ async def test_oscillating_fan(hass): "Alexa.RangeController", "Alexa.ToggleController", "Alexa.EndpointHealth", + "Alexa", ) toggle_capability = get_capability(capabilities, "Alexa.ToggleController") @@ -508,6 +581,7 @@ async def test_direction_fan(hass): "Alexa.RangeController", "Alexa.ModeController", "Alexa.EndpointHealth", + "Alexa", ) mode_capability = get_capability(capabilities, "Alexa.ModeController") @@ -548,7 +622,7 @@ async def test_direction_fan(hass): }, } in supported_modes - call, _ = await assert_request_calls_service( + call, msg = await assert_request_calls_service( "Alexa.ModeController", "SetMode", "fan#test_4", @@ -558,6 +632,25 @@ async def test_direction_fan(hass): instance="fan.direction", ) assert call.data["direction"] == "reverse" + properties = msg["context"]["properties"][0] + assert properties["name"] == "mode" + assert properties["namespace"] == "Alexa.ModeController" + assert properties["value"] == "direction.reverse" + + call, msg = await assert_request_calls_service( + "Alexa.ModeController", + "SetMode", + "fan#test_4", + "fan.set_direction", + hass, + payload={"mode": "direction.forward"}, + instance="fan.direction", + ) + assert call.data["direction"] == "forward" + properties = msg["context"]["properties"][0] + assert properties["name"] == "mode" + assert properties["namespace"] == "Alexa.ModeController" + assert properties["value"] == "direction.forward" # Test for AdjustMode instance=None Error coverage with pytest.raises(AssertionError): @@ -601,6 +694,7 @@ async def test_fan_range(hass): "Alexa.PowerLevelController", "Alexa.RangeController", "Alexa.EndpointHealth", + "Alexa", ) range_capability = get_capability(capabilities, "Alexa.RangeController") @@ -678,7 +772,7 @@ async def test_lock(hass): assert appliance["displayCategories"][0] == "SMARTLOCK" assert appliance["friendlyName"] == "Test lock" assert_endpoint_capabilities( - appliance, "Alexa.LockController", "Alexa.EndpointHealth" + appliance, "Alexa.LockController", "Alexa.EndpointHealth", "Alexa" ) _, msg = await assert_request_calls_service( @@ -729,6 +823,7 @@ async def test_media_player(hass): capabilities = assert_endpoint_capabilities( appliance, + "Alexa", "Alexa.ChannelController", "Alexa.EndpointHealth", "Alexa.InputController", @@ -883,7 +978,7 @@ async def test_media_player(hass): "media_player#test", "media_player.play_media", hass, - payload={"channel": {"number": 24}}, + payload={"channel": {"number": "24"}, "channelMetadata": {"name": ""}}, ) call, _ = await assert_request_calls_service( @@ -892,7 +987,7 @@ async def test_media_player(hass): "media_player#test", "media_player.play_media", hass, - payload={"channel": {"callSign": "ABC"}}, + payload={"channel": {"callSign": "ABC"}, "channelMetadata": {"name": ""}}, ) call, _ = await assert_request_calls_service( @@ -901,7 +996,7 @@ async def test_media_player(hass): "media_player#test", "media_player.play_media", hass, - payload={"channel": {"affiliateCallSign": "ABC"}}, + payload={"channel": {"number": ""}, "channelMetadata": {"name": "ABC"}}, ) call, _ = await assert_request_calls_service( @@ -910,7 +1005,19 @@ async def test_media_player(hass): "media_player#test", "media_player.play_media", hass, - payload={"channel": {"uri": "ABC"}}, + payload={ + "channel": {"affiliateCallSign": "ABC"}, + "channelMetadata": {"name": ""}, + }, + ) + + call, _ = await assert_request_calls_service( + "Alexa.ChannelController", + "ChangeChannel", + "media_player#test", + "media_player.play_media", + hass, + payload={"channel": {"uri": "ABC"}, "channelMetadata": {"name": ""}}, ) call, _ = await assert_request_calls_service( @@ -951,6 +1058,7 @@ async def test_media_player_power(hass): assert_endpoint_capabilities( appliance, + "Alexa", "Alexa.ChannelController", "Alexa.EndpointHealth", "Alexa.InputController", @@ -979,6 +1087,110 @@ async def test_media_player_power(hass): ) +async def test_media_player_inputs(hass): + """Test media player discovery with source list inputs.""" + device = ( + "media_player.test", + "on", + { + "friendly_name": "Test media player", + "supported_features": SUPPORT_SELECT_SOURCE, + "volume_level": 0.75, + "source_list": [ + "foo", + "foo_2", + "hdmi", + "hdmi_2", + "hdmi-3", + "hdmi4", + "hdmi 5", + "HDMI 6", + "hdmi_arc", + "aux", + "input 1", + "tv", + ], + }, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "media_player#test" + assert appliance["displayCategories"][0] == "TV" + assert appliance["friendlyName"] == "Test media player" + + capabilities = assert_endpoint_capabilities( + appliance, + "Alexa", + "Alexa.InputController", + "Alexa.PowerController", + "Alexa.EndpointHealth", + ) + + input_capability = get_capability(capabilities, "Alexa.InputController") + assert input_capability is not None + assert {"name": "AUX"} not in input_capability["inputs"] + assert {"name": "AUX 1"} in input_capability["inputs"] + assert {"name": "HDMI 1"} in input_capability["inputs"] + assert {"name": "HDMI 2"} in input_capability["inputs"] + assert {"name": "HDMI 3"} in input_capability["inputs"] + assert {"name": "HDMI 4"} in input_capability["inputs"] + assert {"name": "HDMI 5"} in input_capability["inputs"] + assert {"name": "HDMI 6"} in input_capability["inputs"] + assert {"name": "HDMI ARC"} in input_capability["inputs"] + assert {"name": "FOO 1"} not in input_capability["inputs"] + assert {"name": "TV"} in input_capability["inputs"] + + call, _ = await assert_request_calls_service( + "Alexa.InputController", + "SelectInput", + "media_player#test", + "media_player.select_source", + hass, + payload={"input": "HDMI 1"}, + ) + assert call.data["source"] == "hdmi" + + call, _ = await assert_request_calls_service( + "Alexa.InputController", + "SelectInput", + "media_player#test", + "media_player.select_source", + hass, + payload={"input": "HDMI 2"}, + ) + assert call.data["source"] == "hdmi_2" + + call, _ = await assert_request_calls_service( + "Alexa.InputController", + "SelectInput", + "media_player#test", + "media_player.select_source", + hass, + payload={"input": "HDMI 5"}, + ) + assert call.data["source"] == "hdmi 5" + + call, _ = await assert_request_calls_service( + "Alexa.InputController", + "SelectInput", + "media_player#test", + "media_player.select_source", + hass, + payload={"input": "HDMI 6"}, + ) + assert call.data["source"] == "HDMI 6" + + call, _ = await assert_request_calls_service( + "Alexa.InputController", + "SelectInput", + "media_player#test", + "media_player.select_source", + hass, + payload={"input": "TV"}, + ) + assert call.data["source"] == "tv" + + async def test_media_player_speaker(hass): """Test media player discovery with device class speaker.""" device = ( @@ -1018,6 +1230,7 @@ async def test_media_player_seek(hass): assert_endpoint_capabilities( appliance, + "Alexa", "Alexa.EndpointHealth", "Alexa.PowerController", "Alexa.SeekController", @@ -1121,7 +1334,7 @@ async def test_alert(hass): assert appliance["displayCategories"][0] == "OTHER" assert appliance["friendlyName"] == "Test alert" assert_endpoint_capabilities( - appliance, "Alexa.PowerController", "Alexa.EndpointHealth" + appliance, "Alexa.PowerController", "Alexa.EndpointHealth", "Alexa" ) await assert_power_controller_works( @@ -1138,7 +1351,7 @@ async def test_automation(hass): assert appliance["displayCategories"][0] == "OTHER" assert appliance["friendlyName"] == "Test automation" assert_endpoint_capabilities( - appliance, "Alexa.PowerController", "Alexa.EndpointHealth" + appliance, "Alexa.PowerController", "Alexa.EndpointHealth", "Alexa" ) await assert_power_controller_works( @@ -1155,7 +1368,7 @@ async def test_group(hass): assert appliance["displayCategories"][0] == "OTHER" assert appliance["friendlyName"] == "Test group" assert_endpoint_capabilities( - appliance, "Alexa.PowerController", "Alexa.EndpointHealth" + appliance, "Alexa.PowerController", "Alexa.EndpointHealth", "Alexa" ) await assert_power_controller_works( @@ -1163,49 +1376,106 @@ async def test_group(hass): ) -async def test_cover(hass): - """Test cover discovery.""" +async def test_cover_position_range(hass): + """Test cover discovery and position using rangeController.""" device = ( - "cover.test", - "off", - {"friendly_name": "Test cover", "supported_features": 255, "position": 30}, + "cover.test_range", + "open", + { + "friendly_name": "Test cover range", + "device_class": "blind", + "supported_features": 7, + "position": 30, + }, ) appliance = await discovery_test(device, hass) - assert appliance["endpointId"] == "cover#test" - assert appliance["displayCategories"][0] == "DOOR" - assert appliance["friendlyName"] == "Test cover" + assert appliance["endpointId"] == "cover#test_range" + assert appliance["displayCategories"][0] == "INTERIOR_BLIND" + assert appliance["friendlyName"] == "Test cover range" - assert_endpoint_capabilities( - appliance, - "Alexa.PercentageController", - "Alexa.PowerController", - "Alexa.EndpointHealth", + capabilities = assert_endpoint_capabilities( + appliance, "Alexa.RangeController", "Alexa.EndpointHealth", "Alexa" ) - await assert_power_controller_works( - "cover#test", "cover.open_cover", "cover.close_cover", hass - ) + range_capability = get_capability(capabilities, "Alexa.RangeController") + assert range_capability is not None + assert range_capability["instance"] == "cover.position" + + properties = range_capability["properties"] + assert properties["nonControllable"] is False + assert {"name": "rangeValue"} in properties["supported"] + + capability_resources = range_capability["capabilityResources"] + assert capability_resources is not None + assert { + "@type": "text", + "value": {"text": "Position", "locale": "en-US"}, + } in capability_resources["friendlyNames"] + + assert { + "@type": "asset", + "value": {"assetId": "Alexa.Setting.Opening"}, + } in capability_resources["friendlyNames"] + + configuration = range_capability["configuration"] + assert configuration is not None + assert configuration["unitOfMeasure"] == "Alexa.Unit.Percent" + + supported_range = configuration["supportedRange"] + assert supported_range["minimumValue"] == 0 + assert supported_range["maximumValue"] == 100 + assert supported_range["precision"] == 1 call, _ = await assert_request_calls_service( - "Alexa.PercentageController", - "SetPercentage", - "cover#test", + "Alexa.RangeController", + "SetRangeValue", + "cover#test_range", "cover.set_cover_position", hass, - payload={"percentage": "50"}, + payload={"rangeValue": "50"}, + instance="cover.position", ) assert call.data["position"] == 50 - await assert_percentage_changes( + call, msg = await assert_request_calls_service( + "Alexa.RangeController", + "SetRangeValue", + "cover#test_range", + "cover.close_cover", hass, - [(25, "-5"), (35, "5"), (0, "-80")], - "Alexa.PercentageController", - "AdjustPercentage", - "cover#test", - "percentageDelta", + payload={"rangeValue": "0"}, + instance="cover.position", + ) + properties = msg["context"]["properties"][0] + assert properties["name"] == "rangeValue" + assert properties["namespace"] == "Alexa.RangeController" + assert properties["value"] == 0 + + call, msg = await assert_request_calls_service( + "Alexa.RangeController", + "SetRangeValue", + "cover#test_range", + "cover.open_cover", + hass, + payload={"rangeValue": "100"}, + instance="cover.position", + ) + properties = msg["context"]["properties"][0] + assert properties["name"] == "rangeValue" + assert properties["namespace"] == "Alexa.RangeController" + assert properties["value"] == 100 + + await assert_range_changes( + hass, + [(25, "-5"), (35, "5"), (0, "-99"), (100, "99")], + "Alexa.RangeController", + "AdjustRangeValue", + "cover#test_range", + False, "cover.set_cover_position", "position", + instance="cover.position", ) @@ -1269,7 +1539,7 @@ async def test_temp_sensor(hass): assert appliance["friendlyName"] == "Test Temp Sensor" capabilities = assert_endpoint_capabilities( - appliance, "Alexa.TemperatureSensor", "Alexa.EndpointHealth" + appliance, "Alexa.TemperatureSensor", "Alexa.EndpointHealth", "Alexa" ) temp_sensor_capability = get_capability(capabilities, "Alexa.TemperatureSensor") @@ -1298,7 +1568,7 @@ async def test_contact_sensor(hass): assert appliance["friendlyName"] == "Test Contact Sensor" capabilities = assert_endpoint_capabilities( - appliance, "Alexa.ContactSensor", "Alexa.EndpointHealth" + appliance, "Alexa.ContactSensor", "Alexa.EndpointHealth", "Alexa" ) contact_sensor_capability = get_capability(capabilities, "Alexa.ContactSensor") @@ -1313,6 +1583,35 @@ async def test_contact_sensor(hass): properties.assert_equal("Alexa.EndpointHealth", "connectivity", {"value": "OK"}) +async def test_forced_contact_sensor(hass): + """Test contact sensor discovery with specified display_category.""" + device = ( + "binary_sensor.test_contact_forced", + "on", + {"friendly_name": "Test Contact Sensor With DisplayCategory"}, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "binary_sensor#test_contact_forced" + assert appliance["displayCategories"][0] == "CONTACT_SENSOR" + assert appliance["friendlyName"] == "Test Contact Sensor With DisplayCategory" + + capabilities = assert_endpoint_capabilities( + appliance, "Alexa.ContactSensor", "Alexa.EndpointHealth", "Alexa" + ) + + contact_sensor_capability = get_capability(capabilities, "Alexa.ContactSensor") + assert contact_sensor_capability is not None + properties = contact_sensor_capability["properties"] + assert properties["retrievable"] is True + assert {"name": "detectionState"} in properties["supported"] + + properties = await reported_properties(hass, "binary_sensor#test_contact_forced") + properties.assert_equal("Alexa.ContactSensor", "detectionState", "DETECTED") + + properties.assert_equal("Alexa.EndpointHealth", "connectivity", {"value": "OK"}) + + async def test_motion_sensor(hass): """Test motion sensor discovery.""" device = ( @@ -1327,7 +1626,7 @@ async def test_motion_sensor(hass): assert appliance["friendlyName"] == "Test Motion Sensor" capabilities = assert_endpoint_capabilities( - appliance, "Alexa.MotionSensor", "Alexa.EndpointHealth" + appliance, "Alexa.MotionSensor", "Alexa.EndpointHealth", "Alexa" ) motion_sensor_capability = get_capability(capabilities, "Alexa.MotionSensor") @@ -1340,6 +1639,35 @@ async def test_motion_sensor(hass): properties.assert_equal("Alexa.MotionSensor", "detectionState", "DETECTED") +async def test_forced_motion_sensor(hass): + """Test motion sensor discovery with specified display_category.""" + device = ( + "binary_sensor.test_motion_forced", + "on", + {"friendly_name": "Test Motion Sensor With DisplayCategory"}, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "binary_sensor#test_motion_forced" + assert appliance["displayCategories"][0] == "MOTION_SENSOR" + assert appliance["friendlyName"] == "Test Motion Sensor With DisplayCategory" + + capabilities = assert_endpoint_capabilities( + appliance, "Alexa.MotionSensor", "Alexa.EndpointHealth", "Alexa" + ) + + motion_sensor_capability = get_capability(capabilities, "Alexa.MotionSensor") + assert motion_sensor_capability is not None + properties = motion_sensor_capability["properties"] + assert properties["retrievable"] is True + assert {"name": "detectionState"} in properties["supported"] + + properties = await reported_properties(hass, "binary_sensor#test_motion_forced") + properties.assert_equal("Alexa.MotionSensor", "detectionState", "DETECTED") + + properties.assert_equal("Alexa.EndpointHealth", "connectivity", {"value": "OK"}) + + async def test_doorbell_sensor(hass): """Test doorbell sensor discovery.""" device = ( @@ -1354,7 +1682,7 @@ async def test_doorbell_sensor(hass): assert appliance["friendlyName"] == "Test Doorbell Sensor" capabilities = assert_endpoint_capabilities( - appliance, "Alexa.DoorbellEventSource", "Alexa.EndpointHealth" + appliance, "Alexa.DoorbellEventSource", "Alexa.EndpointHealth", "Alexa" ) doorbell_capability = get_capability(capabilities, "Alexa.DoorbellEventSource") @@ -1404,6 +1732,7 @@ async def test_thermostat(hass): "Alexa.ThermostatController", "Alexa.TemperatureSensor", "Alexa.EndpointHealth", + "Alexa", ) properties = await reported_properties(hass, "climate#test_thermostat") @@ -1800,7 +2129,7 @@ async def test_entity_config(hass): assert appliance["friendlyName"] == "Config name" assert appliance["description"] == "Config description via Home Assistant" assert_endpoint_capabilities( - appliance, "Alexa.PowerController", "Alexa.EndpointHealth" + appliance, "Alexa.PowerController", "Alexa.EndpointHealth", "Alexa" ) scene = msg["payload"]["endpoints"][1] @@ -1917,7 +2246,7 @@ async def test_alarm_control_panel_disarmed(hass): assert appliance["displayCategories"][0] == "SECURITY_PANEL" assert appliance["friendlyName"] == "Test Alarm Control Panel 1" capabilities = assert_endpoint_capabilities( - appliance, "Alexa.SecurityPanelController", "Alexa.EndpointHealth" + appliance, "Alexa.SecurityPanelController", "Alexa.EndpointHealth", "Alexa" ) security_panel_capability = get_capability( capabilities, "Alexa.SecurityPanelController" @@ -1984,7 +2313,7 @@ async def test_alarm_control_panel_armed(hass): assert appliance["displayCategories"][0] == "SECURITY_PANEL" assert appliance["friendlyName"] == "Test Alarm Control Panel 2" assert_endpoint_capabilities( - appliance, "Alexa.SecurityPanelController", "Alexa.EndpointHealth" + appliance, "Alexa.SecurityPanelController", "Alexa.EndpointHealth", "Alexa" ) properties = await reported_properties(hass, "alarm_control_panel#test_2") @@ -2059,3 +2388,652 @@ async def test_mode_unsupported_domain(hass): assert msg["header"]["name"] == "ErrorResponse" assert msg["header"]["namespace"] == "Alexa" assert msg["payload"]["type"] == "INVALID_DIRECTIVE" + + +async def test_cover_position_mode(hass): + """Test cover discovery and position using modeController.""" + device = ( + "cover.test_mode", + "open", + { + "friendly_name": "Test cover mode", + "device_class": "blind", + "supported_features": 3, + }, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "cover#test_mode" + assert appliance["displayCategories"][0] == "INTERIOR_BLIND" + assert appliance["friendlyName"] == "Test cover mode" + + capabilities = assert_endpoint_capabilities( + appliance, "Alexa", "Alexa.ModeController", "Alexa.EndpointHealth" + ) + + mode_capability = get_capability(capabilities, "Alexa.ModeController") + assert mode_capability is not None + assert mode_capability["instance"] == "cover.position" + + properties = mode_capability["properties"] + assert properties["nonControllable"] is False + assert {"name": "mode"} in properties["supported"] + + capability_resources = mode_capability["capabilityResources"] + assert capability_resources is not None + assert { + "@type": "text", + "value": {"text": "Position", "locale": "en-US"}, + } in capability_resources["friendlyNames"] + + assert { + "@type": "asset", + "value": {"assetId": "Alexa.Setting.Opening"}, + } in capability_resources["friendlyNames"] + + configuration = mode_capability["configuration"] + assert configuration is not None + assert configuration["ordered"] is False + + supported_modes = configuration["supportedModes"] + assert supported_modes is not None + assert { + "value": "position.open", + "modeResources": { + "friendlyNames": [ + {"@type": "asset", "value": {"assetId": "Alexa.Value.Open"}} + ] + }, + } in supported_modes + assert { + "value": "position.closed", + "modeResources": { + "friendlyNames": [ + {"@type": "asset", "value": {"assetId": "Alexa.Value.Close"}} + ] + }, + } in supported_modes + + semantics = mode_capability["semantics"] + assert semantics is not None + + action_mappings = semantics["actionMappings"] + assert action_mappings is not None + + state_mappings = semantics["stateMappings"] + assert state_mappings is not None + + call, msg = await assert_request_calls_service( + "Alexa.ModeController", + "SetMode", + "cover#test_mode", + "cover.close_cover", + hass, + payload={"mode": "position.closed"}, + instance="cover.position", + ) + properties = msg["context"]["properties"][0] + assert properties["name"] == "mode" + assert properties["namespace"] == "Alexa.ModeController" + assert properties["value"] == "position.closed" + + call, msg = await assert_request_calls_service( + "Alexa.ModeController", + "SetMode", + "cover#test_mode", + "cover.open_cover", + hass, + payload={"mode": "position.open"}, + instance="cover.position", + ) + properties = msg["context"]["properties"][0] + assert properties["name"] == "mode" + assert properties["namespace"] == "Alexa.ModeController" + assert properties["value"] == "position.open" + + call, msg = await assert_request_calls_service( + "Alexa.ModeController", + "SetMode", + "cover#test_mode", + "cover.stop_cover", + hass, + payload={"mode": "position.custom"}, + instance="cover.position", + ) + properties = msg["context"]["properties"][0] + assert properties["name"] == "mode" + assert properties["namespace"] == "Alexa.ModeController" + assert properties["value"] == "position.custom" + + +async def test_image_processing(hass): + """Test image_processing discovery as event detection.""" + device = ( + "image_processing.test_face", + 0, + { + "friendly_name": "Test face", + "device_class": "face", + "faces": [], + "total_faces": 0, + }, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "image_processing#test_face" + assert appliance["displayCategories"][0] == "CAMERA" + assert appliance["friendlyName"] == "Test face" + + assert_endpoint_capabilities( + appliance, "Alexa.EventDetectionSensor", "Alexa.EndpointHealth", "Alexa" + ) + + +async def test_motion_sensor_event_detection(hass): + """Test motion sensor with EventDetectionSensor discovery.""" + device = ( + "binary_sensor.test_motion_camera_event", + "off", + {"friendly_name": "Test motion camera event", "device_class": "motion"}, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "binary_sensor#test_motion_camera_event" + assert appliance["displayCategories"][0] == "CAMERA" + assert appliance["friendlyName"] == "Test motion camera event" + + capabilities = assert_endpoint_capabilities( + appliance, + "Alexa", + "Alexa.MotionSensor", + "Alexa.EventDetectionSensor", + "Alexa.EndpointHealth", + ) + + event_detection_capability = get_capability( + capabilities, "Alexa.EventDetectionSensor" + ) + assert event_detection_capability is not None + properties = event_detection_capability["properties"] + assert properties["proactivelyReported"] is True + assert not properties["retrievable"] + assert {"name": "humanPresenceDetectionState"} in properties["supported"] + + +async def test_presence_sensor(hass): + """Test presence sensor.""" + device = ( + "binary_sensor.test_presence_sensor", + "off", + {"friendly_name": "Test presence sensor", "device_class": "presence"}, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "binary_sensor#test_presence_sensor" + assert appliance["displayCategories"][0] == "CAMERA" + assert appliance["friendlyName"] == "Test presence sensor" + + capabilities = assert_endpoint_capabilities( + appliance, "Alexa", "Alexa.EventDetectionSensor", "Alexa.EndpointHealth" + ) + + event_detection_capability = get_capability( + capabilities, "Alexa.EventDetectionSensor" + ) + assert event_detection_capability is not None + properties = event_detection_capability["properties"] + assert properties["proactivelyReported"] is True + assert not properties["retrievable"] + assert {"name": "humanPresenceDetectionState"} in properties["supported"] + + +async def test_cover_tilt_position_range(hass): + """Test cover discovery and tilt position using rangeController.""" + device = ( + "cover.test_tilt_range", + "open", + { + "friendly_name": "Test cover tilt range", + "device_class": "blind", + "supported_features": 240, + "tilt_position": 30, + }, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "cover#test_tilt_range" + assert appliance["displayCategories"][0] == "INTERIOR_BLIND" + assert appliance["friendlyName"] == "Test cover tilt range" + + capabilities = assert_endpoint_capabilities( + appliance, "Alexa.RangeController", "Alexa.EndpointHealth", "Alexa" + ) + + range_capability = get_capability(capabilities, "Alexa.RangeController") + assert range_capability is not None + assert range_capability["instance"] == "cover.tilt_position" + + semantics = range_capability["semantics"] + assert semantics is not None + + action_mappings = semantics["actionMappings"] + assert action_mappings is not None + + state_mappings = semantics["stateMappings"] + assert state_mappings is not None + + call, _ = await assert_request_calls_service( + "Alexa.RangeController", + "SetRangeValue", + "cover#test_tilt_range", + "cover.set_cover_tilt_position", + hass, + payload={"rangeValue": "50"}, + instance="cover.tilt_position", + ) + assert call.data["position"] == 50 + + call, msg = await assert_request_calls_service( + "Alexa.RangeController", + "SetRangeValue", + "cover#test_tilt_range", + "cover.close_cover_tilt", + hass, + payload={"rangeValue": "0"}, + instance="cover.tilt_position", + ) + properties = msg["context"]["properties"][0] + assert properties["name"] == "rangeValue" + assert properties["namespace"] == "Alexa.RangeController" + assert properties["value"] == 0 + + call, msg = await assert_request_calls_service( + "Alexa.RangeController", + "SetRangeValue", + "cover#test_tilt_range", + "cover.open_cover_tilt", + hass, + payload={"rangeValue": "100"}, + instance="cover.tilt_position", + ) + properties = msg["context"]["properties"][0] + assert properties["name"] == "rangeValue" + assert properties["namespace"] == "Alexa.RangeController" + assert properties["value"] == 100 + + await assert_range_changes( + hass, + [(25, "-5"), (35, "5"), (0, "-99"), (100, "99")], + "Alexa.RangeController", + "AdjustRangeValue", + "cover#test_tilt_range", + False, + "cover.set_cover_tilt_position", + "tilt_position", + instance="cover.tilt_position", + ) + + +async def test_cover_semantics(hass): + """Test cover discovery and semantics.""" + device = ( + "cover.test_semantics", + "open", + { + "friendly_name": "Test cover semantics", + "device_class": "blind", + "supported_features": 255, + "position": 30, + "tilt_position": 30, + }, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "cover#test_semantics" + assert appliance["displayCategories"][0] == "INTERIOR_BLIND" + assert appliance["friendlyName"] == "Test cover semantics" + + capabilities = assert_endpoint_capabilities( + appliance, "Alexa.RangeController", "Alexa.EndpointHealth", "Alexa" + ) + + for range_instance in ("cover.position", "cover.tilt_position"): + range_capability = get_capability( + capabilities, "Alexa.RangeController", range_instance + ) + semantics = range_capability["semantics"] + assert semantics is not None + + action_mappings = semantics["actionMappings"] + assert action_mappings is not None + if range_instance == "cover.position": + assert { + "@type": "ActionsToDirective", + "actions": ["Alexa.Actions.Lower"], + "directive": {"name": "SetRangeValue", "payload": {"rangeValue": 0}}, + } in action_mappings + assert { + "@type": "ActionsToDirective", + "actions": ["Alexa.Actions.Raise"], + "directive": {"name": "SetRangeValue", "payload": {"rangeValue": 100}}, + } in action_mappings + elif range_instance == "cover.position": + assert { + "@type": "ActionsToDirective", + "actions": ["Alexa.Actions.Close"], + "directive": {"name": "SetRangeValue", "payload": {"rangeValue": 0}}, + } in action_mappings + assert { + "@type": "ActionsToDirective", + "actions": ["Alexa.Actions.Open"], + "directive": {"name": "SetRangeValue", "payload": {"rangeValue": 100}}, + } in action_mappings + + state_mappings = semantics["stateMappings"] + assert state_mappings is not None + assert { + "@type": "StatesToValue", + "states": ["Alexa.States.Closed"], + "value": 0, + } in state_mappings + assert { + "@type": "StatesToRange", + "states": ["Alexa.States.Open"], + "range": {"minimumValue": 1, "maximumValue": 100}, + } in state_mappings + + +async def test_input_number(hass): + """Test input_number discovery.""" + device = ( + "input_number.test_slider", + 30, + { + "initial": 30, + "min": -20, + "max": 35, + "step": 1, + "mode": "slider", + "friendly_name": "Test Slider", + }, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "input_number#test_slider" + assert appliance["displayCategories"][0] == "OTHER" + assert appliance["friendlyName"] == "Test Slider" + + capabilities = assert_endpoint_capabilities( + appliance, "Alexa.RangeController", "Alexa.EndpointHealth", "Alexa" + ) + + range_capability = get_capability( + capabilities, "Alexa.RangeController", "input_number.value" + ) + + capability_resources = range_capability["capabilityResources"] + assert capability_resources is not None + assert { + "@type": "text", + "value": {"text": "Value", "locale": "en-US"}, + } in capability_resources["friendlyNames"] + + configuration = range_capability["configuration"] + assert configuration is not None + + supported_range = configuration["supportedRange"] + assert supported_range["minimumValue"] == -20 + assert supported_range["maximumValue"] == 35 + assert supported_range["precision"] == 1 + + presets = configuration["presets"] + assert { + "rangeValue": 35, + "presetResources": { + "friendlyNames": [ + {"@type": "asset", "value": {"assetId": "Alexa.Value.Maximum"}} + ] + }, + } in presets + + assert { + "rangeValue": -20, + "presetResources": { + "friendlyNames": [ + {"@type": "asset", "value": {"assetId": "Alexa.Value.Minimum"}} + ] + }, + } in presets + + call, _ = await assert_request_calls_service( + "Alexa.RangeController", + "SetRangeValue", + "input_number#test_slider", + "input_number.set_value", + hass, + payload={"rangeValue": "10"}, + instance="input_number.value", + ) + assert call.data["value"] == 10 + + await assert_range_changes( + hass, + [(25, "-5"), (35, "5"), (-20, "-100"), (35, "100")], + "Alexa.RangeController", + "AdjustRangeValue", + "input_number#test_slider", + False, + "input_number.set_value", + "value", + instance="input_number.value", + ) + + +async def test_input_number_float(hass): + """Test input_number discovery.""" + device = ( + "input_number.test_slider_float", + 0.5, + { + "initial": 0.5, + "min": 0, + "max": 1, + "step": 0.01, + "mode": "slider", + "friendly_name": "Test Slider Float", + }, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "input_number#test_slider_float" + assert appliance["displayCategories"][0] == "OTHER" + assert appliance["friendlyName"] == "Test Slider Float" + + capabilities = assert_endpoint_capabilities( + appliance, "Alexa.RangeController", "Alexa.EndpointHealth", "Alexa" + ) + + range_capability = get_capability( + capabilities, "Alexa.RangeController", "input_number.value" + ) + + capability_resources = range_capability["capabilityResources"] + assert capability_resources is not None + assert { + "@type": "text", + "value": {"text": "Value", "locale": "en-US"}, + } in capability_resources["friendlyNames"] + + configuration = range_capability["configuration"] + assert configuration is not None + + supported_range = configuration["supportedRange"] + assert supported_range["minimumValue"] == 0 + assert supported_range["maximumValue"] == 1 + assert supported_range["precision"] == 0.01 + + presets = configuration["presets"] + assert { + "rangeValue": 1, + "presetResources": { + "friendlyNames": [ + {"@type": "asset", "value": {"assetId": "Alexa.Value.Maximum"}} + ] + }, + } in presets + + assert { + "rangeValue": 0, + "presetResources": { + "friendlyNames": [ + {"@type": "asset", "value": {"assetId": "Alexa.Value.Minimum"}} + ] + }, + } in presets + + call, _ = await assert_request_calls_service( + "Alexa.RangeController", + "SetRangeValue", + "input_number#test_slider_float", + "input_number.set_value", + hass, + payload={"rangeValue": "0.333"}, + instance="input_number.value", + ) + assert call.data["value"] == 0.333 + + await assert_range_changes( + hass, + [(0.4, "-0.1"), (0.6, "0.1"), (0, "-100"), (1, "100"), (0.51, "0.01")], + "Alexa.RangeController", + "AdjustRangeValue", + "input_number#test_slider_float", + False, + "input_number.set_value", + "value", + instance="input_number.value", + ) + + +async def test_media_player_eq_modes(hass): + """Test media player discovery with sound mode list.""" + device = ( + "media_player.test", + "on", + { + "friendly_name": "Test media player", + "supported_features": SUPPORT_SELECT_SOUND_MODE, + "sound_mode": "tv", + "sound_mode_list": ["movie", "music", "night", "sport", "tv", "rocknroll"], + }, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "media_player#test" + assert appliance["friendlyName"] == "Test media player" + + capabilities = assert_endpoint_capabilities( + appliance, + "Alexa", + "Alexa.EqualizerController", + "Alexa.PowerController", + "Alexa.EndpointHealth", + ) + + eq_capability = get_capability(capabilities, "Alexa.EqualizerController") + assert eq_capability is not None + assert "modes" in eq_capability["configurations"] + + eq_modes = eq_capability["configurations"]["modes"] + assert {"name": "rocknroll"} not in eq_modes["supported"] + assert {"name": "ROCKNROLL"} not in eq_modes["supported"] + + for mode in ("MOVIE", "MUSIC", "NIGHT", "SPORT", "TV"): + assert {"name": mode} in eq_modes["supported"] + + call, _ = await assert_request_calls_service( + "Alexa.EqualizerController", + "SetMode", + "media_player#test", + "media_player.select_sound_mode", + hass, + payload={"mode": mode}, + ) + assert call.data["sound_mode"] == mode.lower() + + +async def test_media_player_sound_mode_list_none(hass): + """Test EqualizerController bands directive not supported.""" + device = ( + "media_player.test", + "on", + { + "friendly_name": "Test media player", + "supported_features": SUPPORT_SELECT_SOUND_MODE, + "sound_mode": "unknown", + "sound_mode_list": None, + }, + ) + appliance = await discovery_test(device, hass) + assert appliance["endpointId"] == "media_player#test" + assert appliance["friendlyName"] == "Test media player" + + +async def test_media_player_eq_bands_not_supported(hass): + """Test EqualizerController bands directive not supported.""" + device = ( + "media_player.test_bands", + "on", + { + "friendly_name": "Test media player", + "supported_features": SUPPORT_SELECT_SOUND_MODE, + "sound_mode": "tv", + "sound_mode_list": ["movie", "music", "night", "sport", "tv", "rocknroll"], + }, + ) + await discovery_test(device, hass) + + context = Context() + + # Test for SetBands Error + request = get_new_request( + "Alexa.EqualizerController", "SetBands", "media_player#test_bands" + ) + request["directive"]["payload"] = {"bands": [{"name": "BASS", "value": -2}]} + msg = await smart_home.async_handle_message(hass, DEFAULT_CONFIG, request, context) + + assert "event" in msg + msg = msg["event"] + assert msg["header"]["name"] == "ErrorResponse" + assert msg["header"]["namespace"] == "Alexa" + assert msg["payload"]["type"] == "INVALID_DIRECTIVE" + + # Test for AdjustBands Error + request = get_new_request( + "Alexa.EqualizerController", "AdjustBands", "media_player#test_bands" + ) + request["directive"]["payload"] = { + "bands": [{"name": "BASS", "levelDelta": 3, "levelDirection": "UP"}] + } + msg = await smart_home.async_handle_message(hass, DEFAULT_CONFIG, request, context) + + assert "event" in msg + msg = msg["event"] + assert msg["header"]["name"] == "ErrorResponse" + assert msg["header"]["namespace"] == "Alexa" + assert msg["payload"]["type"] == "INVALID_DIRECTIVE" + + # Test for ResetBands Error + request = get_new_request( + "Alexa.EqualizerController", "ResetBands", "media_player#test_bands" + ) + request["directive"]["payload"] = { + "bands": [{"name": "BASS", "levelDelta": 3, "levelDirection": "UP"}] + } + msg = await smart_home.async_handle_message(hass, DEFAULT_CONFIG, request, context) + + assert "event" in msg + msg = msg["event"] + assert msg["header"]["name"] == "ErrorResponse" + assert msg["header"]["namespace"] == "Alexa" + assert msg["payload"]["type"] == "INVALID_DIRECTIVE" diff --git a/tests/components/alexa/test_smart_home_http.py b/tests/components/alexa/test_smart_home_http.py index 845c375be67..f242a421eac 100644 --- a/tests/components/alexa/test_smart_home_http.py +++ b/tests/components/alexa/test_smart_home_http.py @@ -1,8 +1,8 @@ """Test Smart Home HTTP endpoints.""" import json -from homeassistant.setup import async_setup_component from homeassistant.components.alexa import DOMAIN, smart_home_http +from homeassistant.setup import async_setup_component from . import get_new_request diff --git a/tests/components/alexa/test_state_report.py b/tests/components/alexa/test_state_report.py index 2c58d1ed45e..4cd2a18a833 100644 --- a/tests/components/alexa/test_state_report.py +++ b/tests/components/alexa/test_state_report.py @@ -1,6 +1,7 @@ """Test report state.""" from homeassistant.components.alexa import state_report -from . import TEST_URL, DEFAULT_CONFIG + +from . import DEFAULT_CONFIG, TEST_URL async def test_report_state(hass, aioclient_mock): diff --git a/tests/components/almond/test_config_flow.py b/tests/components/almond/test_config_flow.py index afbe25dff5f..0b402ed407d 100644 --- a/tests/components/almond/test_config_flow.py +++ b/tests/components/almond/test_config_flow.py @@ -1,12 +1,10 @@ """Test the Almond config flow.""" import asyncio - from unittest.mock import patch - -from homeassistant import config_entries, setup, data_entry_flow -from homeassistant.components.almond.const import DOMAIN +from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.almond import config_flow +from homeassistant.components.almond.const import DOMAIN from homeassistant.helpers import config_entry_oauth2_flow from tests.common import MockConfigEntry, mock_coro diff --git a/tests/components/almond/test_init.py b/tests/components/almond/test_init.py index dd44ea1c8f0..f13ad7dd859 100644 --- a/tests/components/almond/test_init.py +++ b/tests/components/almond/test_init.py @@ -1,16 +1,16 @@ """Tests for Almond set up.""" -from unittest.mock import patch from time import time +from unittest.mock import patch import pytest from homeassistant import config_entries, core +from homeassistant.components.almond import const from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.setup import async_setup_component -from homeassistant.components.almond import const from homeassistant.util.dt import utcnow -from tests.common import MockConfigEntry, mock_coro, async_fire_time_changed +from tests.common import MockConfigEntry, async_fire_time_changed, mock_coro @pytest.fixture(autouse=True) diff --git a/tests/components/ambiclimate/test_config_flow.py b/tests/components/ambiclimate/test_config_flow.py index c0940fcc354..acf3717b898 100644 --- a/tests/components/ambiclimate/test_config_flow.py +++ b/tests/components/ambiclimate/test_config_flow.py @@ -1,11 +1,13 @@ """Tests for the Ambiclimate config flow.""" -import ambiclimate from unittest.mock import Mock, patch +import ambiclimate + +from homeassistant import data_entry_flow from homeassistant.components.ambiclimate import config_flow from homeassistant.setup import async_setup_component from homeassistant.util import aiohttp -from homeassistant import data_entry_flow + from tests.common import mock_coro diff --git a/tests/components/ambient_station/test_config_flow.py b/tests/components/ambient_station/test_config_flow.py index 701a6dacb98..25e46090009 100644 --- a/tests/components/ambient_station/test_config_flow.py +++ b/tests/components/ambient_station/test_config_flow.py @@ -1,5 +1,6 @@ """Define tests for the Ambient PWS config flow.""" import json +from unittest.mock import patch import aioambient import pytest @@ -8,7 +9,7 @@ from homeassistant import data_entry_flow from homeassistant.components.ambient_station import CONF_APP_KEY, DOMAIN, config_flow from homeassistant.const import CONF_API_KEY -from tests.common import load_fixture, MockConfigEntry, MockDependency, mock_coro +from tests.common import MockConfigEntry, load_fixture, mock_coro @pytest.fixture @@ -20,9 +21,9 @@ def get_devices_response(): @pytest.fixture def mock_aioambient(get_devices_response): """Mock the aioambient library.""" - with MockDependency("aioambient") as mock_aioambient_: - mock_aioambient_.Client().api.get_devices.return_value = get_devices_response - yield mock_aioambient_ + with patch("homeassistant.components.ambient_station.config_flow.Client") as Client: + Client().api.get_devices.return_value = get_devices_response + yield Client async def test_duplicate_error(hass): diff --git a/tests/components/androidtv/patchers.py b/tests/components/androidtv/patchers.py index 0549ad995e1..c49b6ad11e9 100644 --- a/tests/components/androidtv/patchers.py +++ b/tests/components/androidtv/patchers.py @@ -3,11 +3,11 @@ from unittest.mock import mock_open, patch -class AdbDeviceFake: - """A fake of the `adb_shell.adb_device.AdbDevice` class.""" +class AdbDeviceTcpFake: + """A fake of the `adb_shell.adb_device.AdbDeviceTcp` class.""" def __init__(self, *args, **kwargs): - """Initialize a fake `adb_shell.adb_device.AdbDevice` instance.""" + """Initialize a fake `adb_shell.adb_device.AdbDeviceTcp` instance.""" self.available = False def close(self): @@ -74,39 +74,39 @@ class DeviceFake: def patch_connect(success): - """Mock the `adb_shell.adb_device.AdbDevice` and `ppadb.client.Client` classes.""" + """Mock the `adb_shell.adb_device.AdbDeviceTcp` and `ppadb.client.Client` classes.""" def connect_success_python(self, *args, **kwargs): - """Mock the `AdbDeviceFake.connect` method when it succeeds.""" + """Mock the `AdbDeviceTcpFake.connect` method when it succeeds.""" self.available = True def connect_fail_python(self, *args, **kwargs): - """Mock the `AdbDeviceFake.connect` method when it fails.""" + """Mock the `AdbDeviceTcpFake.connect` method when it fails.""" raise OSError if success: return { "python": patch( - f"{__name__}.AdbDeviceFake.connect", connect_success_python + f"{__name__}.AdbDeviceTcpFake.connect", connect_success_python ), "server": patch("androidtv.adb_manager.Client", ClientFakeSuccess), } return { - "python": patch(f"{__name__}.AdbDeviceFake.connect", connect_fail_python), + "python": patch(f"{__name__}.AdbDeviceTcpFake.connect", connect_fail_python), "server": patch("androidtv.adb_manager.Client", ClientFakeFail), } def patch_shell(response=None, error=False): - """Mock the `AdbDeviceFake.shell` and `DeviceFake.shell` methods.""" + """Mock the `AdbDeviceTcpFake.shell` and `DeviceFake.shell` methods.""" def shell_success(self, cmd): - """Mock the `AdbDeviceFake.shell` and `DeviceFake.shell` methods when they are successful.""" + """Mock the `AdbDeviceTcpFake.shell` and `DeviceFake.shell` methods when they are successful.""" self.shell_cmd = cmd return response def shell_fail_python(self, cmd): - """Mock the `AdbDeviceFake.shell` method when it fails.""" + """Mock the `AdbDeviceTcpFake.shell` method when it fails.""" self.shell_cmd = cmd raise AttributeError @@ -117,16 +117,16 @@ def patch_shell(response=None, error=False): if not error: return { - "python": patch(f"{__name__}.AdbDeviceFake.shell", shell_success), + "python": patch(f"{__name__}.AdbDeviceTcpFake.shell", shell_success), "server": patch(f"{__name__}.DeviceFake.shell", shell_success), } return { - "python": patch(f"{__name__}.AdbDeviceFake.shell", shell_fail_python), + "python": patch(f"{__name__}.AdbDeviceTcpFake.shell", shell_fail_python), "server": patch(f"{__name__}.DeviceFake.shell", shell_fail_server), } -PATCH_ADB_DEVICE = patch("androidtv.adb_manager.AdbDevice", AdbDeviceFake) +PATCH_ADB_DEVICE_TCP = patch("androidtv.adb_manager.AdbDeviceTcp", AdbDeviceTcpFake) PATCH_ANDROIDTV_OPEN = patch("androidtv.adb_manager.open", mock_open()) PATCH_KEYGEN = patch("homeassistant.components.androidtv.media_player.keygen") PATCH_SIGNER = patch("androidtv.adb_manager.PythonRSASigner") @@ -149,5 +149,22 @@ def patch_firetv_update(state, current_app, running_apps): ) -PATCH_LAUNCH_APP = patch("androidtv.firetv.FireTV.launch_app") -PATCH_STOP_APP = patch("androidtv.firetv.FireTV.stop_app") +def patch_androidtv_update( + state, current_app, running_apps, device, is_volume_muted, volume_level +): + """Patch the `AndroidTV.update()` method.""" + return patch( + "androidtv.androidtv.AndroidTV.update", + return_value=( + state, + current_app, + running_apps, + device, + is_volume_muted, + volume_level, + ), + ) + + +PATCH_LAUNCH_APP = patch("androidtv.basetv.BaseTV.launch_app") +PATCH_STOP_APP = patch("androidtv.basetv.BaseTV.stop_app") diff --git a/tests/components/androidtv/test_media_player.py b/tests/components/androidtv/test_media_player.py index 04b0bebf447..15c4897c136 100644 --- a/tests/components/androidtv/test_media_player.py +++ b/tests/components/androidtv/test_media_player.py @@ -1,7 +1,6 @@ """The tests for the androidtv platform.""" import logging -from homeassistant.setup import async_setup_component from homeassistant.components.androidtv.media_player import ( ANDROIDTV_DOMAIN, CONF_ADB_SERVER_IP, @@ -24,16 +23,17 @@ from homeassistant.const import ( STATE_PLAYING, STATE_UNAVAILABLE, ) +from homeassistant.setup import async_setup_component from . import patchers - # Android TV device with Python ADB implementation CONFIG_ANDROIDTV_PYTHON_ADB = { DOMAIN: { CONF_PLATFORM: ANDROIDTV_DOMAIN, CONF_HOST: "127.0.0.1", CONF_NAME: "Android TV", + CONF_DEVICE_CLASS: "androidtv", } } @@ -43,6 +43,7 @@ CONFIG_ANDROIDTV_ADB_SERVER = { CONF_PLATFORM: ANDROIDTV_DOMAIN, CONF_HOST: "127.0.0.1", CONF_NAME: "Android TV", + CONF_DEVICE_CLASS: "androidtv", CONF_ADB_SERVER_IP: "127.0.0.1", } } @@ -94,7 +95,7 @@ async def _test_reconnect(hass, caplog, config): """ patch_key, entity_id = _setup(hass, config) - with patchers.PATCH_ADB_DEVICE, patchers.patch_connect(True)[ + with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell("")[ patch_key @@ -165,7 +166,7 @@ async def _test_adb_shell_returns_none(hass, config): """ patch_key, entity_id = _setup(hass, config) - with patchers.PATCH_ADB_DEVICE, patchers.patch_connect(True)[ + with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell("")[ patch_key @@ -273,7 +274,7 @@ async def test_setup_with_adbkey(hass): config[DOMAIN][CONF_ADBKEY] = hass.config.path("user_provided_adbkey") patch_key, entity_id = _setup(hass, config) - with patchers.PATCH_ADB_DEVICE, patchers.patch_connect(True)[ + with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell("")[ patch_key @@ -285,13 +286,13 @@ async def test_setup_with_adbkey(hass): assert state.state == STATE_OFF -async def test_firetv_sources(hass): - """Test that sources (i.e., apps) are handled correctly for Fire TV devices.""" - config = CONFIG_FIRETV_ADB_SERVER.copy() +async def _test_sources(hass, config0): + """Test that sources (i.e., apps) are handled correctly for Android TV and Fire TV devices.""" + config = config0.copy() config[DOMAIN][CONF_APPS] = {"com.app.test1": "TEST 1"} patch_key, entity_id = _setup(hass, config) - with patchers.PATCH_ADB_DEVICE, patchers.patch_connect(True)[ + with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell("")[patch_key]: assert await async_setup_component(hass, DOMAIN, config) @@ -300,9 +301,21 @@ async def test_firetv_sources(hass): assert state is not None assert state.state == STATE_OFF - with patchers.patch_firetv_update( - "playing", "com.app.test1", ["com.app.test1", "com.app.test2"] - ): + if config[DOMAIN].get(CONF_DEVICE_CLASS) != "firetv": + patch_update = patchers.patch_androidtv_update( + "playing", + "com.app.test1", + ["com.app.test1", "com.app.test2"], + "hdmi", + False, + 1, + ) + else: + patch_update = patchers.patch_firetv_update( + "playing", "com.app.test1", ["com.app.test1", "com.app.test2"] + ) + + with patch_update: await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) assert state is not None @@ -310,9 +323,21 @@ async def test_firetv_sources(hass): assert state.attributes["source"] == "TEST 1" assert state.attributes["source_list"] == ["TEST 1", "com.app.test2"] - with patchers.patch_firetv_update( - "playing", "com.app.test2", ["com.app.test2", "com.app.test1"] - ): + if config[DOMAIN].get(CONF_DEVICE_CLASS) != "firetv": + patch_update = patchers.patch_androidtv_update( + "playing", + "com.app.test2", + ["com.app.test2", "com.app.test1"], + "hdmi", + True, + 0, + ) + else: + patch_update = patchers.patch_firetv_update( + "playing", "com.app.test2", ["com.app.test2", "com.app.test1"] + ) + + with patch_update: await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) assert state is not None @@ -320,14 +345,26 @@ async def test_firetv_sources(hass): assert state.attributes["source"] == "com.app.test2" assert state.attributes["source_list"] == ["com.app.test2", "TEST 1"] + return True -async def _test_firetv_select_source(hass, source, expected_arg, method_patch): - """Test that the `FireTV.launch_app` and `FireTV.stop_app` methods are called with the right parameter.""" - config = CONFIG_FIRETV_ADB_SERVER.copy() + +async def test_androidtv_sources(hass): + """Test that sources (i.e., apps) are handled correctly for Android TV devices.""" + assert await _test_sources(hass, CONFIG_ANDROIDTV_ADB_SERVER) + + +async def test_firetv_sources(hass): + """Test that sources (i.e., apps) are handled correctly for Fire TV devices.""" + assert await _test_sources(hass, CONFIG_FIRETV_ADB_SERVER) + + +async def _test_select_source(hass, config0, source, expected_arg, method_patch): + """Test that the methods for launching and stopping apps are called correctly when selecting a source.""" + config = config0.copy() config[DOMAIN][CONF_APPS] = {"com.app.test1": "TEST 1"} patch_key, entity_id = _setup(hass, config) - with patchers.PATCH_ADB_DEVICE, patchers.patch_connect(True)[ + with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell("")[patch_key]: assert await async_setup_component(hass, DOMAIN, config) @@ -348,43 +385,198 @@ async def _test_firetv_select_source(hass, source, expected_arg, method_patch): return True +async def test_androidtv_select_source_launch_app_id(hass): + """Test that an app can be launched using its app ID.""" + assert await _test_select_source( + hass, + CONFIG_ANDROIDTV_ADB_SERVER, + "com.app.test1", + "com.app.test1", + patchers.PATCH_LAUNCH_APP, + ) + + +async def test_androidtv_select_source_launch_app_name(hass): + """Test that an app can be launched using its friendly name.""" + assert await _test_select_source( + hass, + CONFIG_ANDROIDTV_ADB_SERVER, + "TEST 1", + "com.app.test1", + patchers.PATCH_LAUNCH_APP, + ) + + +async def test_androidtv_select_source_launch_app_id_no_name(hass): + """Test that an app can be launched using its app ID when it has no friendly name.""" + assert await _test_select_source( + hass, + CONFIG_ANDROIDTV_ADB_SERVER, + "com.app.test2", + "com.app.test2", + patchers.PATCH_LAUNCH_APP, + ) + + +async def test_androidtv_select_source_stop_app_id(hass): + """Test that an app can be stopped using its app ID.""" + assert await _test_select_source( + hass, + CONFIG_ANDROIDTV_ADB_SERVER, + "!com.app.test1", + "com.app.test1", + patchers.PATCH_STOP_APP, + ) + + +async def test_androidtv_select_source_stop_app_name(hass): + """Test that an app can be stopped using its friendly name.""" + assert await _test_select_source( + hass, + CONFIG_ANDROIDTV_ADB_SERVER, + "!TEST 1", + "com.app.test1", + patchers.PATCH_STOP_APP, + ) + + +async def test_androidtv_select_source_stop_app_id_no_name(hass): + """Test that an app can be stopped using its app ID when it has no friendly name.""" + assert await _test_select_source( + hass, + CONFIG_ANDROIDTV_ADB_SERVER, + "!com.app.test2", + "com.app.test2", + patchers.PATCH_STOP_APP, + ) + + async def test_firetv_select_source_launch_app_id(hass): """Test that an app can be launched using its app ID.""" - assert await _test_firetv_select_source( - hass, "com.app.test1", "com.app.test1", patchers.PATCH_LAUNCH_APP + assert await _test_select_source( + hass, + CONFIG_FIRETV_ADB_SERVER, + "com.app.test1", + "com.app.test1", + patchers.PATCH_LAUNCH_APP, ) async def test_firetv_select_source_launch_app_name(hass): """Test that an app can be launched using its friendly name.""" - assert await _test_firetv_select_source( - hass, "TEST 1", "com.app.test1", patchers.PATCH_LAUNCH_APP + assert await _test_select_source( + hass, + CONFIG_FIRETV_ADB_SERVER, + "TEST 1", + "com.app.test1", + patchers.PATCH_LAUNCH_APP, ) async def test_firetv_select_source_launch_app_id_no_name(hass): """Test that an app can be launched using its app ID when it has no friendly name.""" - assert await _test_firetv_select_source( - hass, "com.app.test2", "com.app.test2", patchers.PATCH_LAUNCH_APP + assert await _test_select_source( + hass, + CONFIG_FIRETV_ADB_SERVER, + "com.app.test2", + "com.app.test2", + patchers.PATCH_LAUNCH_APP, ) async def test_firetv_select_source_stop_app_id(hass): """Test that an app can be stopped using its app ID.""" - assert await _test_firetv_select_source( - hass, "!com.app.test1", "com.app.test1", patchers.PATCH_STOP_APP + assert await _test_select_source( + hass, + CONFIG_FIRETV_ADB_SERVER, + "!com.app.test1", + "com.app.test1", + patchers.PATCH_STOP_APP, ) async def test_firetv_select_source_stop_app_name(hass): """Test that an app can be stopped using its friendly name.""" - assert await _test_firetv_select_source( - hass, "!TEST 1", "com.app.test1", patchers.PATCH_STOP_APP + assert await _test_select_source( + hass, + CONFIG_FIRETV_ADB_SERVER, + "!TEST 1", + "com.app.test1", + patchers.PATCH_STOP_APP, ) async def test_firetv_select_source_stop_app_id_no_name(hass): """Test that an app can be stopped using its app ID when it has no friendly name.""" - assert await _test_firetv_select_source( - hass, "!com.app.test2", "com.app.test2", patchers.PATCH_STOP_APP + assert await _test_select_source( + hass, + CONFIG_FIRETV_ADB_SERVER, + "!com.app.test2", + "com.app.test2", + patchers.PATCH_STOP_APP, ) + + +async def _test_setup_fail(hass, config): + """Test that the entity is not created when the ADB connection is not established.""" + patch_key, entity_id = _setup(hass, config) + + with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(False)[ + patch_key + ], patchers.patch_shell("")[ + patch_key + ], patchers.PATCH_KEYGEN, patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER: + assert await async_setup_component(hass, DOMAIN, config) + await hass.helpers.entity_component.async_update_entity(entity_id) + state = hass.states.get(entity_id) + assert state is None + + return True + + +async def test_setup_fail_androidtv(hass): + """Test that the Android TV entity is not created when the ADB connection is not established.""" + assert await _test_setup_fail(hass, CONFIG_ANDROIDTV_PYTHON_ADB) + + +async def test_setup_fail_firetv(hass): + """Test that the Fire TV entity is not created when the ADB connection is not established.""" + assert await _test_setup_fail(hass, CONFIG_FIRETV_PYTHON_ADB) + + +async def test_setup_two_devices(hass): + """Test that two devices can be set up.""" + config = { + DOMAIN: [ + CONFIG_ANDROIDTV_ADB_SERVER[DOMAIN], + CONFIG_FIRETV_ADB_SERVER[DOMAIN].copy(), + ] + } + config[DOMAIN][1][CONF_HOST] = "127.0.0.2" + + patch_key = "server" + with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ + patch_key + ], patchers.patch_shell("")[patch_key]: + assert await async_setup_component(hass, DOMAIN, config) + + for entity_id in ["media_player.android_tv", "media_player.fire_tv"]: + 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 + + +async def test_setup_same_device_twice(hass): + """Test that setup succeeds with a duplicated config entry.""" + patch_key = "server" + + with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ + patch_key + ], patchers.patch_shell("")[patch_key]: + assert await async_setup_component(hass, DOMAIN, CONFIG_ANDROIDTV_ADB_SERVER) + + with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ + patch_key + ], patchers.patch_shell("")[patch_key]: + assert await async_setup_component(hass, DOMAIN, CONFIG_ANDROIDTV_ADB_SERVER) diff --git a/tests/components/api/test_init.py b/tests/components/api/test_init.py index a6dbf3f1d96..28cad6dd04b 100644 --- a/tests/components/api/test_init.py +++ b/tests/components/api/test_init.py @@ -1,6 +1,5 @@ """The tests for the Home Assistant API component.""" # pylint: disable=protected-access -import asyncio import json from unittest.mock import patch @@ -23,27 +22,23 @@ def mock_api_client(hass, hass_client): return hass.loop.run_until_complete(hass_client()) -@asyncio.coroutine -def test_api_list_state_entities(hass, mock_api_client): +async def test_api_list_state_entities(hass, mock_api_client): """Test if the debug interface allows us to list state entities.""" hass.states.async_set("test.entity", "hello") - resp = yield from mock_api_client.get(const.URL_API_STATES) + resp = await mock_api_client.get(const.URL_API_STATES) assert resp.status == 200 - json = yield from resp.json() + json = await resp.json() remote_data = [ha.State.from_dict(item) for item in json] assert remote_data == hass.states.async_all() -@asyncio.coroutine -def test_api_get_state(hass, mock_api_client): +async def test_api_get_state(hass, mock_api_client): """Test if the debug interface allows us to get a state.""" hass.states.async_set("hello.world", "nice", {"attr": 1}) - resp = yield from mock_api_client.get( - const.URL_API_STATES_ENTITY.format("hello.world") - ) + resp = await mock_api_client.get(const.URL_API_STATES_ENTITY.format("hello.world")) assert resp.status == 200 - json = yield from resp.json() + json = await resp.json() data = ha.State.from_dict(json) @@ -54,21 +49,19 @@ def test_api_get_state(hass, mock_api_client): assert data.attributes == state.attributes -@asyncio.coroutine -def test_api_get_non_existing_state(hass, mock_api_client): +async def test_api_get_non_existing_state(hass, mock_api_client): """Test if the debug interface allows us to get a state.""" - resp = yield from mock_api_client.get( + resp = await mock_api_client.get( const.URL_API_STATES_ENTITY.format("does_not_exist") ) assert resp.status == 404 -@asyncio.coroutine -def test_api_state_change(hass, mock_api_client): +async def test_api_state_change(hass, mock_api_client): """Test if we can change the state of an entity that exists.""" hass.states.async_set("test.test", "not_to_be_set") - yield from mock_api_client.post( + await mock_api_client.post( const.URL_API_STATES_ENTITY.format("test.test"), json={"state": "debug_state_change2"}, ) @@ -77,12 +70,11 @@ def test_api_state_change(hass, mock_api_client): # pylint: disable=invalid-name -@asyncio.coroutine -def test_api_state_change_of_non_existing_entity(hass, mock_api_client): +async def test_api_state_change_of_non_existing_entity(hass, mock_api_client): """Test if changing a state of a non existing entity is possible.""" new_state = "debug_state_change" - resp = yield from mock_api_client.post( + resp = await mock_api_client.post( const.URL_API_STATES_ENTITY.format("test_entity.that_does_not_exist"), json={"state": new_state}, ) @@ -93,10 +85,9 @@ def test_api_state_change_of_non_existing_entity(hass, mock_api_client): # pylint: disable=invalid-name -@asyncio.coroutine -def test_api_state_change_with_bad_data(hass, mock_api_client): +async def test_api_state_change_with_bad_data(hass, mock_api_client): """Test if API sends appropriate error if we omit state.""" - resp = yield from mock_api_client.post( + resp = await mock_api_client.post( const.URL_API_STATES_ENTITY.format("test_entity.that_does_not_exist"), json={} ) @@ -104,17 +95,16 @@ def test_api_state_change_with_bad_data(hass, mock_api_client): # pylint: disable=invalid-name -@asyncio.coroutine -def test_api_state_change_to_zero_value(hass, mock_api_client): +async def test_api_state_change_to_zero_value(hass, mock_api_client): """Test if changing a state to a zero value is possible.""" - resp = yield from mock_api_client.post( + resp = await mock_api_client.post( const.URL_API_STATES_ENTITY.format("test_entity.with_zero_state"), json={"state": 0}, ) assert resp.status == 201 - resp = yield from mock_api_client.post( + resp = await mock_api_client.post( const.URL_API_STATES_ENTITY.format("test_entity.with_zero_state"), json={"state": 0.0}, ) @@ -123,8 +113,7 @@ def test_api_state_change_to_zero_value(hass, mock_api_client): # pylint: disable=invalid-name -@asyncio.coroutine -def test_api_state_change_push(hass, mock_api_client): +async def test_api_state_change_push(hass, mock_api_client): """Test if we can push a change the state of an entity.""" hass.states.async_set("test.test", "not_to_be_set") @@ -137,23 +126,22 @@ def test_api_state_change_push(hass, mock_api_client): hass.bus.async_listen(const.EVENT_STATE_CHANGED, event_listener) - yield from mock_api_client.post( + await mock_api_client.post( const.URL_API_STATES_ENTITY.format("test.test"), json={"state": "not_to_be_set"} ) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert len(events) == 0 - yield from mock_api_client.post( + await mock_api_client.post( const.URL_API_STATES_ENTITY.format("test.test"), json={"state": "not_to_be_set", "force_update": True}, ) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert len(events) == 1 # pylint: disable=invalid-name -@asyncio.coroutine -def test_api_fire_event_with_no_data(hass, mock_api_client): +async def test_api_fire_event_with_no_data(hass, mock_api_client): """Test if the API allows us to fire an event.""" test_value = [] @@ -164,17 +152,14 @@ def test_api_fire_event_with_no_data(hass, mock_api_client): hass.bus.async_listen_once("test.event_no_data", listener) - yield from mock_api_client.post( - const.URL_API_EVENTS_EVENT.format("test.event_no_data") - ) - yield from hass.async_block_till_done() + await mock_api_client.post(const.URL_API_EVENTS_EVENT.format("test.event_no_data")) + await hass.async_block_till_done() assert len(test_value) == 1 # pylint: disable=invalid-name -@asyncio.coroutine -def test_api_fire_event_with_data(hass, mock_api_client): +async def test_api_fire_event_with_data(hass, mock_api_client): """Test if the API allows us to fire an event.""" test_value = [] @@ -189,18 +174,17 @@ def test_api_fire_event_with_data(hass, mock_api_client): hass.bus.async_listen_once("test_event_with_data", listener) - yield from mock_api_client.post( + await mock_api_client.post( const.URL_API_EVENTS_EVENT.format("test_event_with_data"), json={"test": 1} ) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert len(test_value) == 1 # pylint: disable=invalid-name -@asyncio.coroutine -def test_api_fire_event_with_invalid_json(hass, mock_api_client): +async def test_api_fire_event_with_invalid_json(hass, mock_api_client): """Test if the API allows us to fire an event.""" test_value = [] @@ -211,33 +195,32 @@ def test_api_fire_event_with_invalid_json(hass, mock_api_client): hass.bus.async_listen_once("test_event_bad_data", listener) - resp = yield from mock_api_client.post( + resp = await mock_api_client.post( const.URL_API_EVENTS_EVENT.format("test_event_bad_data"), data=json.dumps("not an object"), ) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert resp.status == 400 assert len(test_value) == 0 # Try now with valid but unusable JSON - resp = yield from mock_api_client.post( + resp = await mock_api_client.post( const.URL_API_EVENTS_EVENT.format("test_event_bad_data"), data=json.dumps([1, 2, 3]), ) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert resp.status == 400 assert len(test_value) == 0 -@asyncio.coroutine -def test_api_get_config(hass, mock_api_client): +async def test_api_get_config(hass, mock_api_client): """Test the return of the configuration.""" - resp = yield from mock_api_client.get(const.URL_API_CONFIG) - result = yield from resp.json() + resp = await mock_api_client.get(const.URL_API_CONFIG) + result = await resp.json() if "components" in result: result["components"] = set(result["components"]) if "whitelist_external_dirs" in result: @@ -246,19 +229,17 @@ def test_api_get_config(hass, mock_api_client): assert hass.config.as_dict() == result -@asyncio.coroutine -def test_api_get_components(hass, mock_api_client): +async def test_api_get_components(hass, mock_api_client): """Test the return of the components.""" - resp = yield from mock_api_client.get(const.URL_API_COMPONENTS) - result = yield from resp.json() + resp = await mock_api_client.get(const.URL_API_COMPONENTS) + result = await resp.json() assert set(result) == hass.config.components -@asyncio.coroutine -def test_api_get_event_listeners(hass, mock_api_client): +async def test_api_get_event_listeners(hass, mock_api_client): """Test if we can get the list of events being listened for.""" - resp = yield from mock_api_client.get(const.URL_API_EVENTS) - data = yield from resp.json() + resp = await mock_api_client.get(const.URL_API_EVENTS) + data = await resp.json() local = hass.bus.async_listeners() @@ -268,11 +249,10 @@ def test_api_get_event_listeners(hass, mock_api_client): assert len(local) == 0 -@asyncio.coroutine -def test_api_get_services(hass, mock_api_client): +async def test_api_get_services(hass, mock_api_client): """Test if we can get a dict describing current services.""" - resp = yield from mock_api_client.get(const.URL_API_SERVICES) - data = yield from resp.json() + resp = await mock_api_client.get(const.URL_API_SERVICES) + data = await resp.json() local_services = hass.services.async_services() for serv_domain in data: @@ -281,8 +261,7 @@ def test_api_get_services(hass, mock_api_client): assert serv_domain["services"] == local -@asyncio.coroutine -def test_api_call_service_no_data(hass, mock_api_client): +async def test_api_call_service_no_data(hass, mock_api_client): """Test if the API allows us to call a service.""" test_value = [] @@ -293,15 +272,14 @@ def test_api_call_service_no_data(hass, mock_api_client): hass.services.async_register("test_domain", "test_service", listener) - yield from mock_api_client.post( + await mock_api_client.post( const.URL_API_SERVICES_SERVICE.format("test_domain", "test_service") ) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert len(test_value) == 1 -@asyncio.coroutine -def test_api_call_service_with_data(hass, mock_api_client): +async def test_api_call_service_with_data(hass, mock_api_client): """Test if the API allows us to call a service.""" test_value = [] @@ -316,88 +294,83 @@ def test_api_call_service_with_data(hass, mock_api_client): hass.services.async_register("test_domain", "test_service", listener) - yield from mock_api_client.post( + await mock_api_client.post( const.URL_API_SERVICES_SERVICE.format("test_domain", "test_service"), json={"test": 1}, ) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert len(test_value) == 1 -@asyncio.coroutine -def test_api_template(hass, mock_api_client): +async def test_api_template(hass, mock_api_client): """Test the template API.""" hass.states.async_set("sensor.temperature", 10) - resp = yield from mock_api_client.post( + resp = await mock_api_client.post( const.URL_API_TEMPLATE, json={"template": "{{ states.sensor.temperature.state }}"}, ) - body = yield from resp.text() + body = await resp.text() assert body == "10" -@asyncio.coroutine -def test_api_template_error(hass, mock_api_client): +async def test_api_template_error(hass, mock_api_client): """Test the template API.""" hass.states.async_set("sensor.temperature", 10) - resp = yield from mock_api_client.post( + resp = await mock_api_client.post( const.URL_API_TEMPLATE, json={"template": "{{ states.sensor.temperature.state"} ) assert resp.status == 400 -@asyncio.coroutine -def test_stream(hass, mock_api_client): +async def test_stream(hass, mock_api_client): """Test the stream.""" listen_count = _listen_count(hass) - resp = yield from mock_api_client.get(const.URL_API_STREAM) + resp = await mock_api_client.get(const.URL_API_STREAM) assert resp.status == 200 assert listen_count + 1 == _listen_count(hass) hass.bus.async_fire("test_event") - data = yield from _stream_next_event(resp.content) + data = await _stream_next_event(resp.content) assert data["event_type"] == "test_event" -@asyncio.coroutine -def test_stream_with_restricted(hass, mock_api_client): +async def test_stream_with_restricted(hass, mock_api_client): """Test the stream with restrictions.""" listen_count = _listen_count(hass) - resp = yield from mock_api_client.get( + resp = await mock_api_client.get( "{}?restrict=test_event1,test_event3".format(const.URL_API_STREAM) ) assert resp.status == 200 assert listen_count + 1 == _listen_count(hass) hass.bus.async_fire("test_event1") - data = yield from _stream_next_event(resp.content) + data = await _stream_next_event(resp.content) assert data["event_type"] == "test_event1" hass.bus.async_fire("test_event2") hass.bus.async_fire("test_event3") - data = yield from _stream_next_event(resp.content) + data = await _stream_next_event(resp.content) assert data["event_type"] == "test_event3" -@asyncio.coroutine -def _stream_next_event(stream): +async def _stream_next_event(stream): """Read the stream for next event while ignoring ping.""" while True: last_new_line = False data = b"" while True: - dat = yield from stream.read(1) + dat = await stream.read(1) if dat == b"\n" and last_new_line: break data += dat diff --git a/tests/components/apns/test_notify.py b/tests/components/apns/test_notify.py index 78f597c58ad..61092899e24 100644 --- a/tests/components/apns/test_notify.py +++ b/tests/components/apns/test_notify.py @@ -1,15 +1,15 @@ """The tests for the APNS component.""" import io import unittest -from unittest.mock import Mock, patch, mock_open +from unittest.mock import Mock, mock_open, patch from apns2.errors import Unregistered import yaml -import homeassistant.components.notify as notify -from homeassistant.setup import setup_component import homeassistant.components.apns.notify as apns +import homeassistant.components.notify as notify from homeassistant.core import State +from homeassistant.setup import setup_component from tests.common import assert_setup_component, get_test_home_assistant @@ -121,7 +121,7 @@ class TestApns(unittest.TestCase): self._setup_notify() assert self.hass.services.call( - notify.DOMAIN, + apns.DOMAIN, "apns_test_app", {"push_id": "1234", "name": "test device"}, blocking=True, @@ -153,7 +153,7 @@ class TestApns(unittest.TestCase): self._setup_notify() assert self.hass.services.call( - notify.DOMAIN, "apns_test_app", {"push_id": "1234"}, blocking=True + apns.DOMAIN, "apns_test_app", {"push_id": "1234"}, blocking=True ) devices = {dev.push_id: dev for dev in written_devices} @@ -183,7 +183,7 @@ class TestApns(unittest.TestCase): self._setup_notify() assert self.hass.services.call( - notify.DOMAIN, + apns.DOMAIN, "apns_test_app", {"push_id": "1234", "name": "updated device 1"}, blocking=True, @@ -222,7 +222,7 @@ class TestApns(unittest.TestCase): self._setup_notify() assert self.hass.services.call( - notify.DOMAIN, + apns.DOMAIN, "apns_test_app", {"push_id": "1234", "name": "updated device 1"}, blocking=True, diff --git a/tests/components/apprise/test_notify.py b/tests/components/apprise/test_notify.py index 237f99de676..a275e57653d 100644 --- a/tests/components/apprise/test_notify.py +++ b/tests/components/apprise/test_notify.py @@ -1,6 +1,5 @@ """The tests for the apprise notification platform.""" -from unittest.mock import patch -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch from homeassistant.setup import async_setup_component diff --git a/tests/components/aprs/test_device_tracker.py b/tests/components/aprs/test_device_tracker.py index d02188a3079..dc0cf09f28d 100644 --- a/tests/components/aprs/test_device_tracker.py +++ b/tests/components/aprs/test_device_tracker.py @@ -302,7 +302,7 @@ def test_aprs_listener_rx_msg_no_position(): def test_setup_scanner(): """Test setup_scanner.""" with patch( - "homeassistant.components." "aprs.device_tracker.AprsListenerThread" + "homeassistant.components.aprs.device_tracker.AprsListenerThread" ) as listener: hass = get_test_home_assistant() hass.start() diff --git a/tests/components/arcam_fmj/conftest.py b/tests/components/arcam_fmj/conftest.py new file mode 100644 index 00000000000..6611cbaf9eb --- /dev/null +++ b/tests/components/arcam_fmj/conftest.py @@ -0,0 +1,59 @@ +"""Tests for the arcam_fmj component.""" +from arcam.fmj.client import Client +from arcam.fmj.state import State +from asynctest import Mock +import pytest + +from homeassistant.components.arcam_fmj import DEVICE_SCHEMA +from homeassistant.components.arcam_fmj.const import DOMAIN +from homeassistant.components.arcam_fmj.media_player import ArcamFmj +from homeassistant.const import CONF_HOST, CONF_PORT + +MOCK_HOST = "127.0.0.1" +MOCK_PORT = 1234 +MOCK_TURN_ON = { + "service": "switch.turn_on", + "data": {"entity_id": "switch.test"}, +} +MOCK_NAME = "dummy" +MOCK_ENTITY_ID = "media_player.arcam_fmj_1" +MOCK_CONFIG = DEVICE_SCHEMA({CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT}) + + +@pytest.fixture(name="config") +def config_fixture(): + """Create hass config fixture.""" + return {DOMAIN: [{CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT}]} + + +@pytest.fixture(name="client") +def client_fixture(): + """Get a mocked client.""" + client = Mock(Client) + client.host = MOCK_HOST + client.port = MOCK_PORT + return client + + +@pytest.fixture(name="state") +def state_fixture(client): + """Get a mocked state.""" + state = Mock(State) + state.client = client + state.zn = 1 + state.get_power.return_value = True + state.get_volume.return_value = 0.0 + state.get_source_list.return_value = [] + state.get_incoming_audio_format.return_value = (0, 0) + state.get_mute.return_value = None + return state + + +@pytest.fixture(name="player") +def player_fixture(hass, state): + """Get standard player.""" + player = ArcamFmj(state, MOCK_NAME, None) + player.entity_id = MOCK_ENTITY_ID + player.hass = hass + player.async_schedule_update_ha_state = Mock() + return player diff --git a/tests/components/arcam_fmj/test_config_flow.py b/tests/components/arcam_fmj/test_config_flow.py index 54fb34443a5..6df280fa92e 100644 --- a/tests/components/arcam_fmj/test_config_flow.py +++ b/tests/components/arcam_fmj/test_config_flow.py @@ -1,41 +1,37 @@ """Tests for the Arcam FMJ config flow module.""" + import pytest + from homeassistant import data_entry_flow -from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.components.arcam_fmj.config_flow import ArcamFmjFlowHandler +from homeassistant.components.arcam_fmj.const import DOMAIN -from tests.common import MockConfigEntry, MockDependency +from .conftest import MOCK_CONFIG, MOCK_NAME -with MockDependency("arcam"), MockDependency("arcam.fmj"), MockDependency( - "arcam.fmj.client" -): - from homeassistant.components.arcam_fmj import DEVICE_SCHEMA - from homeassistant.components.arcam_fmj.config_flow import ArcamFmjFlowHandler - from homeassistant.components.arcam_fmj.const import DOMAIN +from tests.common import MockConfigEntry - MOCK_HOST = "127.0.0.1" - MOCK_PORT = 1234 - MOCK_NAME = "Arcam FMJ" - MOCK_CONFIG = DEVICE_SCHEMA({CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT}) - @pytest.fixture(name="config_entry") - def config_entry_fixture(): - """Create a mock HEOS config entry.""" - return MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG, title=MOCK_NAME) +@pytest.fixture(name="config_entry") +def config_entry_fixture(): + """Create a mock Arcam config entry.""" + return MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG, title=MOCK_NAME) - async def test_single_import_only(hass, config_entry): - """Test form is shown when host not provided.""" - config_entry.add_to_hass(hass) - flow = ArcamFmjFlowHandler() - flow.hass = hass - result = await flow.async_step_import(MOCK_CONFIG) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_setup" - async def test_import(hass): - """Test form is shown when host not provided.""" - flow = ArcamFmjFlowHandler() - flow.hass = hass - result = await flow.async_step_import(MOCK_CONFIG) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == MOCK_NAME - assert result["data"] == MOCK_CONFIG +async def test_single_import_only(hass, config_entry): + """Test form is shown when host not provided.""" + config_entry.add_to_hass(hass) + flow = ArcamFmjFlowHandler() + flow.hass = hass + result = await flow.async_step_import(MOCK_CONFIG) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_setup" + + +async def test_import(hass): + """Test form is shown when host not provided.""" + flow = ArcamFmjFlowHandler() + flow.hass = hass + result = await flow.async_step_import(MOCK_CONFIG) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "Arcam FMJ" + assert result["data"] == MOCK_CONFIG diff --git a/tests/components/arcam_fmj/test_media_player.py b/tests/components/arcam_fmj/test_media_player.py new file mode 100644 index 00000000000..8448a25a7fd --- /dev/null +++ b/tests/components/arcam_fmj/test_media_player.py @@ -0,0 +1,375 @@ +"""Tests for arcam fmj receivers.""" +from math import isclose + +from arcam.fmj import DecodeMode2CH, DecodeModeMCH, IncomingAudioFormat, SourceCodes +from asynctest.mock import ANY, MagicMock, Mock, PropertyMock, patch +import pytest + +from homeassistant.components.media_player.const import MEDIA_TYPE_MUSIC +from homeassistant.core import HomeAssistant + +from .conftest import MOCK_ENTITY_ID, MOCK_HOST, MOCK_NAME, MOCK_PORT + +MOCK_TURN_ON = { + "service": "switch.turn_on", + "data": {"entity_id": "switch.test"}, +} + + +async def update(player, force_refresh=False): + """Force a update of player and return current state data.""" + await player.async_update_ha_state(force_refresh=force_refresh) + return player.hass.states.get(player.entity_id) + + +async def test_properties(player, state): + """Test standard properties.""" + assert player.unique_id is None + assert player.device_info == { + "identifiers": {("arcam_fmj", MOCK_HOST, MOCK_PORT)}, + "model": "FMJ", + "manufacturer": "Arcam", + } + assert not player.should_poll + + +async def test_powered_off(hass, player, state): + """Test properties in powered off state.""" + state.get_source.return_value = None + state.get_power.return_value = None + + data = await update(player) + assert "source" not in data.attributes + assert data.state == "off" + + +async def test_powered_on(player, state): + """Test properties in powered on state.""" + state.get_source.return_value = SourceCodes.PVR + state.get_power.return_value = True + + data = await update(player) + assert data.attributes["source"] == "PVR" + assert data.state == "on" + + +async def test_supported_features_no_service(player, state): + """Test support when turn on service exist.""" + state.get_power.return_value = None + data = await update(player) + assert data.attributes["supported_features"] == 68876 + + state.get_power.return_value = False + data = await update(player) + assert data.attributes["supported_features"] == 69004 + + +async def test_supported_features_service(hass, state): + """Test support when turn on service exist.""" + from homeassistant.components.arcam_fmj.media_player import ArcamFmj + + player = ArcamFmj(state, "dummy", MOCK_TURN_ON) + player.hass = hass + player.entity_id = MOCK_ENTITY_ID + + state.get_power.return_value = None + data = await update(player) + assert data.attributes["supported_features"] == 69004 + + state.get_power.return_value = False + data = await update(player) + assert data.attributes["supported_features"] == 69004 + + +async def test_turn_on_without_service(player, state): + """Test turn on service.""" + state.get_power.return_value = None + await player.async_turn_on() + state.set_power.assert_not_called() + + state.get_power.return_value = False + await player.async_turn_on() + state.set_power.assert_called_with(True) + + +async def test_turn_on_with_service(hass, state): + """Test support when turn on service exist.""" + from homeassistant.components.arcam_fmj.media_player import ArcamFmj + + player = ArcamFmj(state, "dummy", MOCK_TURN_ON) + player.hass = Mock(HomeAssistant) + player.entity_id = MOCK_ENTITY_ID + with patch( + "homeassistant.components.arcam_fmj.media_player.async_call_from_config" + ) as async_call_from_config: + + state.get_power.return_value = None + await player.async_turn_on() + state.set_power.assert_not_called() + async_call_from_config.assert_called_with( + player.hass, + MOCK_TURN_ON, + variables=None, + blocking=True, + validate_config=False, + ) + + +async def test_turn_off(player, state): + """Test command to turn off.""" + await player.async_turn_off() + state.set_power.assert_called_with(False) + + +@pytest.mark.parametrize("mute", [True, False]) +async def test_mute_volume(player, state, mute): + """Test mute functionallity.""" + await player.async_mute_volume(mute) + state.set_mute.assert_called_with(mute) + player.async_schedule_update_ha_state.assert_called_with() + + +async def test_name(player): + """Test name.""" + assert player.name == MOCK_NAME + + +async def test_update(player, state): + """Test update.""" + await update(player, force_refresh=True) + state.update.assert_called_with() + + +@pytest.mark.parametrize( + "fmt, result", + [ + (None, True), + (IncomingAudioFormat.PCM, True), + (IncomingAudioFormat.ANALOGUE_DIRECT, True), + (IncomingAudioFormat.DOLBY_DIGITAL, False), + ], +) +async def test_2ch(player, state, fmt, result): + """Test selection of 2ch mode.""" + state.get_incoming_audio_format.return_value = (fmt, None) + assert player._get_2ch() == result # pylint: disable=W0212 + + +@pytest.mark.parametrize( + "source, value", + [("PVR", SourceCodes.PVR), ("BD", SourceCodes.BD), ("INVALID", None)], +) +async def test_select_source(player, state, source, value): + """Test selection of source.""" + await player.async_select_source(source) + if value: + state.set_source.assert_called_with(value) + else: + state.set_source.assert_not_called() + + +async def test_source_list(player, state): + """Test source list.""" + state.get_source_list.return_value = [SourceCodes.BD] + data = await update(player) + assert data.attributes["source_list"] == ["BD"] + + +@pytest.mark.parametrize( + "mode, mode_sel, mode_2ch, mode_mch", + [ + ("STEREO", True, DecodeMode2CH.STEREO, None), + ("STEREO", False, None, None), + ("STEREO", False, None, None), + ], +) +async def test_select_sound_mode(player, state, mode, mode_sel, mode_2ch, mode_mch): + """Test selection sound mode.""" + player._get_2ch = Mock(return_value=mode_sel) # pylint: disable=W0212 + + await player.async_select_sound_mode(mode) + if mode_2ch: + state.set_decode_mode_2ch.assert_called_with(mode_2ch) + else: + state.set_decode_mode_2ch.assert_not_called() + + if mode_mch: + state.set_decode_mode_mch.assert_called_with(mode_mch) + else: + state.set_decode_mode_mch.assert_not_called() + + +async def test_volume_up(player, state): + """Test mute functionallity.""" + await player.async_volume_up() + state.inc_volume.assert_called_with() + player.async_schedule_update_ha_state.assert_called_with() + + +async def test_volume_down(player, state): + """Test mute functionallity.""" + await player.async_volume_down() + state.dec_volume.assert_called_with() + player.async_schedule_update_ha_state.assert_called_with() + + +@pytest.mark.parametrize( + "mode, mode_sel, mode_2ch, mode_mch", + [ + ("STEREO", True, DecodeMode2CH.STEREO, None), + ("STEREO_DOWNMIX", False, None, DecodeModeMCH.STEREO_DOWNMIX), + (None, False, None, None), + ], +) +async def test_sound_mode(player, state, mode, mode_sel, mode_2ch, mode_mch): + """Test selection sound mode.""" + player._get_2ch = Mock(return_value=mode_sel) # pylint: disable=W0212 + state.get_decode_mode_2ch.return_value = mode_2ch + state.get_decode_mode_mch.return_value = mode_mch + + assert player.sound_mode == mode + + +async def test_sound_mode_list(player, state): + """Test sound mode list.""" + player._get_2ch = Mock(return_value=True) # pylint: disable=W0212 + assert sorted(player.sound_mode_list) == sorted([x.name for x in DecodeMode2CH]) + player._get_2ch = Mock(return_value=False) # pylint: disable=W0212 + assert sorted(player.sound_mode_list) == sorted([x.name for x in DecodeModeMCH]) + + +async def test_sound_mode_zone_x(player, state): + """Test second zone sound mode.""" + state.zn = 2 + assert player.sound_mode is None + assert player.sound_mode_list is None + + +async def test_is_volume_muted(player, state): + """Test muted.""" + state.get_mute.return_value = True + assert player.is_volume_muted is True # pylint: disable=singleton-comparison + state.get_mute.return_value = False + assert player.is_volume_muted is False # pylint: disable=singleton-comparison + state.get_mute.return_value = None + assert player.is_volume_muted is None + + +async def test_volume_level(player, state): + """Test volume.""" + state.get_volume.return_value = 0 + assert isclose(player.volume_level, 0.0) + state.get_volume.return_value = 50 + assert isclose(player.volume_level, 50.0 / 99) + state.get_volume.return_value = 99 + assert isclose(player.volume_level, 1.0) + state.get_volume.return_value = None + assert player.volume_level is None + + +@pytest.mark.parametrize("volume, call", [(0.0, 0), (0.5, 50), (1.0, 99)]) +async def test_set_volume_level(player, state, volume, call): + """Test setting volume.""" + await player.async_set_volume_level(volume) + state.set_volume.assert_called_with(call) + + +@pytest.mark.parametrize( + "source, media_content_type", + [ + (SourceCodes.DAB, MEDIA_TYPE_MUSIC), + (SourceCodes.FM, MEDIA_TYPE_MUSIC), + (SourceCodes.PVR, None), + (None, None), + ], +) +async def test_media_content_type(player, state, source, media_content_type): + """Test content type deduction.""" + state.get_source.return_value = source + assert player.media_content_type == media_content_type + + +@pytest.mark.parametrize( + "source, dab, rds, channel", + [ + (SourceCodes.DAB, "dab", "rds", "dab"), + (SourceCodes.DAB, None, None, None), + (SourceCodes.FM, "dab", "rds", "rds"), + (SourceCodes.FM, None, None, None), + (SourceCodes.PVR, "dab", "rds", None), + ], +) +async def test_media_channel(player, state, source, dab, rds, channel): + """Test media channel.""" + state.get_dab_station.return_value = dab + state.get_rds_information.return_value = rds + state.get_source.return_value = source + assert player.media_channel == channel + + +@pytest.mark.parametrize( + "source, dls, artist", + [ + (SourceCodes.DAB, "dls", "dls"), + (SourceCodes.FM, "dls", None), + (SourceCodes.DAB, None, None), + ], +) +async def test_media_artist(player, state, source, dls, artist): + """Test media artist.""" + state.get_dls_pdt.return_value = dls + state.get_source.return_value = source + assert player.media_artist == artist + + +@pytest.mark.parametrize( + "source, channel, title", + [ + (SourceCodes.DAB, "channel", "DAB - channel"), + (SourceCodes.DAB, None, "DAB"), + (None, None, None), + ], +) +async def test_media_title(player, state, source, channel, title): + """Test media title.""" + from homeassistant.components.arcam_fmj.media_player import ArcamFmj + + state.get_source.return_value = source + with patch.object( + ArcamFmj, "media_channel", new_callable=PropertyMock + ) as media_channel: + media_channel.return_value = channel + data = await update(player) + if title is None: + assert "media_title" not in data.attributes + else: + assert data.attributes["media_title"] == title + + +async def test_added_to_hass(player, state): + """Test addition to hass.""" + from homeassistant.components.arcam_fmj.const import ( + SIGNAL_CLIENT_DATA, + SIGNAL_CLIENT_STARTED, + SIGNAL_CLIENT_STOPPED, + ) + + connectors = {} + + def _connect(signal, fun): + connectors[signal] = fun + + player.hass = MagicMock() + player.hass.helpers.dispatcher.async_dispatcher_connect.side_effects = _connect + + await player.async_added_to_hass() + state.start.assert_called_with() + player.hass.helpers.dispatcher.async_dispatcher_connect.assert_any_call( + SIGNAL_CLIENT_DATA, ANY + ) + player.hass.helpers.dispatcher.async_dispatcher_connect.assert_any_call( + SIGNAL_CLIENT_STARTED, ANY + ) + player.hass.helpers.dispatcher.async_dispatcher_connect.assert_any_call( + SIGNAL_CLIENT_STOPPED, ANY + ) diff --git a/tests/components/arlo/test_sensor.py b/tests/components/arlo/test_sensor.py index d06b48ac3a2..ac64ad8f272 100644 --- a/tests/components/arlo/test_sensor.py +++ b/tests/components/arlo/test_sensor.py @@ -1,14 +1,15 @@ """The tests for the Netgear Arlo sensors.""" from collections import namedtuple -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch + import pytest + +from homeassistant.components.arlo import DATA_ARLO, sensor as arlo from homeassistant.const import ( - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_HUMIDITY, ATTR_ATTRIBUTION, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, ) -from homeassistant.components.arlo import sensor as arlo -from homeassistant.components.arlo import DATA_ARLO def _get_named_tuple(input_dict): diff --git a/tests/components/asuswrt/test_device_tracker.py b/tests/components/asuswrt/test_device_tracker.py index a3fde3a6855..2ecab9c1d37 100644 --- a/tests/components/asuswrt/test_device_tracker.py +++ b/tests/components/asuswrt/test_device_tracker.py @@ -1,16 +1,17 @@ """The tests for the ASUSWRT device tracker platform.""" -from homeassistant.setup import async_setup_component +from unittest.mock import patch from homeassistant.components.asuswrt import ( - CONF_PROTOCOL, CONF_MODE, - DOMAIN, CONF_PORT, + CONF_PROTOCOL, DATA_ASUSWRT, + DOMAIN, ) -from homeassistant.const import CONF_PLATFORM, CONF_PASSWORD, CONF_USERNAME, CONF_HOST +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PLATFORM, CONF_USERNAME +from homeassistant.setup import async_setup_component -from tests.common import MockDependency, mock_coro_func +from tests.common import mock_coro_func FAKEFILE = None @@ -28,9 +29,9 @@ VALID_CONFIG_ROUTER_SSH = { async def test_password_or_pub_key_required(hass): """Test creating an AsusWRT scanner without a pass or pubkey.""" - with MockDependency("aioasuswrt.asuswrt") as mocked_asus: - mocked_asus.AsusWrt().connection.async_connect = mock_coro_func() - mocked_asus.AsusWrt().is_connected = False + with patch("homeassistant.components.asuswrt.AsusWrt") as AsusWrt: + AsusWrt().connection.async_connect = mock_coro_func() + AsusWrt().is_connected = False result = await async_setup_component( hass, DOMAIN, {DOMAIN: {CONF_HOST: "fake_host", CONF_USERNAME: "fake_user"}} ) @@ -39,9 +40,9 @@ async def test_password_or_pub_key_required(hass): async def test_get_scanner_with_password_no_pubkey(hass): """Test creating an AsusWRT scanner with a password and no pubkey.""" - with MockDependency("aioasuswrt.asuswrt") as mocked_asus: - mocked_asus.AsusWrt().connection.async_connect = mock_coro_func() - mocked_asus.AsusWrt().connection.async_get_connected_devices = mock_coro_func( + with patch("homeassistant.components.asuswrt.AsusWrt") as AsusWrt: + AsusWrt().connection.async_connect = mock_coro_func() + AsusWrt().connection.async_get_connected_devices = mock_coro_func( return_value={} ) result = await async_setup_component( diff --git a/tests/components/aurora/test_binary_sensor.py b/tests/components/aurora/test_binary_sensor.py index 72cef0b44ca..1683e1951a0 100644 --- a/tests/components/aurora/test_binary_sensor.py +++ b/tests/components/aurora/test_binary_sensor.py @@ -5,7 +5,8 @@ import unittest import requests_mock from homeassistant.components.aurora import binary_sensor as aurora -from tests.common import load_fixture, get_test_home_assistant + +from tests.common import get_test_home_assistant, load_fixture class TestAuroraSensorSetUp(unittest.TestCase): diff --git a/tests/components/auth/__init__.py b/tests/components/auth/__init__.py index 5114e18889b..7ce65964086 100644 --- a/tests/components/auth/__init__.py +++ b/tests/components/auth/__init__.py @@ -4,7 +4,6 @@ from homeassistant.setup import async_setup_component from tests.common import ensure_auth_manager_loaded - BASE_CONFIG = [ { "name": "Example", diff --git a/tests/components/auth/test_indieauth.py b/tests/components/auth/test_indieauth.py index 8cfb573939e..ce8edae1466 100644 --- a/tests/components/auth/test_indieauth.py +++ b/tests/components/auth/test_indieauth.py @@ -166,3 +166,24 @@ async def test_find_link_tag_max_size(hass, mock_session): redirect_uris = await indieauth.fetch_redirect_uris(hass, "http://127.0.0.1:8000") assert redirect_uris == ["http://127.0.0.1:8000/wine"] + + +@pytest.mark.parametrize( + "client_id", ["https://home-assistant.io/android", "https://home-assistant.io/iOS"] +) +async def test_verify_redirect_uri_android_ios(client_id): + """Test that we verify redirect uri correctly for Android/iOS.""" + with patch.object( + indieauth, "fetch_redirect_uris", side_effect=lambda *_: mock_coro([]) + ): + assert await indieauth.verify_redirect_uri( + None, client_id, "homeassistant://auth-callback" + ) + + assert not await indieauth.verify_redirect_uri( + None, client_id, "homeassistant://something-else" + ) + + assert not await indieauth.verify_redirect_uri( + None, "https://incorrect.com", "homeassistant://auth-callback" + ) diff --git a/tests/components/auth/test_init.py b/tests/components/auth/test_init.py index de91613b74b..162569aa0e8 100644 --- a/tests/components/auth/test_init.py +++ b/tests/components/auth/test_init.py @@ -3,15 +3,15 @@ from datetime import timedelta from unittest.mock import patch from homeassistant.auth.models import Credentials +from homeassistant.components import auth from homeassistant.components.auth import RESULT_TYPE_USER from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow -from homeassistant.components import auth - -from tests.common import CLIENT_ID, CLIENT_REDIRECT_URI, MockUser from . import async_setup_auth +from tests.common import CLIENT_ID, CLIENT_REDIRECT_URI, MockUser + async def test_login_new_user_and_trying_refresh_token(hass, aiohttp_client): """Test logging in with new user and refreshing tokens.""" diff --git a/tests/components/auth/test_mfa_setup_flow.py b/tests/components/auth/test_mfa_setup_flow.py index ffce9266e57..3569d7d5233 100644 --- a/tests/components/auth/test_mfa_setup_flow.py +++ b/tests/components/auth/test_mfa_setup_flow.py @@ -4,7 +4,7 @@ from homeassistant.auth import auth_manager_from_config from homeassistant.components.auth import mfa_setup_flow from homeassistant.setup import async_setup_component -from tests.common import MockUser, CLIENT_ID, ensure_auth_manager_loaded +from tests.common import CLIENT_ID, MockUser, ensure_auth_manager_loaded async def test_ws_setup_depose_mfa(hass, hass_ws_client): diff --git a/tests/components/automatic/test_device_tracker.py b/tests/components/automatic/test_device_tracker.py index 4186316e7ac..3611a5ae0e3 100644 --- a/tests/components/automatic/test_device_tracker.py +++ b/tests/components/automatic/test_device_tracker.py @@ -2,11 +2,12 @@ import asyncio from datetime import datetime import logging -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch + import aioautomatic -from homeassistant.setup import async_setup_component from homeassistant.components.automatic.device_tracker import async_setup_scanner +from homeassistant.setup import async_setup_component _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/automation/common.py b/tests/components/automation/common.py index 6fadbd14199..95b156bcb14 100644 --- a/tests/components/automation/common.py +++ b/tests/components/automation/common.py @@ -6,43 +6,46 @@ components. Instead call the service directly. from homeassistant.components.automation import DOMAIN, SERVICE_TRIGGER from homeassistant.const import ( ATTR_ENTITY_ID, - SERVICE_TURN_ON, - SERVICE_TURN_OFF, - SERVICE_TOGGLE, + ENTITY_MATCH_ALL, SERVICE_RELOAD, + SERVICE_TOGGLE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, ) from homeassistant.loader import bind_hass @bind_hass -async def async_turn_on(hass, entity_id=None): +async def async_turn_on(hass, entity_id=ENTITY_MATCH_ALL): """Turn on specified automation or all.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} await hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data) @bind_hass -async def async_turn_off(hass, entity_id=None): +async def async_turn_off(hass, entity_id=ENTITY_MATCH_ALL): """Turn off specified automation or all.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} await hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data) @bind_hass -async def async_toggle(hass, entity_id=None): +async def async_toggle(hass, entity_id=ENTITY_MATCH_ALL): """Toggle specified automation or all.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} await hass.services.async_call(DOMAIN, SERVICE_TOGGLE, data) @bind_hass -async def async_trigger(hass, entity_id=None): +async def async_trigger(hass, entity_id=ENTITY_MATCH_ALL): """Trigger specified automation or all.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} await hass.services.async_call(DOMAIN, SERVICE_TRIGGER, data) @bind_hass -async def async_reload(hass): +async def async_reload(hass, context=None): """Reload the automation from config.""" - await hass.services.async_call(DOMAIN, SERVICE_RELOAD) + await hass.services.async_call( + DOMAIN, SERVICE_RELOAD, blocking=True, context=context + ) diff --git a/tests/components/automation/test_event.py b/tests/components/automation/test_event.py index e8d6089f500..26d19d6fa47 100644 --- a/tests/components/automation/test_event.py +++ b/tests/components/automation/test_event.py @@ -1,13 +1,12 @@ """The tests for the Event automation.""" import pytest +import homeassistant.components.automation as automation from homeassistant.core import Context from homeassistant.setup import async_setup_component -import homeassistant.components.automation as automation -from tests.common import mock_component +from tests.common import async_mock_service, mock_component from tests.components.automation import common -from tests.common import async_mock_service @pytest.fixture diff --git a/tests/components/automation/test_geo_location.py b/tests/components/automation/test_geo_location.py index d1ded8da1c6..05e30458ef3 100644 --- a/tests/components/automation/test_geo_location.py +++ b/tests/components/automation/test_geo_location.py @@ -5,9 +5,8 @@ from homeassistant.components import automation, zone from homeassistant.core import Context from homeassistant.setup import async_setup_component -from tests.common import mock_component +from tests.common import async_mock_service, mock_component from tests.components.automation import common -from tests.common import async_mock_service @pytest.fixture diff --git a/tests/components/automation/test_homeassistant.py b/tests/components/automation/test_homeassistant.py index 003e900babc..d5bd4c6dd5b 100644 --- a/tests/components/automation/test_homeassistant.py +++ b/tests/components/automation/test_homeassistant.py @@ -1,9 +1,9 @@ """The tests for the Event automation.""" -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch +import homeassistant.components.automation as automation from homeassistant.core import CoreState from homeassistant.setup import async_setup_component -import homeassistant.components.automation as automation from tests.common import async_mock_service, mock_coro diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index a0573ce7c1b..d5498c04814 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -1,28 +1,28 @@ """The tests for the automation component.""" from datetime import timedelta -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch import pytest -from homeassistant.core import State, CoreState, Context -from homeassistant.setup import async_setup_component import homeassistant.components.automation as automation from homeassistant.const import ( - ATTR_NAME, ATTR_ENTITY_ID, - STATE_ON, - STATE_OFF, - EVENT_HOMEASSISTANT_START, + ATTR_NAME, EVENT_AUTOMATION_TRIGGERED, + EVENT_HOMEASSISTANT_START, + STATE_OFF, + STATE_ON, ) -from homeassistant.exceptions import HomeAssistantError +from homeassistant.core import Context, CoreState, State +from homeassistant.exceptions import HomeAssistantError, Unauthorized +from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from tests.common import ( assert_setup_component, async_fire_time_changed, - mock_restore_cache, async_mock_service, + mock_restore_cache, ) from tests.components.automation import common @@ -445,7 +445,7 @@ async def test_services(hass, calls): assert automation.is_on(hass, entity_id) -async def test_reload_config_service(hass, calls): +async def test_reload_config_service(hass, calls, hass_admin_user, hass_read_only_user): """Test the reload config service.""" assert await async_setup_component( hass, @@ -488,7 +488,10 @@ async def test_reload_config_service(hass, calls): }, ): with patch("homeassistant.config.find_config_file", return_value=""): - await common.async_reload(hass) + with pytest.raises(Unauthorized): + await common.async_reload(hass, Context(user_id=hass_read_only_user.id)) + await hass.async_block_till_done() + await common.async_reload(hass, Context(user_id=hass_admin_user.id)) await hass.async_block_till_done() # De-flake ?! await hass.async_block_till_done() diff --git a/tests/components/automation/test_litejet.py b/tests/components/automation/test_litejet.py index 2e6f578ef4c..75fbc03a589 100644 --- a/tests/components/automation/test_litejet.py +++ b/tests/components/automation/test_litejet.py @@ -1,13 +1,14 @@ """The tests for the litejet component.""" +from datetime import timedelta import logging from unittest import mock -from datetime import timedelta + import pytest from homeassistant import setup -import homeassistant.util.dt as dt_util from homeassistant.components import litejet import homeassistant.components.automation as automation +import homeassistant.util.dt as dt_util from tests.common import async_fire_time_changed, async_mock_service @@ -33,7 +34,7 @@ def get_switch_name(number): @pytest.fixture def mock_lj(hass): """Initialize components.""" - with mock.patch("pylitejet.LiteJet") as mock_pylitejet: + with mock.patch("homeassistant.components.litejet.LiteJet") as mock_pylitejet: mock_lj = mock_pylitejet.return_value mock_lj.switch_pressed_callbacks = {} diff --git a/tests/components/automation/test_mqtt.py b/tests/components/automation/test_mqtt.py index 7c6db978f5c..9dbe93a7998 100644 --- a/tests/components/automation/test_mqtt.py +++ b/tests/components/automation/test_mqtt.py @@ -1,14 +1,16 @@ """The tests for the MQTT automation.""" -import pytest from unittest import mock -from homeassistant.setup import async_setup_component +import pytest + import homeassistant.components.automation as automation +from homeassistant.setup import async_setup_component + from tests.common import ( async_fire_mqtt_message, - mock_component, - async_mock_service, async_mock_mqtt_component, + async_mock_service, + mock_component, ) from tests.components.automation import common diff --git a/tests/components/automation/test_numeric_state.py b/tests/components/automation/test_numeric_state.py index 3cb8e2588fc..c6c1fd83184 100644 --- a/tests/components/automation/test_numeric_state.py +++ b/tests/components/automation/test_numeric_state.py @@ -1,18 +1,19 @@ """The tests for numeric state automation.""" from datetime import timedelta -import pytest from unittest.mock import patch +import pytest + import homeassistant.components.automation as automation from homeassistant.core import Context from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from tests.common import ( - mock_component, - async_fire_time_changed, assert_setup_component, + async_fire_time_changed, async_mock_service, + mock_component, ) from tests.components.automation import common diff --git a/tests/components/automation/test_state.py b/tests/components/automation/test_state.py index 9d84fb3e8ce..b6f9a50cf9d 100644 --- a/tests/components/automation/test_state.py +++ b/tests/components/automation/test_state.py @@ -1,17 +1,21 @@ """The test for state automation.""" from datetime import timedelta - -import pytest from unittest.mock import patch +import pytest + +import homeassistant.components.automation as automation from homeassistant.core import Context from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -import homeassistant.components.automation as automation -from tests.common import async_fire_time_changed, assert_setup_component, mock_component +from tests.common import ( + assert_setup_component, + async_fire_time_changed, + async_mock_service, + mock_component, +) from tests.components.automation import common -from tests.common import async_mock_service @pytest.fixture diff --git a/tests/components/automation/test_sun.py b/tests/components/automation/test_sun.py index 2668ac97053..3468c9e9480 100644 --- a/tests/components/automation/test_sun.py +++ b/tests/components/automation/test_sun.py @@ -1,16 +1,16 @@ """The tests for the sun automation.""" from datetime import datetime - -import pytest from unittest.mock import patch -from homeassistant.const import SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET -from homeassistant.setup import async_setup_component +import pytest + from homeassistant.components import sun import homeassistant.components.automation as automation +from homeassistant.const import SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET +from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.common import async_fire_time_changed, mock_component, async_mock_service +from tests.common import async_fire_time_changed, async_mock_service, mock_component from tests.components.automation import common ORIG_TIME_ZONE = dt_util.DEFAULT_TIME_ZONE diff --git a/tests/components/automation/test_template.py b/tests/components/automation/test_template.py index d7726b7ffd8..d9566b8f464 100644 --- a/tests/components/automation/test_template.py +++ b/tests/components/automation/test_template.py @@ -4,14 +4,18 @@ from unittest import mock import pytest +import homeassistant.components.automation as automation from homeassistant.core import Context from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -import homeassistant.components.automation as automation -from tests.common import async_fire_time_changed, assert_setup_component, mock_component +from tests.common import ( + assert_setup_component, + async_fire_time_changed, + async_mock_service, + mock_component, +) from tests.components.automation import common -from tests.common import async_mock_service @pytest.fixture diff --git a/tests/components/automation/test_time.py b/tests/components/automation/test_time.py index fa931d06bfc..d84fd18fb6b 100644 --- a/tests/components/automation/test_time.py +++ b/tests/components/automation/test_time.py @@ -4,12 +4,16 @@ from unittest.mock import patch import pytest +import homeassistant.components.automation as automation from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -import homeassistant.components.automation as automation -from tests.common import async_fire_time_changed, assert_setup_component, mock_component -from tests.common import async_mock_service +from tests.common import ( + assert_setup_component, + async_fire_time_changed, + async_mock_service, + mock_component, +) @pytest.fixture @@ -35,7 +39,7 @@ 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 }}" }, }, } diff --git a/tests/components/automation/test_time_pattern.py b/tests/components/automation/test_time_pattern.py index 479bab1c78e..70d647a1241 100644 --- a/tests/components/automation/test_time_pattern.py +++ b/tests/components/automation/test_time_pattern.py @@ -1,13 +1,12 @@ """The tests for the time_pattern automation.""" import pytest +import homeassistant.components.automation as automation from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -import homeassistant.components.automation as automation -from tests.common import async_fire_time_changed, mock_component +from tests.common import async_fire_time_changed, async_mock_service, mock_component from tests.components.automation import common -from tests.common import async_mock_service @pytest.fixture diff --git a/tests/components/automation/test_zone.py b/tests/components/automation/test_zone.py index 07be09a8454..44ad20e16f0 100644 --- a/tests/components/automation/test_zone.py +++ b/tests/components/automation/test_zone.py @@ -1,12 +1,12 @@ """The tests for the location automation.""" import pytest +from homeassistant.components import automation, zone from homeassistant.core import Context from homeassistant.setup import async_setup_component -from homeassistant.components import automation, zone -from tests.components.automation import common from tests.common import async_mock_service, mock_component +from tests.components.automation import common @pytest.fixture diff --git a/tests/components/awair/test_sensor.py b/tests/components/awair/test_sensor.py index ee4e4906826..ded1520718f 100644 --- a/tests/components/awair/test_sensor.py +++ b/tests/components/awair/test_sensor.py @@ -6,7 +6,6 @@ import json import logging from unittest.mock import patch -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.awair.sensor import ( ATTR_LAST_API_UPDATE, ATTR_TIMESTAMP, @@ -15,6 +14,7 @@ from homeassistant.components.awair.sensor import ( DEVICE_CLASS_SCORE, DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, ) +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, diff --git a/tests/components/aws/test_init.py b/tests/components/aws/test_init.py index a9701ec7ff9..c7fa9d0a5c1 100644 --- a/tests/components/aws/test_init.py +++ b/tests/components/aws/test_init.py @@ -1,5 +1,5 @@ """Tests for the aws component config and setup.""" -from asynctest import patch as async_patch, MagicMock, CoroutineMock +from asynctest import CoroutineMock, MagicMock, patch as async_patch from homeassistant.components import aws from homeassistant.setup import async_setup_component diff --git a/tests/components/axis/test_binary_sensor.py b/tests/components/axis/test_binary_sensor.py index 8e5a6f9675d..ca3e984c993 100644 --- a/tests/components/axis/test_binary_sensor.py +++ b/tests/components/axis/test_binary_sensor.py @@ -4,9 +4,8 @@ from unittest.mock import Mock from homeassistant import config_entries from homeassistant.components import axis -from homeassistant.setup import async_setup_component - import homeassistant.components.binary_sensor as binary_sensor +from homeassistant.setup import async_setup_component EVENTS = [ { diff --git a/tests/components/axis/test_camera.py b/tests/components/axis/test_camera.py index 027dc42748e..67ca7e3690a 100644 --- a/tests/components/axis/test_camera.py +++ b/tests/components/axis/test_camera.py @@ -4,10 +4,8 @@ from unittest.mock import Mock from homeassistant import config_entries from homeassistant.components import axis -from homeassistant.setup import async_setup_component - import homeassistant.components.camera as camera - +from homeassistant.setup import async_setup_component ENTRY_CONFIG = { axis.CONF_DEVICE: { diff --git a/tests/components/axis/test_config_flow.py b/tests/components/axis/test_config_flow.py index 5aec416961d..a29c270e0b8 100644 --- a/tests/components/axis/test_config_flow.py +++ b/tests/components/axis/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import Mock, patch from homeassistant.components import axis from homeassistant.components.axis import config_flow -from tests.common import mock_coro, MockConfigEntry +from tests.common import MockConfigEntry, mock_coro async def test_configured_devices(hass): diff --git a/tests/components/axis/test_device.py b/tests/components/axis/test_device.py index b15e9e68ada..a9f38cc4f3a 100644 --- a/tests/components/axis/test_device.py +++ b/tests/components/axis/test_device.py @@ -3,11 +3,11 @@ from unittest.mock import Mock, patch import pytest -from tests.common import mock_coro, MockConfigEntry - from homeassistant.components.axis import device, errors from homeassistant.components.axis.camera import AxisCamera +from tests.common import MockConfigEntry, mock_coro + DEVICE_DATA = { device.CONF_HOST: "1.2.3.4", device.CONF_USERNAME: "username", diff --git a/tests/components/axis/test_init.py b/tests/components/axis/test_init.py index b1317eb88e9..831d1d6ea08 100644 --- a/tests/components/axis/test_init.py +++ b/tests/components/axis/test_init.py @@ -1,10 +1,10 @@ """Test Axis component setup process.""" from unittest.mock import Mock, patch -from homeassistant.setup import async_setup_component from homeassistant.components import axis +from homeassistant.setup import async_setup_component -from tests.common import mock_coro, MockConfigEntry +from tests.common import MockConfigEntry, mock_coro async def test_setup(hass): diff --git a/tests/components/axis/test_switch.py b/tests/components/axis/test_switch.py index 3469106c436..406e3170ab2 100644 --- a/tests/components/axis/test_switch.py +++ b/tests/components/axis/test_switch.py @@ -1,12 +1,11 @@ """Axis switch platform tests.""" -from unittest.mock import call as mock_call, Mock +from unittest.mock import Mock, call as mock_call from homeassistant import config_entries from homeassistant.components import axis -from homeassistant.setup import async_setup_component - import homeassistant.components.switch as switch +from homeassistant.setup import async_setup_component EVENTS = [ { diff --git a/tests/components/bayesian/test_binary_sensor.py b/tests/components/bayesian/test_binary_sensor.py index 0ea92b143be..d9341bb3271 100644 --- a/tests/components/bayesian/test_binary_sensor.py +++ b/tests/components/bayesian/test_binary_sensor.py @@ -1,8 +1,8 @@ """The test for the bayesian sensor platform.""" import unittest -from homeassistant.setup import setup_component from homeassistant.components.bayesian import binary_sensor as bayesian +from homeassistant.setup import setup_component from tests.common import get_test_home_assistant diff --git a/tests/components/binary_sensor/test_device_condition.py b/tests/components/binary_sensor/test_device_condition.py index 34cf4030a50..ecf5e86bdad 100644 --- a/tests/components/binary_sensor/test_device_condition.py +++ b/tests/components/binary_sensor/test_device_condition.py @@ -1,23 +1,24 @@ """The test for binary_sensor device automation.""" from datetime import timedelta -import pytest from unittest.mock import patch -from homeassistant.components.binary_sensor import DOMAIN, DEVICE_CLASSES -from homeassistant.components.binary_sensor.device_condition import ENTITY_CONDITIONS -from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM -from homeassistant.setup import async_setup_component +import pytest + import homeassistant.components.automation as automation +from homeassistant.components.binary_sensor import DEVICE_CLASSES, DOMAIN +from homeassistant.components.binary_sensor.device_condition import ENTITY_CONDITIONS +from homeassistant.const import CONF_PLATFORM, 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, + async_get_device_automation_capabilities, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, - async_get_device_automations, - async_get_device_automation_capabilities, ) diff --git a/tests/components/binary_sensor/test_device_trigger.py b/tests/components/binary_sensor/test_device_trigger.py index 9bab1ff1f36..404def66491 100644 --- a/tests/components/binary_sensor/test_device_trigger.py +++ b/tests/components/binary_sensor/test_device_trigger.py @@ -1,23 +1,24 @@ """The test for binary_sensor device automation.""" from datetime import timedelta + import pytest -from homeassistant.components.binary_sensor import DOMAIN, DEVICE_CLASSES -from homeassistant.components.binary_sensor.device_trigger import ENTITY_TRIGGERS -from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM -from homeassistant.setup import async_setup_component import homeassistant.components.automation as automation +from homeassistant.components.binary_sensor import DEVICE_CLASSES, DOMAIN +from homeassistant.components.binary_sensor.device_trigger import ENTITY_TRIGGERS +from homeassistant.const import CONF_PLATFORM, 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, async_fire_time_changed, + async_get_device_automation_capabilities, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, - async_get_device_automations, - async_get_device_automation_capabilities, ) diff --git a/tests/components/binary_sensor/test_init.py b/tests/components/binary_sensor/test_init.py index 0aa1a798487..2299ba4c9a2 100644 --- a/tests/components/binary_sensor/test_init.py +++ b/tests/components/binary_sensor/test_init.py @@ -3,7 +3,7 @@ import unittest from unittest import mock from homeassistant.components import binary_sensor -from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.const import STATE_OFF, STATE_ON class TestBinarySensor(unittest.TestCase): @@ -14,12 +14,11 @@ class TestBinarySensor(unittest.TestCase): sensor = binary_sensor.BinarySensorDevice() assert STATE_OFF == sensor.state with mock.patch( - "homeassistant.components.binary_sensor." "BinarySensorDevice.is_on", + "homeassistant.components.binary_sensor.BinarySensorDevice.is_on", new=False, ): assert STATE_OFF == binary_sensor.BinarySensorDevice().state with mock.patch( - "homeassistant.components.binary_sensor." "BinarySensorDevice.is_on", - new=True, + "homeassistant.components.binary_sensor.BinarySensorDevice.is_on", new=True, ): assert STATE_ON == binary_sensor.BinarySensorDevice().state diff --git a/tests/components/blackbird/test_media_player.py b/tests/components/blackbird/test_media_player.py index 34309fdbcf3..b090368a4ce 100644 --- a/tests/components/blackbird/test_media_player.py +++ b/tests/components/blackbird/test_media_player.py @@ -1,25 +1,25 @@ """The tests for the Monoprice Blackbird media player platform.""" +from collections import defaultdict import unittest from unittest import mock + +import pytest import voluptuous as vol -from collections import defaultdict -from homeassistant.components.media_player.const import ( - DOMAIN, - SUPPORT_TURN_ON, - SUPPORT_TURN_OFF, - SUPPORT_SELECT_SOURCE, -) -from homeassistant.const import STATE_ON, STATE_OFF - -import tests.common +from homeassistant.components.blackbird.const import DOMAIN, SERVICE_SETALLZONES from homeassistant.components.blackbird.media_player import ( DATA_BLACKBIRD, PLATFORM_SCHEMA, - SERVICE_SETALLZONES, setup_platform, ) -import pytest +from homeassistant.components.media_player.const import ( + SUPPORT_SELECT_SOURCE, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, +) +from homeassistant.const import STATE_OFF, STATE_ON + +import tests.common class AttrDict(dict): diff --git a/tests/components/bom/test_sensor.py b/tests/components/bom/test_sensor.py index 66d00e50796..6d452f7a6a3 100644 --- a/tests/components/bom/test_sensor.py +++ b/tests/components/bom/test_sensor.py @@ -10,6 +10,7 @@ import requests from homeassistant.components import sensor from homeassistant.components.bom.sensor import BOMCurrentData from homeassistant.setup import setup_component + from tests.common import assert_setup_component, get_test_home_assistant, load_fixture VALID_CONFIG = { diff --git a/tests/components/broadlink/test_init.py b/tests/components/broadlink/test_init.py index c15ef12125f..d4e3c993cd0 100644 --- a/tests/components/broadlink/test_init.py +++ b/tests/components/broadlink/test_init.py @@ -1,13 +1,13 @@ """The tests for the broadlink component.""" -from datetime import timedelta from base64 import b64decode -from unittest.mock import MagicMock, patch, call +from datetime import timedelta +from unittest.mock import MagicMock, call, patch import pytest -from homeassistant.util.dt import utcnow from homeassistant.components.broadlink import async_setup_service, data_packet from homeassistant.components.broadlink.const import DOMAIN, SERVICE_LEARN, SERVICE_SEND +from homeassistant.util.dt import utcnow DUMMY_IR_PACKET = ( "JgBGAJKVETkRORA6ERQRFBEUERQRFBE5ETkQOhAVEBUQFREUEBUQ" diff --git a/tests/components/buienradar/test_camera.py b/tests/components/buienradar/test_camera.py index dcbbb4ae076..6faac295d54 100644 --- a/tests/components/buienradar/test_camera.py +++ b/tests/components/buienradar/test_camera.py @@ -1,10 +1,10 @@ """The tests for generic camera component.""" import asyncio + from aiohttp.client_exceptions import ClientResponseError -from homeassistant.util import dt as dt_util - from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util # An infinitesimally small time-delta. EPSILON_DELTA = 0.0000000001 @@ -12,7 +12,7 @@ EPSILON_DELTA = 0.0000000001 def radar_map_url(dim: int = 512) -> str: """Build map url, defaulting to 512 wide (as in component).""" - return ("https://api.buienradar.nl/" "image/1.0/RadarMapNL?w={dim}&h={dim}").format( + return ("https://api.buienradar.nl/image/1.0/RadarMapNL?w={dim}&h={dim}").format( dim=dim ) diff --git a/tests/components/buienradar/test_sensor.py b/tests/components/buienradar/test_sensor.py index c1569e4576b..0c1cbd2a158 100644 --- a/tests/components/buienradar/test_sensor.py +++ b/tests/components/buienradar/test_sensor.py @@ -1,7 +1,6 @@ """The tests for the Buienradar sensor platform.""" -from homeassistant.setup import async_setup_component from homeassistant.components import sensor - +from homeassistant.setup import async_setup_component CONDITIONS = ["stationname", "temperature"] BASE_CONFIG = { diff --git a/tests/components/buienradar/test_weather.py b/tests/components/buienradar/test_weather.py index 1a8c94e1712..081616a1406 100644 --- a/tests/components/buienradar/test_weather.py +++ b/tests/components/buienradar/test_weather.py @@ -2,7 +2,6 @@ from homeassistant.components import weather from homeassistant.setup import async_setup_component - # Example config snippet from documentation. BASE_CONFIG = { "weather": [ diff --git a/tests/components/camera/common.py b/tests/components/camera/common.py index 9d5d2dcd6fd..8c05295b5d1 100644 --- a/tests/components/camera/common.py +++ b/tests/components/camera/common.py @@ -9,24 +9,29 @@ from homeassistant.components.camera import ( SERVICE_SNAPSHOT, ) from homeassistant.components.camera.const import ( - DOMAIN, DATA_CAMERA_PREFS, + DOMAIN, PREF_PRELOAD_STREAM, ) -from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON +from homeassistant.const import ( + ATTR_ENTITY_ID, + ENTITY_MATCH_ALL, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) from homeassistant.core import callback from homeassistant.loader import bind_hass @bind_hass -async def async_turn_off(hass, entity_id=None): +async def async_turn_off(hass, entity_id=ENTITY_MATCH_ALL): """Turn off camera.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} await hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data) @bind_hass -async def async_turn_on(hass, entity_id=None): +async def async_turn_on(hass, entity_id=ENTITY_MATCH_ALL): """Turn on camera, and set operation mode.""" data = {} if entity_id is not None: @@ -36,7 +41,7 @@ async def async_turn_on(hass, entity_id=None): @bind_hass -def enable_motion_detection(hass, entity_id=None): +def enable_motion_detection(hass, entity_id=ENTITY_MATCH_ALL): """Enable Motion Detection.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_ENABLE_MOTION, data)) @@ -44,7 +49,7 @@ def enable_motion_detection(hass, entity_id=None): @bind_hass @callback -def async_snapshot(hass, filename, entity_id=None): +def async_snapshot(hass, filename, entity_id=ENTITY_MATCH_ALL): """Make a snapshot from a camera.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} data[ATTR_FILENAME] = filename diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index 17bcaadb92b..de48a1d48f3 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -2,26 +2,26 @@ import asyncio import base64 import io -from unittest.mock import patch, mock_open, PropertyMock +from unittest.mock import PropertyMock, mock_open, patch import pytest -from homeassistant.setup import setup_component, async_setup_component +from homeassistant.components import camera, http +from homeassistant.components.camera.const import DOMAIN, PREF_PRELOAD_STREAM +from homeassistant.components.camera.prefs import CameraEntityPreferences +from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE, EVENT_HOMEASSISTANT_START, ) -from homeassistant.components import camera, http -from homeassistant.components.camera.const import DOMAIN, PREF_PRELOAD_STREAM -from homeassistant.components.camera.prefs import CameraEntityPreferences -from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.exceptions import HomeAssistantError +from homeassistant.setup import async_setup_component, setup_component from tests.common import ( + assert_setup_component, get_test_home_assistant, get_test_instance_port, - assert_setup_component, mock_coro, ) from tests.components.camera import common @@ -119,7 +119,7 @@ class TestGetImage: def test_get_image_without_exists_camera(self): """Try to get image without exists camera.""" with patch( - "homeassistant.helpers.entity_component.EntityComponent." "get_entity", + "homeassistant.helpers.entity_component.EntityComponent.get_entity", return_value=None, ), pytest.raises(HomeAssistantError): asyncio.run_coroutine_threadsafe( @@ -147,8 +147,7 @@ class TestGetImage: ).result() -@asyncio.coroutine -def test_snapshot_service(hass, mock_camera): +async def test_snapshot_service(hass, mock_camera): """Test snapshot service.""" mopen = mock_open() @@ -156,7 +155,7 @@ def test_snapshot_service(hass, mock_camera): "homeassistant.components.camera.open", mopen, create=True ), patch.object(hass.config, "is_allowed_path", return_value=True): common.async_snapshot(hass, "/tmp/bla") - yield from hass.async_block_till_done() + await hass.async_block_till_done() mock_write = mopen().write diff --git a/tests/components/canary/test_init.py b/tests/components/canary/test_init.py index cb671e30618..819d1ce0e90 100644 --- a/tests/components/canary/test_init.py +++ b/tests/components/canary/test_init.py @@ -1,9 +1,10 @@ """The tests for the Canary component.""" import unittest -from unittest.mock import patch, MagicMock, PropertyMock +from unittest.mock import MagicMock, PropertyMock, patch -import homeassistant.components.canary as canary from homeassistant import setup +import homeassistant.components.canary as canary + from tests.common import get_test_home_assistant diff --git a/tests/components/canary/test_sensor.py b/tests/components/canary/test_sensor.py index cfb8d46141a..7d5e829a347 100644 --- a/tests/components/canary/test_sensor.py +++ b/tests/components/canary/test_sensor.py @@ -3,16 +3,16 @@ import copy import unittest from unittest.mock import Mock -from homeassistant.components.canary import DATA_CANARY -from homeassistant.components.canary import sensor as canary +from homeassistant.components.canary import DATA_CANARY, sensor as canary from homeassistant.components.canary.sensor import ( - CanarySensor, - SENSOR_TYPES, ATTR_AIR_QUALITY, - STATE_AIR_QUALITY_NORMAL, + SENSOR_TYPES, STATE_AIR_QUALITY_ABNORMAL, + STATE_AIR_QUALITY_NORMAL, STATE_AIR_QUALITY_VERY_ABNORMAL, + CanarySensor, ) + from tests.common import get_test_home_assistant from tests.components.canary.test_init import mock_device, mock_location diff --git a/tests/components/cast/test_home_assistant_cast.py b/tests/components/cast/test_home_assistant_cast.py index 8db6fd4609e..10dd253704e 100644 --- a/tests/components/cast/test_home_assistant_cast.py +++ b/tests/components/cast/test_home_assistant_cast.py @@ -1,5 +1,6 @@ """Test Home Assistant Cast.""" from unittest.mock import Mock, patch + from homeassistant.components.cast import home_assistant_cast from tests.common import MockConfigEntry, async_mock_signal diff --git a/tests/components/cast/test_init.py b/tests/components/cast/test_init.py index 9062e521bef..6971c071353 100644 --- a/tests/components/cast/test_init.py +++ b/tests/components/cast/test_init.py @@ -2,8 +2,8 @@ from unittest.mock import patch from homeassistant import config_entries, data_entry_flow -from homeassistant.setup import async_setup_component from homeassistant.components import cast +from homeassistant.setup import async_setup_component from tests.common import MockDependency, mock_coro diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index 8f33709fb2d..93df75db8ba 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -1,19 +1,18 @@ """The tests for the Cast Media player platform.""" # pylint: disable=protected-access -import asyncio from typing import Optional -from unittest.mock import patch, MagicMock, Mock +from unittest.mock import MagicMock, Mock, patch from uuid import UUID import attr import pytest -from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.components.cast import media_player as cast from homeassistant.components.cast.media_player import ChromecastInfo from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.components.cast import media_player as cast +from homeassistant.helpers.typing import HomeAssistantType from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, mock_coro @@ -124,23 +123,21 @@ async def async_setup_media_player_cast(hass: HomeAssistantType, info: Chromecas return chromecast, entity -@asyncio.coroutine -def test_start_discovery_called_once(hass): +async def test_start_discovery_called_once(hass): """Test pychromecast.start_discovery called exactly once.""" with patch( "homeassistant.components.cast.discovery.pychromecast.start_discovery", return_value=(None, None), ) as start_discovery: - yield from async_setup_cast(hass) + await async_setup_cast(hass) assert start_discovery.call_count == 1 - yield from async_setup_cast(hass) + await async_setup_cast(hass) assert start_discovery.call_count == 1 -@asyncio.coroutine -def test_stop_discovery_called_on_stop(hass): +async def test_stop_discovery_called_on_stop(hass): """Test pychromecast.stop_discovery called on shutdown.""" browser = MagicMock(zc={}) @@ -149,7 +146,7 @@ def test_stop_discovery_called_on_stop(hass): return_value=(None, browser), ) as start_discovery: # start_discovery should be called with empty config - yield from async_setup_cast(hass, {}) + await async_setup_cast(hass, {}) assert start_discovery.call_count == 1 @@ -158,7 +155,7 @@ def test_stop_discovery_called_on_stop(hass): ) as stop_discovery: # stop discovery should be called on shutdown hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) - yield from hass.async_block_till_done() + await hass.async_block_till_done() stop_discovery.assert_called_once_with(browser) @@ -167,7 +164,7 @@ def test_stop_discovery_called_on_stop(hass): return_value=(None, browser), ) as start_discovery: # start_discovery should be called again on re-startup - yield from async_setup_cast(hass) + await async_setup_cast(hass) assert start_discovery.call_count == 1 diff --git a/tests/components/cert_expiry/test_config_flow.py b/tests/components/cert_expiry/test_config_flow.py index 3754551c230..bcd1482195d 100644 --- a/tests/components/cert_expiry/test_config_flow.py +++ b/tests/components/cert_expiry/test_config_flow.py @@ -1,13 +1,14 @@ """Tests for the Cert Expiry config flow.""" -import pytest -import ssl import socket +import ssl from unittest.mock import patch +import pytest + from homeassistant import data_entry_flow from homeassistant.components.cert_expiry import config_flow from homeassistant.components.cert_expiry.const import DEFAULT_NAME, DEFAULT_PORT -from homeassistant.const import CONF_PORT, CONF_NAME, CONF_HOST +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT from tests.common import MockConfigEntry, mock_coro diff --git a/tests/components/climate/common.py b/tests/components/climate/common.py index 9f1ef8b084a..5b75dc98e69 100644 --- a/tests/components/climate/common.py +++ b/tests/components/climate/common.py @@ -25,13 +25,14 @@ from homeassistant.components.climate.const import ( from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_TEMPERATURE, + ENTITY_MATCH_ALL, SERVICE_TURN_OFF, SERVICE_TURN_ON, ) from homeassistant.loader import bind_hass -async def async_set_preset_mode(hass, preset_mode, entity_id=None): +async def async_set_preset_mode(hass, preset_mode, entity_id=ENTITY_MATCH_ALL): """Set new preset mode.""" data = {ATTR_PRESET_MODE: preset_mode} @@ -42,7 +43,7 @@ async def async_set_preset_mode(hass, preset_mode, entity_id=None): @bind_hass -def set_preset_mode(hass, preset_mode, entity_id=None): +def set_preset_mode(hass, preset_mode, entity_id=ENTITY_MATCH_ALL): """Set new preset mode.""" data = {ATTR_PRESET_MODE: preset_mode} @@ -52,7 +53,7 @@ def set_preset_mode(hass, preset_mode, entity_id=None): hass.services.call(DOMAIN, SERVICE_SET_PRESET_MODE, data) -async def async_set_aux_heat(hass, aux_heat, entity_id=None): +async def async_set_aux_heat(hass, aux_heat, entity_id=ENTITY_MATCH_ALL): """Turn all or specified climate devices auxiliary heater on.""" data = {ATTR_AUX_HEAT: aux_heat} @@ -63,7 +64,7 @@ async def async_set_aux_heat(hass, aux_heat, entity_id=None): @bind_hass -def set_aux_heat(hass, aux_heat, entity_id=None): +def set_aux_heat(hass, aux_heat, entity_id=ENTITY_MATCH_ALL): """Turn all or specified climate devices auxiliary heater on.""" data = {ATTR_AUX_HEAT: aux_heat} @@ -76,7 +77,7 @@ def set_aux_heat(hass, aux_heat, entity_id=None): async def async_set_temperature( hass, temperature=None, - entity_id=None, + entity_id=ENTITY_MATCH_ALL, target_temp_high=None, target_temp_low=None, hvac_mode=None, @@ -103,7 +104,7 @@ async def async_set_temperature( def set_temperature( hass, temperature=None, - entity_id=None, + entity_id=ENTITY_MATCH_ALL, target_temp_high=None, target_temp_low=None, hvac_mode=None, @@ -124,7 +125,7 @@ def set_temperature( hass.services.call(DOMAIN, SERVICE_SET_TEMPERATURE, kwargs) -async def async_set_humidity(hass, humidity, entity_id=None): +async def async_set_humidity(hass, humidity, entity_id=ENTITY_MATCH_ALL): """Set new target humidity.""" data = {ATTR_HUMIDITY: humidity} @@ -135,7 +136,7 @@ async def async_set_humidity(hass, humidity, entity_id=None): @bind_hass -def set_humidity(hass, humidity, entity_id=None): +def set_humidity(hass, humidity, entity_id=ENTITY_MATCH_ALL): """Set new target humidity.""" data = {ATTR_HUMIDITY: humidity} @@ -145,7 +146,7 @@ def set_humidity(hass, humidity, entity_id=None): hass.services.call(DOMAIN, SERVICE_SET_HUMIDITY, data) -async def async_set_fan_mode(hass, fan, entity_id=None): +async def async_set_fan_mode(hass, fan, entity_id=ENTITY_MATCH_ALL): """Set all or specified climate devices fan mode on.""" data = {ATTR_FAN_MODE: fan} @@ -156,7 +157,7 @@ async def async_set_fan_mode(hass, fan, entity_id=None): @bind_hass -def set_fan_mode(hass, fan, entity_id=None): +def set_fan_mode(hass, fan, entity_id=ENTITY_MATCH_ALL): """Set all or specified climate devices fan mode on.""" data = {ATTR_FAN_MODE: fan} @@ -166,7 +167,7 @@ def set_fan_mode(hass, fan, entity_id=None): hass.services.call(DOMAIN, SERVICE_SET_FAN_MODE, data) -async def async_set_hvac_mode(hass, hvac_mode, entity_id=None): +async def async_set_hvac_mode(hass, hvac_mode, entity_id=ENTITY_MATCH_ALL): """Set new target operation mode.""" data = {ATTR_HVAC_MODE: hvac_mode} @@ -177,7 +178,7 @@ async def async_set_hvac_mode(hass, hvac_mode, entity_id=None): @bind_hass -def set_operation_mode(hass, hvac_mode, entity_id=None): +def set_operation_mode(hass, hvac_mode, entity_id=ENTITY_MATCH_ALL): """Set new target operation mode.""" data = {ATTR_HVAC_MODE: hvac_mode} @@ -187,7 +188,7 @@ def set_operation_mode(hass, hvac_mode, entity_id=None): hass.services.call(DOMAIN, SERVICE_SET_HVAC_MODE, data) -async def async_set_swing_mode(hass, swing_mode, entity_id=None): +async def async_set_swing_mode(hass, swing_mode, entity_id=ENTITY_MATCH_ALL): """Set new target swing mode.""" data = {ATTR_SWING_MODE: swing_mode} @@ -198,7 +199,7 @@ async def async_set_swing_mode(hass, swing_mode, entity_id=None): @bind_hass -def set_swing_mode(hass, swing_mode, entity_id=None): +def set_swing_mode(hass, swing_mode, entity_id=ENTITY_MATCH_ALL): """Set new target swing mode.""" data = {ATTR_SWING_MODE: swing_mode} @@ -208,7 +209,7 @@ def set_swing_mode(hass, swing_mode, entity_id=None): hass.services.call(DOMAIN, SERVICE_SET_SWING_MODE, data) -async def async_turn_on(hass, entity_id=None): +async def async_turn_on(hass, entity_id=ENTITY_MATCH_ALL): """Turn on device.""" data = {} @@ -218,7 +219,7 @@ async def async_turn_on(hass, entity_id=None): await hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data, blocking=True) -async def async_turn_off(hass, entity_id=None): +async def async_turn_off(hass, entity_id=ENTITY_MATCH_ALL): """Turn off device.""" data = {} diff --git a/tests/components/climate/test_device_action.py b/tests/components/climate/test_device_action.py index 3eb1f38ec41..ff78b837591 100644 --- a/tests/components/climate/test_device_action.py +++ b/tests/components/climate/test_device_action.py @@ -2,18 +2,18 @@ import pytest import voluptuous_serialize -from homeassistant.components.climate import DOMAIN, const, device_action -from homeassistant.setup import async_setup_component import homeassistant.components.automation as automation -from homeassistant.helpers import device_registry, config_validation as cv +from homeassistant.components.climate import DOMAIN, const, device_action +from homeassistant.helpers import config_validation as cv, device_registry +from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, assert_lists_same, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, - async_get_device_automations, ) @@ -39,6 +39,7 @@ async def test_get_actions(hass, device_reg, entity_reg): ) entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) hass.states.async_set("climate.test_5678", const.HVAC_MODE_COOL, {}) + hass.states.async_set("climate.test_5678", "attributes", {"supported_features": 17}) expected_actions = [ { "domain": DOMAIN, @@ -57,6 +58,29 @@ async def test_get_actions(hass, device_reg, entity_reg): assert_lists_same(actions, expected_actions) +async def test_get_action_hvac_only(hass, device_reg, entity_reg): + """Test we get the expected actions from a climate.""" + 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("climate.test_5678", const.HVAC_MODE_COOL, {}) + hass.states.async_set("climate.test_5678", "attributes", {"supported_features": 1}) + expected_actions = [ + { + "domain": DOMAIN, + "type": "set_hvac_mode", + "device_id": device_entry.id, + "entity_id": "climate.test_5678", + }, + ] + actions = await async_get_device_automations(hass, "action", device_entry.id) + assert_lists_same(actions, expected_actions) + + async def test_action(hass): """Test for actions.""" hass.states.async_set( diff --git a/tests/components/climate/test_device_condition.py b/tests/components/climate/test_device_condition.py index 82b6f595fb0..c8aaf0e1967 100644 --- a/tests/components/climate/test_device_condition.py +++ b/tests/components/climate/test_device_condition.py @@ -2,18 +2,18 @@ import pytest import voluptuous_serialize -from homeassistant.components.climate import DOMAIN, const, device_condition -from homeassistant.setup import async_setup_component import homeassistant.components.automation as automation -from homeassistant.helpers import device_registry, config_validation as cv +from homeassistant.components.climate import DOMAIN, const, device_condition +from homeassistant.helpers import config_validation as cv, device_registry +from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, assert_lists_same, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, - async_get_device_automations, ) @@ -53,6 +53,7 @@ async def test_get_conditions(hass, device_reg, entity_reg): const.ATTR_PRESET_MODES: [const.PRESET_HOME, const.PRESET_AWAY], }, ) + hass.states.async_set("climate.test_5678", "attributes", {"supported_features": 17}) expected_conditions = [ { "condition": "device", @@ -73,6 +74,38 @@ async def test_get_conditions(hass, device_reg, entity_reg): assert_lists_same(conditions, expected_conditions) +async def test_get_conditions_hvac_only(hass, device_reg, entity_reg): + """Test we get the expected conditions from a climate.""" + 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( + f"{DOMAIN}.test_5678", + const.HVAC_MODE_COOL, + { + const.ATTR_HVAC_MODE: const.HVAC_MODE_COOL, + const.ATTR_PRESET_MODE: const.PRESET_AWAY, + const.ATTR_PRESET_MODES: [const.PRESET_HOME, const.PRESET_AWAY], + }, + ) + hass.states.async_set("climate.test_5678", "attributes", {"supported_features": 1}) + expected_conditions = [ + { + "condition": "device", + "domain": DOMAIN, + "type": "is_hvac_mode", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + } + ] + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + assert_lists_same(conditions, expected_conditions) + + async def test_if_state(hass, calls): """Test for turn_on and turn_off conditions.""" hass.states.async_set( diff --git a/tests/components/climate/test_device_trigger.py b/tests/components/climate/test_device_trigger.py index 3b497912c52..d9bfd6d5ba4 100644 --- a/tests/components/climate/test_device_trigger.py +++ b/tests/components/climate/test_device_trigger.py @@ -1,19 +1,19 @@ """The tests for Climate device triggers.""" -import voluptuous_serialize import pytest +import voluptuous_serialize -from homeassistant.components.climate import DOMAIN, const, device_trigger -from homeassistant.setup import async_setup_component import homeassistant.components.automation as automation -from homeassistant.helpers import device_registry, config_validation as cv +from homeassistant.components.climate import DOMAIN, const, device_trigger +from homeassistant.helpers import config_validation as cv, device_registry +from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, assert_lists_same, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, - async_get_device_automations, ) diff --git a/tests/components/climate/test_init.py b/tests/components/climate/test_init.py index c9d9f1f6425..4345ecedcf7 100644 --- a/tests/components/climate/test_init.py +++ b/tests/components/climate/test_init.py @@ -1,10 +1,17 @@ """The tests for the climate component.""" +from typing import List from unittest.mock import MagicMock import pytest import voluptuous as vol -from homeassistant.components.climate import SET_TEMPERATURE_SCHEMA, ClimateDevice +from homeassistant.components.climate import ( + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + SET_TEMPERATURE_SCHEMA, + ClimateDevice, +) + from tests.common import async_mock_service @@ -38,9 +45,29 @@ async def test_set_temp_schema(hass, caplog): assert calls[-1].data == data +class MockClimateDevice(ClimateDevice): + """Mock Climate device to use in tests.""" + + @property + def hvac_mode(self) -> str: + """Return hvac operation ie. heat, cool mode. + + Need to be one of HVAC_MODE_*. + """ + return HVAC_MODE_HEAT + + @property + def hvac_modes(self) -> List[str]: + """Return the list of available hvac operation modes. + + Need to be a subset of HVAC_MODES. + """ + return [HVAC_MODE_OFF, HVAC_MODE_HEAT] + + async def test_sync_turn_on(hass): - """Test if adding turn_on work.""" - climate = ClimateDevice() + """Test if async turn_on calls sync turn_on.""" + climate = MockClimateDevice() climate.hass = hass climate.turn_on = MagicMock() @@ -50,8 +77,8 @@ async def test_sync_turn_on(hass): async def test_sync_turn_off(hass): - """Test if adding turn_on work.""" - climate = ClimateDevice() + """Test if async turn_off calls sync turn_off.""" + climate = MockClimateDevice() climate.hass = hass climate.turn_off = MagicMock() diff --git a/tests/components/climate/test_reproduce_state.py b/tests/components/climate/test_reproduce_state.py index fe995868840..df0b6314d63 100644 --- a/tests/components/climate/test_reproduce_state.py +++ b/tests/components/climate/test_reproduce_state.py @@ -2,7 +2,6 @@ import pytest -from homeassistant.components.climate.reproduce_state import async_reproduce_states from homeassistant.components.climate.const import ( ATTR_AUX_HEAT, ATTR_HUMIDITY, @@ -21,6 +20,7 @@ from homeassistant.components.climate.const import ( SERVICE_SET_SWING_MODE, SERVICE_SET_TEMPERATURE, ) +from homeassistant.components.climate.reproduce_state import async_reproduce_states from homeassistant.const import ATTR_TEMPERATURE from homeassistant.core import Context, State diff --git a/tests/components/cloud/__init__.py b/tests/components/cloud/__init__.py index 45ea4e43ee4..571b73e8d09 100644 --- a/tests/components/cloud/__init__.py +++ b/tests/components/cloud/__init__.py @@ -1,9 +1,9 @@ """Tests for the cloud component.""" from unittest.mock import patch -from homeassistant.setup import async_setup_component from homeassistant.components import cloud from homeassistant.components.cloud import const +from homeassistant.setup import async_setup_component from tests.common import mock_coro diff --git a/tests/components/cloud/test_account_link.py b/tests/components/cloud/test_account_link.py index 60116895beb..a8c247cc985 100644 --- a/tests/components/cloud/test_account_link.py +++ b/tests/components/cloud/test_account_link.py @@ -6,12 +6,12 @@ from unittest.mock import Mock, patch import pytest -from homeassistant import data_entry_flow, config_entries -from homeassistant.helpers import config_entry_oauth2_flow +from homeassistant import config_entries, data_entry_flow from homeassistant.components.cloud import account_link +from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.util.dt import utcnow -from tests.common import mock_coro, async_fire_time_changed, mock_platform +from tests.common import async_fire_time_changed, mock_coro, mock_platform TEST_DOMAIN = "oauth2_test" diff --git a/tests/components/cloud/test_alexa_config.py b/tests/components/cloud/test_alexa_config.py index a7c8898659a..508626b43f0 100644 --- a/tests/components/cloud/test_alexa_config.py +++ b/tests/components/cloud/test_alexa_config.py @@ -1,11 +1,12 @@ """Test Alexa config.""" import contextlib -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch from homeassistant.components.cloud import ALEXA_SCHEMA, alexa_config -from homeassistant.util.dt import utcnow from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED -from tests.common import mock_coro, async_fire_time_changed +from homeassistant.util.dt import utcnow + +from tests.common import async_fire_time_changed, mock_coro async def test_alexa_config_expose_entity_prefs(hass, cloud_prefs): diff --git a/tests/components/cloud/test_binary_sensor.py b/tests/components/cloud/test_binary_sensor.py index 99ae2f43bc5..24b0563890b 100644 --- a/tests/components/cloud/test_binary_sensor.py +++ b/tests/components/cloud/test_binary_sensor.py @@ -1,8 +1,8 @@ """Tests for the cloud binary sensor.""" from unittest.mock import Mock -from homeassistant.setup import async_setup_component from homeassistant.components.cloud.const import DISPATCHER_REMOTE_UPDATE +from homeassistant.setup import async_setup_component async def test_remote_connection_sensor(hass): diff --git a/tests/components/cloud/test_client.py b/tests/components/cloud/test_client.py index 054b38daffc..b3bfebb0ee7 100644 --- a/tests/components/cloud/test_client.py +++ b/tests/components/cloud/test_client.py @@ -1,17 +1,19 @@ """Test the cloud.iot module.""" -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch from aiohttp import web import pytest +from homeassistant.components.cloud import DOMAIN +from homeassistant.components.cloud.client import CloudClient +from homeassistant.components.cloud.const import PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE from homeassistant.core import State from homeassistant.setup import async_setup_component -from homeassistant.components.cloud import DOMAIN -from homeassistant.components.cloud.const import PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE -from tests.components.alexa import test_smart_home as test_alexa -from tests.common import mock_coro -from . import mock_cloud_prefs, mock_cloud +from . import mock_cloud, mock_cloud_prefs + +from tests.common import mock_coro +from tests.components.alexa import test_smart_home as test_alexa @pytest.fixture @@ -101,16 +103,13 @@ async def test_handler_google_actions(hass): reqid = "5711642932632160983" data = {"requestId": reqid, "inputs": [{"intent": "action.devices.SYNC"}]} - with patch( - "hass_nabucasa.Cloud._decode_claims", - return_value={"cognito:username": "myUserName"}, - ): - resp = await cloud.client.async_google_message(data) + config = await cloud.client.get_google_config() + resp = await cloud.client.async_google_message(data) assert resp["requestId"] == reqid payload = resp["payload"] - assert payload["agentUserId"] == "myUserName" + assert payload["agentUserId"] == config.cloud_user devices = payload["devices"] assert len(devices) == 1 @@ -187,25 +186,42 @@ async def test_google_config_expose_entity(hass, mock_cloud_setup, mock_cloud_lo """Test Google config exposing entity method uses latest config.""" cloud_client = hass.data[DOMAIN].client state = State("light.kitchen", "on") + gconf = await cloud_client.get_google_config() - assert cloud_client.google_config.should_expose(state) + assert gconf.should_expose(state) await cloud_client.prefs.async_update_google_entity_config( entity_id="light.kitchen", should_expose=False ) - assert not cloud_client.google_config.should_expose(state) + assert not gconf.should_expose(state) async def test_google_config_should_2fa(hass, mock_cloud_setup, mock_cloud_login): """Test Google config disabling 2FA method uses latest config.""" cloud_client = hass.data[DOMAIN].client + gconf = await cloud_client.get_google_config() state = State("light.kitchen", "on") - assert cloud_client.google_config.should_2fa(state) + assert gconf.should_2fa(state) await cloud_client.prefs.async_update_google_entity_config( entity_id="light.kitchen", disable_2fa=True ) - assert not cloud_client.google_config.should_2fa(state) + assert not gconf.should_2fa(state) + + +async def test_set_username(hass): + """Test we set username during loggin.""" + prefs = MagicMock( + alexa_enabled=False, + google_enabled=False, + async_set_username=MagicMock(return_value=mock_coro()), + ) + client = CloudClient(hass, prefs, None, {}, {}) + client.cloud = MagicMock(is_logged_in=True, username="mock-username") + await client.logged_in() + + assert len(prefs.async_set_username.mock_calls) == 1 + assert prefs.async_set_username.mock_calls[0][1][0] == "mock-username" diff --git a/tests/components/cloud/test_google_config.py b/tests/components/cloud/test_google_config.py index 43914f489d6..830751029d7 100644 --- a/tests/components/cloud/test_google_config.py +++ b/tests/components/cloud/test_google_config.py @@ -1,18 +1,26 @@ """Test the Cloud Google Config.""" -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch -from homeassistant.components.google_assistant import helpers as ga_helpers from homeassistant.components.cloud import GACTIONS_SCHEMA from homeassistant.components.cloud.google_config import CloudGoogleConfig -from homeassistant.util.dt import utcnow +from homeassistant.components.google_assistant import helpers as ga_helpers from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED +from homeassistant.util.dt import utcnow -from tests.common import mock_coro, async_fire_time_changed +from tests.common import async_fire_time_changed, mock_coro async def test_google_update_report_state(hass, cloud_prefs): """Test Google config responds to updating preference.""" - config = CloudGoogleConfig(hass, GACTIONS_SCHEMA({}), cloud_prefs, None) + config = CloudGoogleConfig( + hass, + GACTIONS_SCHEMA({}), + "mock-user-id", + cloud_prefs, + Mock(claims={"cognito:username": "abcdefghjkl"}), + ) + await config.async_initialize() + await config.async_connect_agent_user("mock-user-id") with patch.object( config, "async_sync_entities", side_effect=mock_coro @@ -32,6 +40,7 @@ async def test_sync_entities(aioclient_mock, hass, cloud_prefs): config = CloudGoogleConfig( hass, GACTIONS_SCHEMA({}), + "mock-user-id", cloud_prefs, Mock( google_actions_sync_url="http://example.com", @@ -39,12 +48,20 @@ async def test_sync_entities(aioclient_mock, hass, cloud_prefs): ), ) - assert await config.async_sync_entities() == 404 + assert await config.async_sync_entities("user") == 404 async def test_google_update_expose_trigger_sync(hass, cloud_prefs): """Test Google config responds to updating exposed entities.""" - config = CloudGoogleConfig(hass, GACTIONS_SCHEMA({}), cloud_prefs, None) + config = CloudGoogleConfig( + hass, + GACTIONS_SCHEMA({}), + "mock-user-id", + cloud_prefs, + Mock(claims={"cognito:username": "abcdefghjkl"}), + ) + await config.async_initialize() + await config.async_connect_agent_user("mock-user-id") with patch.object( config, "async_sync_entities", side_effect=mock_coro @@ -80,8 +97,10 @@ async def test_google_update_expose_trigger_sync(hass, cloud_prefs): async def test_google_entity_registry_sync(hass, mock_cloud_login, cloud_prefs): """Test Google config responds to entity registry.""" config = CloudGoogleConfig( - hass, GACTIONS_SCHEMA({}), cloud_prefs, hass.data["cloud"] + hass, GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, hass.data["cloud"] ) + await config.async_initialize() + await config.async_connect_agent_user("mock-user-id") with patch.object( config, "async_sync_entities", side_effect=mock_coro diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 8d05f1a14c3..b82b2b5481e 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -1,26 +1,26 @@ """Tests for the HTTP API for the cloud component.""" import asyncio -from unittest.mock import patch, MagicMock from ipaddress import ip_network +from unittest.mock import MagicMock, patch -import pytest -from jose import jwt +from hass_nabucasa import thingtalk from hass_nabucasa.auth import Unauthenticated, UnknownError from hass_nabucasa.const import STATE_CONNECTED -from hass_nabucasa import thingtalk +from jose import jwt +import pytest -from homeassistant.core import State from homeassistant.auth.providers import trusted_networks as tn_auth +from homeassistant.components.alexa import errors as alexa_errors +from homeassistant.components.alexa.entities import LightCapabilities from homeassistant.components.cloud.const import DOMAIN, RequireRelink from homeassistant.components.google_assistant.helpers import GoogleEntity -from homeassistant.components.alexa.entities import LightCapabilities -from homeassistant.components.alexa import errors as alexa_errors +from homeassistant.core import State + +from . import mock_cloud, mock_cloud_prefs from tests.common import mock_coro from tests.components.google_assistant import MockConfig -from . import mock_cloud, mock_cloud_prefs - GOOGLE_ACTIONS_SYNC_URL = "https://api-test.hass.io/google_actions_sync" SUBSCRIPTION_INFO_URL = "https://api-test.hass.io/subscription_info" @@ -85,46 +85,36 @@ def mock_cognito(): yield mock_cog() -async def test_google_actions_sync(mock_cognito, cloud_client, aioclient_mock): +async def test_google_actions_sync( + mock_cognito, mock_cloud_login, cloud_client, aioclient_mock +): """Test syncing Google Actions.""" aioclient_mock.post(GOOGLE_ACTIONS_SYNC_URL) req = await cloud_client.post("/api/cloud/google_actions/sync") assert req.status == 200 -async def test_google_actions_sync_fails(mock_cognito, cloud_client, aioclient_mock): +async def test_google_actions_sync_fails( + mock_cognito, mock_cloud_login, cloud_client, aioclient_mock +): """Test syncing Google Actions gone bad.""" aioclient_mock.post(GOOGLE_ACTIONS_SYNC_URL, status=403) req = await cloud_client.post("/api/cloud/google_actions/sync") assert req.status == 403 -async def test_login_view(hass, cloud_client, mock_cognito): +async def test_login_view(hass, cloud_client): """Test logging in.""" - mock_cognito.id_token = jwt.encode( - {"email": "hello@home-assistant.io", "custom:sub-exp": "2018-01-03"}, "test" - ) - mock_cognito.access_token = "access_token" - mock_cognito.refresh_token = "refresh_token" + hass.data["cloud"] = MagicMock(login=MagicMock(return_value=mock_coro())) - with patch("hass_nabucasa.iot.CloudIoT.connect") as mock_connect, patch( - "hass_nabucasa.auth.CognitoAuth._authenticate", return_value=mock_cognito - ) as mock_auth: - req = await cloud_client.post( - "/api/cloud/login", json={"email": "my_username", "password": "my_password"} - ) + req = await cloud_client.post( + "/api/cloud/login", json={"email": "my_username", "password": "my_password"} + ) assert req.status == 200 result = await req.json() assert result == {"success": True} - assert len(mock_connect.mock_calls) == 1 - - assert len(mock_auth.mock_calls) == 1 - result_user, result_pass = mock_auth.mock_calls[0][1] - assert result_user == "my_username" - assert result_pass == "my_password" - async def test_login_view_random_exception(cloud_client): """Try logging in with invalid JSON.""" @@ -331,7 +321,7 @@ async def test_websocket_status( client = await hass_ws_client(hass) with patch.dict( - "homeassistant.components.google_assistant.const." "DOMAIN_TO_GOOGLE_TYPES", + "homeassistant.components.google_assistant.const.DOMAIN_TO_GOOGLE_TYPES", {"light": None}, clear=True, ), patch.dict( @@ -347,7 +337,6 @@ async def test_websocket_status( "cloud": "connected", "prefs": { "alexa_enabled": True, - "cloud_user": None, "cloudhooks": {}, "google_enabled": True, "google_entity_configs": {}, @@ -694,7 +683,7 @@ async def test_list_google_entities(hass, hass_ws_client, setup_api, mock_cloud_ hass, MockConfig(should_expose=lambda *_: False), State("light.kitchen", "on") ) with patch( - "homeassistant.components.google_assistant.helpers" ".async_get_entities", + "homeassistant.components.google_assistant.helpers.async_get_entities", return_value=[entity], ): await client.send_json({"id": 5, "type": "cloud/google_assistant/entities"}) @@ -790,7 +779,7 @@ async def test_list_alexa_entities(hass, hass_ws_client, setup_api, mock_cloud_l hass, MagicMock(entity_config={}), State("light.kitchen", "on") ) with patch( - "homeassistant.components.alexa.entities" ".async_get_entities", + "homeassistant.components.alexa.entities.async_get_entities", return_value=[entity], ): await client.send_json({"id": 5, "type": "cloud/alexa/entities"}) @@ -801,7 +790,7 @@ async def test_list_alexa_entities(hass, hass_ws_client, setup_api, mock_cloud_l assert response["result"][0] == { "entity_id": "light.kitchen", "display_categories": ["LIGHT"], - "interfaces": ["Alexa.PowerController", "Alexa.EndpointHealth"], + "interfaces": ["Alexa.PowerController", "Alexa.EndpointHealth", "Alexa"], } diff --git a/tests/components/cloud/test_init.py b/tests/components/cloud/test_init.py index e160ea8826a..5d0ba76f80b 100644 --- a/tests/components/cloud/test_init.py +++ b/tests/components/cloud/test_init.py @@ -3,14 +3,14 @@ from unittest.mock import patch import pytest -from homeassistant.core import Context -from homeassistant.exceptions import Unauthorized -from homeassistant.auth.const import GROUP_ID_ADMIN from homeassistant.components import cloud from homeassistant.components.cloud.const import DOMAIN from homeassistant.components.cloud.prefs import STORAGE_KEY from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP +from homeassistant.core import Context +from homeassistant.exceptions import Unauthorized from homeassistant.setup import async_setup_component + from tests.common import mock_coro @@ -142,68 +142,11 @@ async def test_setup_existing_cloud_user(hass, hass_storage): assert hass_storage[STORAGE_KEY]["data"]["cloud_user"] == user.id -async def test_setup_invalid_cloud_user(hass, hass_storage): - """Test setup with API push default data.""" - hass_storage[STORAGE_KEY] = {"version": 1, "data": {"cloud_user": "non-existing"}} - with patch("hass_nabucasa.Cloud.start", return_value=mock_coro()): - result = await async_setup_component( - hass, - "cloud", - { - "http": {}, - "cloud": { - cloud.CONF_MODE: cloud.MODE_DEV, - "cognito_client_id": "test-cognito_client_id", - "user_pool_id": "test-user_pool_id", - "region": "test-region", - "relayer": "test-relayer", - }, - }, - ) - assert result - - assert hass_storage[STORAGE_KEY]["data"]["cloud_user"] != "non-existing" - cloud_user = await hass.auth.async_get_user( - hass_storage[STORAGE_KEY]["data"]["cloud_user"] - ) - - assert cloud_user - assert cloud_user.groups[0].id == GROUP_ID_ADMIN - - -async def test_setup_setup_cloud_user(hass, hass_storage): - """Test setup with API push default data.""" - hass_storage[STORAGE_KEY] = {"version": 1, "data": {"cloud_user": None}} - with patch("hass_nabucasa.Cloud.start", return_value=mock_coro()): - result = await async_setup_component( - hass, - "cloud", - { - "http": {}, - "cloud": { - cloud.CONF_MODE: cloud.MODE_DEV, - "cognito_client_id": "test-cognito_client_id", - "user_pool_id": "test-user_pool_id", - "region": "test-region", - "relayer": "test-relayer", - }, - }, - ) - assert result - - cloud_user = await hass.auth.async_get_user( - hass_storage[STORAGE_KEY]["data"]["cloud_user"] - ) - - assert cloud_user - assert cloud_user.groups[0].id == GROUP_ID_ADMIN - - async def test_on_connect(hass, mock_cloud_fixture): """Test cloud on connect triggers.""" cl = hass.data["cloud"] - assert len(cl.iot._on_connect) == 4 + assert len(cl.iot._on_connect) == 3 assert len(hass.states.async_entity_ids("binary_sensor")) == 0 diff --git a/tests/components/cloud/test_prefs.py b/tests/components/cloud/test_prefs.py new file mode 100644 index 00000000000..d1b6f9ed867 --- /dev/null +++ b/tests/components/cloud/test_prefs.py @@ -0,0 +1,80 @@ +"""Test Cloud preferences.""" +from unittest.mock import patch + +from homeassistant.auth.const import GROUP_ID_ADMIN +from homeassistant.components.cloud.prefs import STORAGE_KEY, CloudPreferences + + +async def test_set_username(hass): + """Test we clear config if we set different username.""" + prefs = CloudPreferences(hass) + await prefs.async_initialize() + + assert prefs.google_enabled + + await prefs.async_update(google_enabled=False) + + assert not prefs.google_enabled + + await prefs.async_set_username("new-username") + + assert prefs.google_enabled + + +async def test_set_username_migration(hass): + """Test we not clear config if we had no username.""" + prefs = CloudPreferences(hass) + + with patch.object(prefs, "_empty_config", return_value=prefs._empty_config(None)): + await prefs.async_initialize() + + assert prefs.google_enabled + + await prefs.async_update(google_enabled=False) + + assert not prefs.google_enabled + + await prefs.async_set_username("new-username") + + assert not prefs.google_enabled + + +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"}} + + prefs = CloudPreferences(hass) + await prefs.async_initialize() + + cloud_user_id = await prefs.get_cloud_user() + + assert cloud_user_id != "non-existing" + + cloud_user = await hass.auth.async_get_user( + hass_storage[STORAGE_KEY]["data"]["cloud_user"] + ) + + assert cloud_user + assert cloud_user.groups[0].id == GROUP_ID_ADMIN + + +async def test_setup_remove_cloud_user(hass, hass_storage): + """Test creating and removing cloud user.""" + hass_storage[STORAGE_KEY] = {"version": 1, "data": {"cloud_user": None}} + + prefs = CloudPreferences(hass) + await prefs.async_initialize() + await prefs.async_set_username("user1") + + cloud_user = await hass.auth.async_get_user(await prefs.get_cloud_user()) + + assert cloud_user + assert cloud_user.groups[0].id == GROUP_ID_ADMIN + + await prefs.async_set_username("user2") + + cloud_user2 = await hass.auth.async_get_user(await prefs.get_cloud_user()) + + assert cloud_user2 + assert cloud_user2.groups[0].id == GROUP_ID_ADMIN + assert cloud_user2.id != cloud_user.id diff --git a/tests/components/coinmarketcap/test_sensor.py b/tests/components/coinmarketcap/test_sensor.py index d629bf14184..9d1e89fbc24 100644 --- a/tests/components/coinmarketcap/test_sensor.py +++ b/tests/components/coinmarketcap/test_sensor.py @@ -1,12 +1,12 @@ """Tests for the CoinMarketCap sensor platform.""" import json - import unittest from unittest.mock import patch import homeassistant.components.sensor as sensor from homeassistant.setup import setup_component -from tests.common import get_test_home_assistant, load_fixture, assert_setup_component + +from tests.common import assert_setup_component, get_test_home_assistant, load_fixture VALID_CONFIG = { "platform": "coinmarketcap", diff --git a/tests/components/command_line/test_binary_sensor.py b/tests/components/command_line/test_binary_sensor.py index 333404cf904..33c28b7d65a 100644 --- a/tests/components/command_line/test_binary_sensor.py +++ b/tests/components/command_line/test_binary_sensor.py @@ -1,8 +1,8 @@ """The tests for the Command line Binary sensor platform.""" import unittest -from homeassistant.const import STATE_ON, STATE_OFF from homeassistant.components.command_line import binary_sensor as command_line +from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.helpers import template from tests.common import get_test_home_assistant diff --git a/tests/components/command_line/test_cover.py b/tests/components/command_line/test_cover.py index 05b77a1c85d..662ab0c969c 100644 --- a/tests/components/command_line/test_cover.py +++ b/tests/components/command_line/test_cover.py @@ -5,8 +5,8 @@ from unittest import mock import pytest -from homeassistant.components.cover import DOMAIN import homeassistant.components.command_line.cover as cmd_rs +from homeassistant.components.cover import DOMAIN from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_CLOSE_COVER, diff --git a/tests/components/command_line/test_notify.py b/tests/components/command_line/test_notify.py index 964a053f403..8bdc4ba0a01 100644 --- a/tests/components/command_line/test_notify.py +++ b/tests/components/command_line/test_notify.py @@ -4,8 +4,9 @@ import tempfile import unittest from unittest.mock import patch -from homeassistant.setup import setup_component import homeassistant.components.notify as notify +from homeassistant.setup import setup_component + from tests.common import assert_setup_component, get_test_home_assistant diff --git a/tests/components/command_line/test_sensor.py b/tests/components/command_line/test_sensor.py index 58a170cb37a..e51c0187460 100644 --- a/tests/components/command_line/test_sensor.py +++ b/tests/components/command_line/test_sensor.py @@ -2,8 +2,9 @@ import unittest from unittest.mock import patch -from homeassistant.helpers.template import Template from homeassistant.components.command_line import sensor as command_line +from homeassistant.helpers.template import Template + from tests.common import get_test_home_assistant diff --git a/tests/components/command_line/test_switch.py b/tests/components/command_line/test_switch.py index ef010081958..497fb0c2523 100644 --- a/tests/components/command_line/test_switch.py +++ b/tests/components/command_line/test_switch.py @@ -4,10 +4,10 @@ import os import tempfile import unittest -from homeassistant.setup import setup_component -from homeassistant.const import STATE_ON, STATE_OFF -import homeassistant.components.switch as switch import homeassistant.components.command_line.switch as command_line +import homeassistant.components.switch as switch +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.setup import setup_component from tests.common import get_test_home_assistant from tests.components.switch import common diff --git a/tests/components/config/test_area_registry.py b/tests/components/config/test_area_registry.py index 0505597c6ff..f66e16e606f 100644 --- a/tests/components/config/test_area_registry.py +++ b/tests/components/config/test_area_registry.py @@ -2,6 +2,7 @@ import pytest from homeassistant.components.config import area_registry + from tests.common import mock_area_registry diff --git a/tests/components/config/test_auth.py b/tests/components/config/test_auth.py index 4d90f345657..b07df39a8fe 100644 --- a/tests/components/config/test_auth.py +++ b/tests/components/config/test_auth.py @@ -4,7 +4,7 @@ import pytest from homeassistant.auth import models as auth_models from homeassistant.components.config import auth as auth_config -from tests.common import MockGroup, MockUser, CLIENT_ID +from tests.common import CLIENT_ID, MockGroup, MockUser @pytest.fixture(autouse=True) diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 3d22d3ac1a7..ccd41eeb3a5 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -1,6 +1,5 @@ """Test config entries API.""" -import asyncio from collections import OrderedDict from unittest.mock import patch @@ -8,18 +7,18 @@ import pytest import voluptuous as vol from homeassistant import config_entries as core_ce, data_entry_flow +from homeassistant.components.config import config_entries from homeassistant.config_entries import HANDLERS from homeassistant.core import callback -from homeassistant.setup import async_setup_component -from homeassistant.components.config import config_entries from homeassistant.generated import config_flows +from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, MockModule, mock_coro_func, - mock_integration, mock_entity_platform, + mock_integration, ) @@ -94,16 +93,15 @@ async def test_get_entries(hass, client): ] -@asyncio.coroutine -def test_remove_entry(hass, client): +async def test_remove_entry(hass, client): """Test removing an entry via the API.""" entry = MockConfigEntry(domain="demo", state=core_ce.ENTRY_STATE_LOADED) entry.add_to_hass(hass) - resp = yield from client.delete( + resp = await client.delete( "/api/config/config_entries/entry/{}".format(entry.entry_id) ) assert resp.status == 200 - data = yield from resp.json() + data = await resp.json() assert data == {"require_restart": True} assert len(hass.config_entries.async_entries()) == 0 @@ -120,13 +118,12 @@ async def test_remove_entry_unauth(hass, client, hass_admin_user): assert len(hass.config_entries.async_entries()) == 1 -@asyncio.coroutine -def test_available_flows(hass, client): +async def test_available_flows(hass, client): """Test querying the available flows.""" with patch.object(config_flows, "FLOWS", ["hello", "world"]): - resp = yield from client.get("/api/config/config_entries/flow_handlers") + resp = await client.get("/api/config/config_entries/flow_handlers") assert resp.status == 200 - data = yield from resp.json() + data = await resp.json() assert set(data) == set(["hello", "world"]) @@ -135,14 +132,12 @@ def test_available_flows(hass, client): ############################ -@asyncio.coroutine -def test_initialize_flow(hass, client): +async def test_initialize_flow(hass, client): """Test we can initialize a flow.""" mock_entity_platform(hass, "config_flow.test", None) class TestFlow(core_ce.ConfigFlow): - @asyncio.coroutine - def async_step_user(self, user_input=None): + async def async_step_user(self, user_input=None): schema = OrderedDict() schema[vol.Required("username")] = str schema[vol.Required("password")] = str @@ -155,12 +150,12 @@ def test_initialize_flow(hass, client): ) with patch.dict(HANDLERS, {"test": TestFlow}): - resp = yield from client.post( + resp = await client.post( "/api/config/config_entries/flow", json={"handler": "test"} ) assert resp.status == 200 - data = yield from resp.json() + data = await resp.json() data.pop("flow_id") @@ -182,8 +177,7 @@ async def test_initialize_flow_unauth(hass, client, hass_admin_user): hass_admin_user.groups = [] class TestFlow(core_ce.ConfigFlow): - @asyncio.coroutine - def async_step_user(self, user_input=None): + async def async_step_user(self, user_input=None): schema = OrderedDict() schema[vol.Required("username")] = str schema[vol.Required("password")] = str @@ -203,23 +197,21 @@ async def test_initialize_flow_unauth(hass, client, hass_admin_user): assert resp.status == 401 -@asyncio.coroutine -def test_abort(hass, client): +async def test_abort(hass, client): """Test a flow that aborts.""" mock_entity_platform(hass, "config_flow.test", None) class TestFlow(core_ce.ConfigFlow): - @asyncio.coroutine - def async_step_user(self, user_input=None): + async def async_step_user(self, user_input=None): return self.async_abort(reason="bla") with patch.dict(HANDLERS, {"test": TestFlow}): - resp = yield from client.post( + resp = await client.post( "/api/config/config_entries/flow", json={"handler": "test"} ) assert resp.status == 200 - data = yield from resp.json() + data = await resp.json() data.pop("flow_id") assert data == { "description_placeholders": None, @@ -229,8 +221,7 @@ def test_abort(hass, client): } -@asyncio.coroutine -def test_create_account(hass, client): +async def test_create_account(hass, client): """Test a flow that creates an account.""" mock_entity_platform(hass, "config_flow.test", None) @@ -239,14 +230,13 @@ def test_create_account(hass, client): class TestFlow(core_ce.ConfigFlow): VERSION = 1 - @asyncio.coroutine - def async_step_user(self, user_input=None): + async def async_step_user(self, user_input=None): return self.async_create_entry( title="Test Entry", data={"secret": "account_token"} ) with patch.dict(HANDLERS, {"test": TestFlow}): - resp = yield from client.post( + resp = await client.post( "/api/config/config_entries/flow", json={"handler": "test"} ) @@ -255,7 +245,7 @@ def test_create_account(hass, client): entries = hass.config_entries.async_entries("test") assert len(entries) == 1 - data = yield from resp.json() + data = await resp.json() data.pop("flow_id") assert data == { "handler": "test", @@ -268,8 +258,7 @@ def test_create_account(hass, client): } -@asyncio.coroutine -def test_two_step_flow(hass, client): +async def test_two_step_flow(hass, client): """Test we can finish a two step flow.""" mock_integration(hass, MockModule("test", async_setup_entry=mock_coro_func(True))) mock_entity_platform(hass, "config_flow.test", None) @@ -277,24 +266,22 @@ def test_two_step_flow(hass, client): class TestFlow(core_ce.ConfigFlow): VERSION = 1 - @asyncio.coroutine - def async_step_user(self, user_input=None): + async def async_step_user(self, user_input=None): return self.async_show_form( step_id="account", data_schema=vol.Schema({"user_title": str}) ) - @asyncio.coroutine - def async_step_account(self, user_input=None): + async def async_step_account(self, user_input=None): return self.async_create_entry( title=user_input["user_title"], data={"secret": "account_token"} ) with patch.dict(HANDLERS, {"test": TestFlow}): - resp = yield from client.post( + resp = await client.post( "/api/config/config_entries/flow", json={"handler": "test"} ) assert resp.status == 200 - data = yield from resp.json() + data = await resp.json() flow_id = data.pop("flow_id") assert data == { "type": "form", @@ -306,7 +293,7 @@ def test_two_step_flow(hass, client): } with patch.dict(HANDLERS, {"test": TestFlow}): - resp = yield from client.post( + resp = await client.post( "/api/config/config_entries/flow/{}".format(flow_id), json={"user_title": "user-title"}, ) @@ -315,7 +302,7 @@ def test_two_step_flow(hass, client): entries = hass.config_entries.async_entries("test") assert len(entries) == 1 - data = yield from resp.json() + data = await resp.json() data.pop("flow_id") assert data == { "handler": "test", @@ -336,14 +323,12 @@ async def test_continue_flow_unauth(hass, client, hass_admin_user): class TestFlow(core_ce.ConfigFlow): VERSION = 1 - @asyncio.coroutine - def async_step_user(self, user_input=None): + async def async_step_user(self, user_input=None): return self.async_show_form( step_id="account", data_schema=vol.Schema({"user_title": str}) ) - @asyncio.coroutine - def async_step_account(self, user_input=None): + async def async_step_account(self, user_input=None): return self.async_create_entry( title=user_input["user_title"], data={"secret": "account_token"} ) @@ -415,14 +400,12 @@ async def test_get_progress_index_unauth(hass, hass_ws_client, hass_admin_user): assert response["error"]["code"] == "unauthorized" -@asyncio.coroutine -def test_get_progress_flow(hass, client): +async def test_get_progress_flow(hass, client): """Test we can query the API for same result as we get from init a flow.""" mock_entity_platform(hass, "config_flow.test", None) class TestFlow(core_ce.ConfigFlow): - @asyncio.coroutine - def async_step_user(self, user_input=None): + async def async_step_user(self, user_input=None): schema = OrderedDict() schema[vol.Required("username")] = str schema[vol.Required("password")] = str @@ -434,19 +417,19 @@ def test_get_progress_flow(hass, client): ) with patch.dict(HANDLERS, {"test": TestFlow}): - resp = yield from client.post( + resp = await client.post( "/api/config/config_entries/flow", json={"handler": "test"} ) assert resp.status == 200 - data = yield from resp.json() + data = await resp.json() - resp2 = yield from client.get( + resp2 = await client.get( "/api/config/config_entries/flow/{}".format(data["flow_id"]) ) assert resp2.status == 200 - data2 = yield from resp2.json() + data2 = await resp2.json() assert data == data2 @@ -634,3 +617,42 @@ async def test_update_system_options(hass, hass_ws_client): assert response["success"] assert response["result"]["disable_new_entities"] assert entry.system_options.disable_new_entities + + +async def test_ignore_flow(hass, hass_ws_client): + """Test we can ignore a flow.""" + assert await async_setup_component(hass, "config", {}) + mock_integration(hass, MockModule("test", async_setup_entry=mock_coro_func(True))) + mock_entity_platform(hass, "config_flow.test", None) + + class TestFlow(core_ce.ConfigFlow): + VERSION = 1 + + async def async_step_user(self, user_input=None): + await self.async_set_unique_id("mock-unique-id") + return self.async_show_form(step_id="account", data_schema=vol.Schema({})) + + ws_client = await hass_ws_client(hass) + + with patch.dict(HANDLERS, {"test": TestFlow}): + result = await hass.config_entries.flow.async_init( + "test", context={"source": "user"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + await ws_client.send_json( + { + "id": 5, + "type": "config_entries/ignore_flow", + "flow_id": result["flow_id"], + } + ) + response = await ws_client.receive_json() + + assert response["success"] + + assert len(hass.config_entries.flow.async_progress()) == 0 + + entry = hass.config_entries.async_entries("test")[0] + assert entry.source == "ignore" + assert entry.unique_id == "mock-unique-id" diff --git a/tests/components/config/test_core.py b/tests/components/config/test_core.py index 050190d8dbe..8caa0f3e6fb 100644 --- a/tests/components/config/test_core.py +++ b/tests/components/config/test_core.py @@ -8,6 +8,7 @@ from homeassistant.components import config from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.const import CONF_UNIT_SYSTEM, CONF_UNIT_SYSTEM_IMPERIAL from homeassistant.util import dt as dt_util, location + from tests.common import mock_coro ORIG_TIME_ZONE = dt_util.DEFAULT_TIME_ZONE diff --git a/tests/components/config/test_customize.py b/tests/components/config/test_customize.py index f6a678ce8f0..45c1f40d4ad 100644 --- a/tests/components/config/test_customize.py +++ b/tests/components/config/test_customize.py @@ -1,5 +1,4 @@ """Test Customize config panel.""" -import asyncio import json from unittest.mock import patch @@ -8,13 +7,12 @@ from homeassistant.components import config from homeassistant.config import DATA_CUSTOMIZE -@asyncio.coroutine -def test_get_entity(hass, hass_client): +async def test_get_entity(hass, hass_client): """Test getting entity.""" with patch.object(config, "SECTIONS", ["customize"]): - yield from async_setup_component(hass, "config", {}) + await async_setup_component(hass, "config", {}) - client = yield from hass_client() + client = await hass_client() def mock_read(path): """Mock reading data.""" @@ -22,21 +20,20 @@ def test_get_entity(hass, hass_client): hass.data[DATA_CUSTOMIZE] = {"hello.beer": {"cold": "beer"}} with patch("homeassistant.components.config._read", mock_read): - resp = yield from client.get("/api/config/customize/config/hello.beer") + resp = await client.get("/api/config/customize/config/hello.beer") assert resp.status == 200 - result = yield from resp.json() + result = await resp.json() assert result == {"local": {"free": "beer"}, "global": {"cold": "beer"}} -@asyncio.coroutine -def test_update_entity(hass, hass_client): +async def test_update_entity(hass, hass_client): """Test updating entity.""" with patch.object(config, "SECTIONS", ["customize"]): - yield from async_setup_component(hass, "config", {}) + await async_setup_component(hass, "config", {}) - client = yield from hass_client() + client = await hass_client() orig_data = { "hello.beer": {"ignored": True}, @@ -57,7 +54,7 @@ def test_update_entity(hass, hass_client): with patch("homeassistant.components.config._read", mock_read), patch( "homeassistant.components.config._write", mock_write ): - resp = yield from client.post( + resp = await client.post( "/api/config/customize/config/hello.world", data=json.dumps( {"name": "Beer", "entities": ["light.top", "light.bottom"]} @@ -65,7 +62,7 @@ def test_update_entity(hass, hass_client): ) assert resp.status == 200 - result = yield from resp.json() + result = await resp.json() assert result == {"result": "ok"} state = hass.states.get("hello.world") @@ -82,31 +79,27 @@ def test_update_entity(hass, hass_client): assert written[0] == orig_data -@asyncio.coroutine -def test_update_entity_invalid_key(hass, hass_client): +async def test_update_entity_invalid_key(hass, hass_client): """Test updating entity.""" with patch.object(config, "SECTIONS", ["customize"]): - yield from async_setup_component(hass, "config", {}) + await async_setup_component(hass, "config", {}) - client = yield from hass_client() + client = await hass_client() - resp = yield from client.post( + resp = await client.post( "/api/config/customize/config/not_entity", data=json.dumps({"name": "YO"}) ) assert resp.status == 400 -@asyncio.coroutine -def test_update_entity_invalid_json(hass, hass_client): +async def test_update_entity_invalid_json(hass, hass_client): """Test updating entity.""" with patch.object(config, "SECTIONS", ["customize"]): - yield from async_setup_component(hass, "config", {}) + await async_setup_component(hass, "config", {}) - client = yield from hass_client() + client = await hass_client() - resp = yield from client.post( - "/api/config/customize/config/hello.beer", data="not json" - ) + resp = await client.post("/api/config/customize/config/hello.beer", data="not json") assert resp.status == 400 diff --git a/tests/components/config/test_device_registry.py b/tests/components/config/test_device_registry.py index cd305e5b567..a3710d48b94 100644 --- a/tests/components/config/test_device_registry.py +++ b/tests/components/config/test_device_registry.py @@ -2,6 +2,7 @@ import pytest from homeassistant.components.config import device_registry + from tests.common import mock_device_registry diff --git a/tests/components/config/test_entity_registry.py b/tests/components/config/test_entity_registry.py index 9472d888254..133c88d9ceb 100644 --- a/tests/components/config/test_entity_registry.py +++ b/tests/components/config/test_entity_registry.py @@ -3,9 +3,10 @@ from collections import OrderedDict import pytest -from homeassistant.helpers.entity_registry import RegistryEntry from homeassistant.components.config import entity_registry -from tests.common import mock_registry, MockEntity, MockEntityPlatform +from homeassistant.helpers.entity_registry import RegistryEntry + +from tests.common import MockEntity, MockEntityPlatform, mock_registry @pytest.fixture diff --git a/tests/components/config/test_group.py b/tests/components/config/test_group.py index 3240dbe9c13..1b79f30a5b6 100644 --- a/tests/components/config/test_group.py +++ b/tests/components/config/test_group.py @@ -1,43 +1,39 @@ """Test Group config panel.""" -import asyncio import json -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch from homeassistant.bootstrap import async_setup_component from homeassistant.components import config - VIEW_NAME = "api:config:group:config" -@asyncio.coroutine -def test_get_device_config(hass, hass_client): +async def test_get_device_config(hass, hass_client): """Test getting device config.""" with patch.object(config, "SECTIONS", ["group"]): - yield from async_setup_component(hass, "config", {}) + await async_setup_component(hass, "config", {}) - client = yield from hass_client() + client = await hass_client() def mock_read(path): """Mock reading data.""" return {"hello.beer": {"free": "beer"}, "other.entity": {"do": "something"}} with patch("homeassistant.components.config._read", mock_read): - resp = yield from client.get("/api/config/group/config/hello.beer") + resp = await client.get("/api/config/group/config/hello.beer") assert resp.status == 200 - result = yield from resp.json() + result = await resp.json() assert result == {"free": "beer"} -@asyncio.coroutine -def test_update_device_config(hass, hass_client): +async def test_update_device_config(hass, hass_client): """Test updating device config.""" with patch.object(config, "SECTIONS", ["group"]): - yield from async_setup_component(hass, "config", {}) + await async_setup_component(hass, "config", {}) - client = yield from hass_client() + client = await hass_client() orig_data = { "hello.beer": {"ignored": True}, @@ -59,7 +55,7 @@ def test_update_device_config(hass, hass_client): with patch("homeassistant.components.config._read", mock_read), patch( "homeassistant.components.config._write", mock_write ), patch.object(hass.services, "async_call", mock_call): - resp = yield from client.post( + resp = await client.post( "/api/config/group/config/hello_beer", data=json.dumps( {"name": "Beer", "entities": ["light.top", "light.bottom"]} @@ -67,7 +63,7 @@ def test_update_device_config(hass, hass_client): ) assert resp.status == 200 - result = yield from resp.json() + result = await resp.json() assert result == {"result": "ok"} orig_data["hello_beer"]["name"] = "Beer" @@ -77,46 +73,41 @@ def test_update_device_config(hass, hass_client): mock_call.assert_called_once_with("group", "reload") -@asyncio.coroutine -def test_update_device_config_invalid_key(hass, hass_client): +async def test_update_device_config_invalid_key(hass, hass_client): """Test updating device config.""" with patch.object(config, "SECTIONS", ["group"]): - yield from async_setup_component(hass, "config", {}) + await async_setup_component(hass, "config", {}) - client = yield from hass_client() + client = await hass_client() - resp = yield from client.post( + resp = await client.post( "/api/config/group/config/not a slug", data=json.dumps({"name": "YO"}) ) assert resp.status == 400 -@asyncio.coroutine -def test_update_device_config_invalid_data(hass, hass_client): +async def test_update_device_config_invalid_data(hass, hass_client): """Test updating device config.""" with patch.object(config, "SECTIONS", ["group"]): - yield from async_setup_component(hass, "config", {}) + await async_setup_component(hass, "config", {}) - client = yield from hass_client() + client = await hass_client() - resp = yield from client.post( + resp = await client.post( "/api/config/group/config/hello_beer", data=json.dumps({"invalid_option": 2}) ) assert resp.status == 400 -@asyncio.coroutine -def test_update_device_config_invalid_json(hass, hass_client): +async def test_update_device_config_invalid_json(hass, hass_client): """Test updating device config.""" with patch.object(config, "SECTIONS", ["group"]): - yield from async_setup_component(hass, "config", {}) + await async_setup_component(hass, "config", {}) - client = yield from hass_client() + client = await hass_client() - resp = yield from client.post( - "/api/config/group/config/hello_beer", data="not json" - ) + resp = await client.post("/api/config/group/config/hello_beer", data="not json") assert resp.status == 400 diff --git a/tests/components/config/test_init.py b/tests/components/config/test_init.py index 8e5fab494eb..7f9b62d71f6 100644 --- a/tests/components/config/test_init.py +++ b/tests/components/config/test_init.py @@ -1,23 +1,20 @@ """Test config init.""" -import asyncio from unittest.mock import patch -from homeassistant.const import EVENT_COMPONENT_LOADED -from homeassistant.setup import async_setup_component, ATTR_COMPONENT from homeassistant.components import config +from homeassistant.const import EVENT_COMPONENT_LOADED +from homeassistant.setup import ATTR_COMPONENT, async_setup_component -from tests.common import mock_coro, mock_component +from tests.common import mock_component, mock_coro -@asyncio.coroutine -def test_config_setup(hass, loop): +async def test_config_setup(hass, loop): """Test it sets up hassbian.""" - yield from async_setup_component(hass, "config", {}) + await async_setup_component(hass, "config", {}) assert "config" in hass.config.components -@asyncio.coroutine -def test_load_on_demand_already_loaded(hass, aiohttp_client): +async def test_load_on_demand_already_loaded(hass, aiohttp_client): """Test getting suites.""" mock_component(hass, "zwave") @@ -26,25 +23,24 @@ def test_load_on_demand_already_loaded(hass, aiohttp_client): ), patch("homeassistant.components.config.zwave.async_setup") as stp: stp.return_value = mock_coro(True) - yield from async_setup_component(hass, "config", {}) + await async_setup_component(hass, "config", {}) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert stp.called -@asyncio.coroutine -def test_load_on_demand_on_load(hass, aiohttp_client): +async def test_load_on_demand_on_load(hass, aiohttp_client): """Test getting suites.""" with patch.object(config, "SECTIONS", []), patch.object( config, "ON_DEMAND", ["zwave"] ): - yield from async_setup_component(hass, "config", {}) + await async_setup_component(hass, "config", {}) assert "config.zwave" not in hass.config.components with patch("homeassistant.components.config.zwave.async_setup") as stp: stp.return_value = mock_coro(True) hass.bus.async_fire(EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: "zwave"}) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert stp.called diff --git a/tests/components/config/test_zwave.py b/tests/components/config/test_zwave.py index 9f62c3f9ac2..267c57717f9 100644 --- a/tests/components/config/test_zwave.py +++ b/tests/components/config/test_zwave.py @@ -1,5 +1,4 @@ """Test Z-Wave config panel.""" -import asyncio import json from unittest.mock import MagicMock, patch @@ -7,10 +6,9 @@ import pytest from homeassistant.bootstrap import async_setup_component from homeassistant.components import config - from homeassistant.components.zwave import DATA_NETWORK, const -from tests.mock.zwave import MockNode, MockValue, MockEntityValues +from tests.mock.zwave import MockEntityValues, MockNode, MockValue VIEW_NAME = "api:config:zwave:device_config" @@ -24,8 +22,7 @@ def client(loop, hass, hass_client): return loop.run_until_complete(hass_client()) -@asyncio.coroutine -def test_get_device_config(client): +async def test_get_device_config(client): """Test getting device config.""" def mock_read(path): @@ -33,16 +30,15 @@ def test_get_device_config(client): return {"hello.beer": {"free": "beer"}, "other.entity": {"do": "something"}} with patch("homeassistant.components.config._read", mock_read): - resp = yield from client.get("/api/config/zwave/device_config/hello.beer") + resp = await client.get("/api/config/zwave/device_config/hello.beer") assert resp.status == 200 - result = yield from resp.json() + result = await resp.json() assert result == {"free": "beer"} -@asyncio.coroutine -def test_update_device_config(client): +async def test_update_device_config(client): """Test updating device config.""" orig_data = { "hello.beer": {"ignored": True}, @@ -62,13 +58,13 @@ def test_update_device_config(client): with patch("homeassistant.components.config._read", mock_read), patch( "homeassistant.components.config._write", mock_write ): - resp = yield from client.post( + resp = await client.post( "/api/config/zwave/device_config/hello.beer", data=json.dumps({"polling_intensity": 2}), ) assert resp.status == 200 - result = yield from resp.json() + result = await resp.json() assert result == {"result": "ok"} orig_data["hello.beer"]["polling_intensity"] = 2 @@ -76,10 +72,9 @@ def test_update_device_config(client): assert written[0] == orig_data -@asyncio.coroutine -def test_update_device_config_invalid_key(client): +async def test_update_device_config_invalid_key(client): """Test updating device config.""" - resp = yield from client.post( + resp = await client.post( "/api/config/zwave/device_config/invalid_entity", data=json.dumps({"polling_intensity": 2}), ) @@ -87,10 +82,9 @@ def test_update_device_config_invalid_key(client): assert resp.status == 400 -@asyncio.coroutine -def test_update_device_config_invalid_data(client): +async def test_update_device_config_invalid_data(client): """Test updating device config.""" - resp = yield from client.post( + resp = await client.post( "/api/config/zwave/device_config/hello.beer", data=json.dumps({"invalid_option": 2}), ) @@ -98,18 +92,16 @@ def test_update_device_config_invalid_data(client): assert resp.status == 400 -@asyncio.coroutine -def test_update_device_config_invalid_json(client): +async def test_update_device_config_invalid_json(client): """Test updating device config.""" - resp = yield from client.post( + resp = await client.post( "/api/config/zwave/device_config/hello.beer", data="not json" ) assert resp.status == 400 -@asyncio.coroutine -def test_get_values(hass, client): +async def test_get_values(hass, client): """Test getting values on node.""" node = MockNode(node_id=1) value = MockValue( @@ -126,10 +118,10 @@ def test_get_values(hass, client): values2 = MockEntityValues(primary=value2) hass.data[const.DATA_ENTITY_VALUES] = [values, values2] - resp = yield from client.get("/api/zwave/values/1") + resp = await client.get("/api/zwave/values/1") assert resp.status == 200 - result = yield from resp.json() + result = await resp.json() assert result == { "123456": { @@ -141,8 +133,7 @@ def test_get_values(hass, client): } -@asyncio.coroutine -def test_get_groups(hass, client): +async def test_get_groups(hass, client): """Test getting groupdata on node.""" network = hass.data[DATA_NETWORK] = MagicMock() node = MockNode(node_id=2) @@ -153,10 +144,10 @@ def test_get_groups(hass, client): node.groups = {1: node.groups} network.nodes = {2: node} - resp = yield from client.get("/api/zwave/groups/2") + resp = await client.get("/api/zwave/groups/2") assert resp.status == 200 - result = yield from resp.json() + result = await resp.json() assert result == { "1": { @@ -168,38 +159,35 @@ def test_get_groups(hass, client): } -@asyncio.coroutine -def test_get_groups_nogroups(hass, client): +async def test_get_groups_nogroups(hass, client): """Test getting groupdata on node with no groups.""" network = hass.data[DATA_NETWORK] = MagicMock() node = MockNode(node_id=2) network.nodes = {2: node} - resp = yield from client.get("/api/zwave/groups/2") + resp = await client.get("/api/zwave/groups/2") assert resp.status == 200 - result = yield from resp.json() + result = await resp.json() assert result == {} -@asyncio.coroutine -def test_get_groups_nonode(hass, client): +async def test_get_groups_nonode(hass, client): """Test getting groupdata on nonexisting node.""" network = hass.data[DATA_NETWORK] = MagicMock() network.nodes = {1: 1, 5: 5} - resp = yield from client.get("/api/zwave/groups/2") + resp = await client.get("/api/zwave/groups/2") assert resp.status == 404 - result = yield from resp.json() + result = await resp.json() assert result == {"message": "Node not found"} -@asyncio.coroutine -def test_get_config(hass, client): +async def test_get_config(hass, client): """Test getting config on node.""" network = hass.data[DATA_NETWORK] = MagicMock() node = MockNode(node_id=2) @@ -215,10 +203,10 @@ def test_get_config(hass, client): network.nodes = {2: node} node.get_values.return_value = node.values - resp = yield from client.get("/api/zwave/config/2") + resp = await client.get("/api/zwave/config/2") assert resp.status == 200 - result = yield from resp.json() + result = await resp.json() assert result == { "12": { @@ -233,8 +221,7 @@ def test_get_config(hass, client): } -@asyncio.coroutine -def test_get_config_noconfig_node(hass, client): +async def test_get_config_noconfig_node(hass, client): """Test getting config on node without config.""" network = hass.data[DATA_NETWORK] = MagicMock() node = MockNode(node_id=2) @@ -242,44 +229,41 @@ def test_get_config_noconfig_node(hass, client): network.nodes = {2: node} node.get_values.return_value = node.values - resp = yield from client.get("/api/zwave/config/2") + resp = await client.get("/api/zwave/config/2") assert resp.status == 200 - result = yield from resp.json() + result = await resp.json() assert result == {} -@asyncio.coroutine -def test_get_config_nonode(hass, client): +async def test_get_config_nonode(hass, client): """Test getting config on nonexisting node.""" network = hass.data[DATA_NETWORK] = MagicMock() network.nodes = {1: 1, 5: 5} - resp = yield from client.get("/api/zwave/config/2") + resp = await client.get("/api/zwave/config/2") assert resp.status == 404 - result = yield from resp.json() + result = await resp.json() assert result == {"message": "Node not found"} -@asyncio.coroutine -def test_get_usercodes_nonode(hass, client): +async def test_get_usercodes_nonode(hass, client): """Test getting usercodes on nonexisting node.""" network = hass.data[DATA_NETWORK] = MagicMock() network.nodes = {1: 1, 5: 5} - resp = yield from client.get("/api/zwave/usercodes/2") + resp = await client.get("/api/zwave/usercodes/2") assert resp.status == 404 - result = yield from resp.json() + result = await resp.json() assert result == {"message": "Node not found"} -@asyncio.coroutine -def test_get_usercodes(hass, client): +async def test_get_usercodes(hass, client): """Test getting usercodes on node.""" network = hass.data[DATA_NETWORK] = MagicMock() node = MockNode(node_id=18, command_classes=[const.COMMAND_CLASS_USER_CODE]) @@ -291,16 +275,15 @@ def test_get_usercodes(hass, client): network.nodes = {18: node} node.get_values.return_value = node.values - resp = yield from client.get("/api/zwave/usercodes/18") + resp = await client.get("/api/zwave/usercodes/18") assert resp.status == 200 - result = yield from resp.json() + result = await resp.json() assert result == {"0": {"code": "1234", "label": "label", "length": 4}} -@asyncio.coroutine -def test_get_usercode_nousercode_node(hass, client): +async def test_get_usercode_nousercode_node(hass, client): """Test getting usercodes on node without usercodes.""" network = hass.data[DATA_NETWORK] = MagicMock() node = MockNode(node_id=18) @@ -308,16 +291,15 @@ def test_get_usercode_nousercode_node(hass, client): network.nodes = {18: node} node.get_values.return_value = node.values - resp = yield from client.get("/api/zwave/usercodes/18") + resp = await client.get("/api/zwave/usercodes/18") assert resp.status == 200 - result = yield from resp.json() + result = await resp.json() assert result == {} -@asyncio.coroutine -def test_get_usercodes_no_genreuser(hass, client): +async def test_get_usercodes_no_genreuser(hass, client): """Test getting usercodes on node missing genre user.""" network = hass.data[DATA_NETWORK] = MagicMock() node = MockNode(node_id=18, command_classes=[const.COMMAND_CLASS_USER_CODE]) @@ -329,33 +311,31 @@ def test_get_usercodes_no_genreuser(hass, client): network.nodes = {18: node} node.get_values.return_value = node.values - resp = yield from client.get("/api/zwave/usercodes/18") + resp = await client.get("/api/zwave/usercodes/18") assert resp.status == 200 - result = yield from resp.json() + result = await resp.json() assert result == {} -@asyncio.coroutine -def test_save_config_no_network(hass, client): +async def test_save_config_no_network(hass, client): """Test saving configuration without network data.""" - resp = yield from client.post("/api/zwave/saveconfig") + resp = await client.post("/api/zwave/saveconfig") assert resp.status == 404 - result = yield from resp.json() + result = await resp.json() assert result == {"message": "No Z-Wave network data found"} -@asyncio.coroutine -def test_save_config(hass, client): +async def test_save_config(hass, client): """Test saving configuration.""" network = hass.data[DATA_NETWORK] = MagicMock() - resp = yield from client.post("/api/zwave/saveconfig") + resp = await client.post("/api/zwave/saveconfig") assert resp.status == 200 - result = yield from resp.json() + result = await resp.json() assert network.write_config.called assert result == {"message": "Z-Wave configuration saved to file."} diff --git a/tests/components/configurator/test_init.py b/tests/components/configurator/test_init.py index 43395c0e7e8..b572609c5a2 100644 --- a/tests/components/configurator/test_init.py +++ b/tests/components/configurator/test_init.py @@ -3,7 +3,7 @@ import unittest import homeassistant.components.configurator as configurator -from homeassistant.const import EVENT_TIME_CHANGED, ATTR_FRIENDLY_NAME +from homeassistant.const import ATTR_FRIENDLY_NAME, EVENT_TIME_CHANGED from tests.common import get_test_home_assistant diff --git a/tests/components/conftest.py b/tests/components/conftest.py index 4f1f3e64e02..a589839c03f 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -3,13 +3,13 @@ from unittest.mock import patch import pytest -from homeassistant.setup import async_setup_component -from homeassistant.components.websocket_api.http import URL from homeassistant.components.websocket_api.auth import ( TYPE_AUTH, TYPE_AUTH_OK, TYPE_AUTH_REQUIRED, ) +from homeassistant.components.websocket_api.http import URL +from homeassistant.setup import async_setup_component from tests.common import mock_coro diff --git a/tests/components/conversation/test_init.py b/tests/components/conversation/test_init.py index ff44eaccc8e..f84d2109095 100644 --- a/tests/components/conversation/test_init.py +++ b/tests/components/conversation/test_init.py @@ -1,12 +1,10 @@ """The tests for the Conversation component.""" -# pylint: disable=protected-access import pytest -from homeassistant.core import DOMAIN as HASS_DOMAIN -from homeassistant.setup import async_setup_component from homeassistant.components import conversation -from homeassistant.components.cover import SERVICE_OPEN_COVER +from homeassistant.core import DOMAIN as HASS_DOMAIN, Context from homeassistant.helpers import intent +from homeassistant.setup import async_setup_component from tests.common import async_mock_intent, async_mock_service @@ -25,10 +23,13 @@ async def test_calling_intent(hass): ) assert result + context = Context() + await hass.services.async_call( "conversation", "process", {conversation.ATTR_TEXT: "I would like the Grolsch beer"}, + context=context, ) await hass.async_block_till_done() @@ -38,6 +39,7 @@ async def test_calling_intent(hass): assert intent.intent_type == "OrderBeer" assert intent.slots == {"type": {"value": "Grolsch"}} assert intent.text_input == "I would like the Grolsch beer" + assert intent.context is context async def test_register_before_setup(hass): @@ -80,7 +82,7 @@ async def test_register_before_setup(hass): assert intent.text_input == "I would like the Grolsch beer" -async def test_http_processing_intent(hass, hass_client): +async def test_http_processing_intent(hass, hass_client, hass_admin_user): """Test processing intent via HTTP API.""" class TestIntentHandler(intent.IntentHandler): @@ -90,6 +92,7 @@ async def test_http_processing_intent(hass, hass_client): async def async_handle(self, intent): """Handle the intent.""" + assert intent.context.user_id == hass_admin_user.id response = intent.create_response() response.async_set_speech( "I've ordered a {}!".format(intent.slots["type"]["value"]) @@ -148,32 +151,6 @@ async def test_turn_on_intent(hass, sentence): assert call.data == {"entity_id": "light.kitchen"} -async def test_cover_intents_loading(hass): - """Test Cover Intents Loading.""" - with pytest.raises(intent.UnknownIntent): - await intent.async_handle( - hass, "test", "HassOpenCover", {"name": {"value": "garage door"}} - ) - - result = await async_setup_component(hass, "cover", {}) - assert result - - hass.states.async_set("cover.garage_door", "closed") - calls = async_mock_service(hass, "cover", SERVICE_OPEN_COVER) - - response = await intent.async_handle( - hass, "test", "HassOpenCover", {"name": {"value": "garage door"}} - ) - await hass.async_block_till_done() - - assert response.speech["plain"]["speech"] == "Opened garage door" - assert len(calls) == 1 - call = calls[0] - assert call.domain == "cover" - assert call.service == "open_cover" - assert call.data == {"entity_id": "cover.garage_door"} - - @pytest.mark.parametrize("sentence", ("turn off kitchen", "turn kitchen off")) async def test_turn_off_intent(hass, sentence): """Test calling the turn on intent.""" @@ -263,7 +240,7 @@ async def test_http_api_wrong_data(hass, hass_client): assert resp.status == 400 -async def test_custom_agent(hass, hass_client): +async def test_custom_agent(hass, hass_client, hass_admin_user): """Test a custom conversation agent.""" calls = [] @@ -271,9 +248,9 @@ async def test_custom_agent(hass, hass_client): class MyAgent(conversation.AbstractConversationAgent): """Test Agent.""" - async def async_process(self, text, conversation_id): + async def async_process(self, text, context, conversation_id): """Process some text.""" - calls.append((text, conversation_id)) + calls.append((text, context, conversation_id)) response = intent.IntentResponse() response.async_set_speech("Test response") return response @@ -296,4 +273,5 @@ async def test_custom_agent(hass, hass_client): assert len(calls) == 1 assert calls[0][0] == "Test Text" - assert calls[0][1] == "test-conv-id" + assert calls[0][1].user_id == hass_admin_user.id + assert calls[0][2] == "test-conv-id" diff --git a/tests/components/coolmaster/test_config_flow.py b/tests/components/coolmaster/test_config_flow.py index d0126ff2cb6..c71f308dece 100644 --- a/tests/components/coolmaster/test_config_flow.py +++ b/tests/components/coolmaster/test_config_flow.py @@ -2,7 +2,7 @@ from unittest.mock import patch from homeassistant import config_entries, setup -from homeassistant.components.coolmaster.const import DOMAIN, AVAILABLE_MODES +from homeassistant.components.coolmaster.const import AVAILABLE_MODES, DOMAIN from tests.common import mock_coro diff --git a/tests/components/counter/common.py b/tests/components/counter/common.py index 0f735e52f9f..5f47e4faa77 100644 --- a/tests/components/counter/common.py +++ b/tests/components/counter/common.py @@ -3,13 +3,13 @@ All containing methods are legacy helpers that should not be used by new components. Instead call the service directly. """ -from homeassistant.const import ATTR_ENTITY_ID from homeassistant.components.counter import ( DOMAIN, SERVICE_DECREMENT, SERVICE_INCREMENT, SERVICE_RESET, ) +from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import callback from homeassistant.loader import bind_hass diff --git a/tests/components/counter/test_init.py b/tests/components/counter/test_init.py index 8ce90e164b6..3e85a080806 100644 --- a/tests/components/counter/test_init.py +++ b/tests/components/counter/test_init.py @@ -1,6 +1,5 @@ """The tests for the counter component.""" # pylint: disable=protected-access -import asyncio import logging from homeassistant.components.counter import ( @@ -14,6 +13,7 @@ from homeassistant.components.counter import ( from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_ICON from homeassistant.core import Context, CoreState, State from homeassistant.setup import async_setup_component + from tests.common import mock_restore_cache from tests.components.counter.common import ( async_decrement, @@ -142,8 +142,7 @@ async def test_methods_with_config(hass): assert 15 == int(state.state) -@asyncio.coroutine -def test_initial_state_overrules_restore_state(hass): +async def test_initial_state_overrules_restore_state(hass): """Ensure states are restored on startup.""" mock_restore_cache( hass, (State("counter.test1", "11"), State("counter.test2", "-22")) @@ -151,7 +150,7 @@ def test_initial_state_overrules_restore_state(hass): hass.state = CoreState.starting - yield from async_setup_component( + await async_setup_component( hass, DOMAIN, { @@ -171,8 +170,7 @@ def test_initial_state_overrules_restore_state(hass): assert int(state.state) == 10 -@asyncio.coroutine -def test_restore_state_overrules_initial_state(hass): +async def test_restore_state_overrules_initial_state(hass): """Ensure states are restored on startup.""" attr = {"initial": 6, "minimum": 1, "maximum": 8, "step": 2} @@ -188,7 +186,7 @@ def test_restore_state_overrules_initial_state(hass): hass.state = CoreState.starting - yield from async_setup_component( + await async_setup_component( hass, DOMAIN, {DOMAIN: {"test1": {}, "test2": {CONF_INITIAL: 10}, "test3": {}}} ) @@ -209,12 +207,11 @@ def test_restore_state_overrules_initial_state(hass): assert state.attributes.get("step") == 2 -@asyncio.coroutine -def test_no_initial_state_and_no_restore_state(hass): +async def test_no_initial_state_and_no_restore_state(hass): """Ensure that entity is create without initial and restore feature.""" hass.state = CoreState.starting - yield from async_setup_component(hass, DOMAIN, {DOMAIN: {"test1": {CONF_STEP: 5}}}) + await async_setup_component(hass, DOMAIN, {DOMAIN: {"test1": {CONF_STEP: 5}}}) state = hass.states.get("counter.test1") assert state diff --git a/tests/components/cover/test_device_condition.py b/tests/components/cover/test_device_condition.py index 8ca912b640b..13c6fd8701f 100644 --- a/tests/components/cover/test_device_condition.py +++ b/tests/components/cover/test_device_condition.py @@ -1,26 +1,26 @@ """The tests for Cover device conditions.""" import pytest +import homeassistant.components.automation as automation from homeassistant.components.cover import DOMAIN from homeassistant.const import ( CONF_PLATFORM, - STATE_OPEN, STATE_CLOSED, - STATE_OPENING, STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, ) -from homeassistant.setup import async_setup_component -import homeassistant.components.automation as automation from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, assert_lists_same, + async_get_device_automation_capabilities, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, - async_get_device_automations, - async_get_device_automation_capabilities, ) diff --git a/tests/components/cover/test_device_trigger.py b/tests/components/cover/test_device_trigger.py index 4f50c0639c0..3f82babc2ed 100644 --- a/tests/components/cover/test_device_trigger.py +++ b/tests/components/cover/test_device_trigger.py @@ -1,6 +1,7 @@ """The tests for Cover device triggers.""" import pytest +import homeassistant.components.automation as automation from homeassistant.components.cover import DOMAIN from homeassistant.const import ( CONF_PLATFORM, @@ -9,18 +10,17 @@ from homeassistant.const import ( STATE_OPEN, STATE_OPENING, ) -from homeassistant.setup import async_setup_component -import homeassistant.components.automation as automation from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, assert_lists_same, + async_get_device_automation_capabilities, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, - async_get_device_automations, - async_get_device_automation_capabilities, ) diff --git a/tests/components/cover/test_init.py b/tests/components/cover/test_intent.py similarity index 83% rename from tests/components/cover/test_init.py rename to tests/components/cover/test_intent.py index d1ca17d18f3..29d3378a0f9 100644 --- a/tests/components/cover/test_init.py +++ b/tests/components/cover/test_intent.py @@ -1,15 +1,18 @@ """The tests for the cover platform.""" -from homeassistant.components.cover import SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER +from homeassistant.components.cover import ( + SERVICE_CLOSE_COVER, + SERVICE_OPEN_COVER, + intent as cover_intent, +) from homeassistant.helpers import intent -import homeassistant.components as comps + from tests.common import async_mock_service async def test_open_cover_intent(hass): """Test HassOpenCover intent.""" - result = await comps.cover.async_setup(hass, {}) - assert result + await cover_intent.async_setup_intents(hass) hass.states.async_set("cover.garage_door", "closed") calls = async_mock_service(hass, "cover", SERVICE_OPEN_COVER) @@ -29,8 +32,7 @@ async def test_open_cover_intent(hass): async def test_close_cover_intent(hass): """Test HassCloseCover intent.""" - result = await comps.cover.async_setup(hass, {}) - assert result + await cover_intent.async_setup_intents(hass) hass.states.async_set("cover.garage_door", "open") calls = async_mock_service(hass, "cover", SERVICE_CLOSE_COVER) diff --git a/tests/components/cover/test_reproduce_state.py b/tests/components/cover/test_reproduce_state.py index 39fdf3d3992..2e2d0f63467 100644 --- a/tests/components/cover/test_reproduce_state.py +++ b/tests/components/cover/test_reproduce_state.py @@ -16,6 +16,7 @@ from homeassistant.const import ( STATE_OPEN, ) from homeassistant.core import State + from tests.common import async_mock_service diff --git a/tests/components/daikin/test_config_flow.py b/tests/components/daikin/test_config_flow.py index f9fba67d554..aea78f17564 100644 --- a/tests/components/daikin/test_config_flow.py +++ b/tests/components/daikin/test_config_flow.py @@ -1,6 +1,7 @@ -# pylint: disable=W0621 +# pylint: disable=redefined-outer-name """Tests for the Daikin config flow.""" import asyncio +from unittest.mock import patch import pytest @@ -9,7 +10,7 @@ from homeassistant.components.daikin import config_flow from homeassistant.components.daikin.const import KEY_IP, KEY_MAC from homeassistant.const import CONF_HOST -from tests.common import MockConfigEntry, MockDependency +from tests.common import MockConfigEntry MAC = "AABBCCDDEEFF" HOST = "127.0.0.1" @@ -30,10 +31,10 @@ def mock_daikin(): """Mock the init function in pydaikin.""" pass - with MockDependency("pydaikin.appliance") as mock_daikin_: - mock_daikin_.Appliance().values.get.return_value = "AABBCCDDEEFF" - mock_daikin_.Appliance().init = mock_daikin_init - yield mock_daikin_ + with patch("homeassistant.components.daikin.config_flow.Appliance") as Appliance: + Appliance().values.get.return_value = "AABBCCDDEEFF" + Appliance().init = mock_daikin_init + yield Appliance async def test_user(hass, mock_daikin): @@ -94,7 +95,7 @@ async def test_discovery(hass, mock_daikin): async def test_device_abort(hass, mock_daikin, s_effect, reason): """Test device abort.""" flow = init_config_flow(hass) - mock_daikin.Appliance.side_effect = s_effect + mock_daikin.side_effect = s_effect result = await flow.async_step_user({CONF_HOST: HOST}) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT diff --git a/tests/components/darksky/test_sensor.py b/tests/components/darksky/test_sensor.py index be66b74c186..bb716ed17ec 100644 --- a/tests/components/darksky/test_sensor.py +++ b/tests/components/darksky/test_sensor.py @@ -1,18 +1,17 @@ """The tests for the Dark Sky platform.""" +from datetime import timedelta import re import unittest from unittest.mock import MagicMock, patch -from datetime import timedelta - -from requests.exceptions import HTTPError -import requests_mock import forecastio +from requests.exceptions import HTTPError +import requests_mock from homeassistant.components.darksky import sensor as darksky from homeassistant.setup import setup_component -from tests.common import load_fixture, get_test_home_assistant, MockDependency +from tests.common import MockDependency, get_test_home_assistant, load_fixture VALID_CONFIG_MINIMAL = { "sensor": { diff --git a/tests/components/darksky/test_weather.py b/tests/components/darksky/test_weather.py index ca328f45839..09ffe7bdc90 100644 --- a/tests/components/darksky/test_weather.py +++ b/tests/components/darksky/test_weather.py @@ -4,15 +4,14 @@ import unittest from unittest.mock import patch import forecastio +from requests.exceptions import ConnectionError import requests_mock -from requests.exceptions import ConnectionError - from homeassistant.components import weather -from homeassistant.util.unit_system import METRIC_SYSTEM from homeassistant.setup import setup_component +from homeassistant.util.unit_system import METRIC_SYSTEM -from tests.common import load_fixture, get_test_home_assistant +from tests.common import get_test_home_assistant, load_fixture class TestDarkSky(unittest.TestCase): diff --git a/tests/components/datadog/test_init.py b/tests/components/datadog/test_init.py index 56d70b18d91..fdaec26204d 100644 --- a/tests/components/datadog/test_init.py +++ b/tests/components/datadog/test_init.py @@ -1,18 +1,18 @@ """The tests for the Datadog component.""" -from unittest import mock import unittest +from unittest import mock +import homeassistant.components.datadog as datadog from homeassistant.const import ( EVENT_LOGBOOK_ENTRY, EVENT_STATE_CHANGED, STATE_OFF, STATE_ON, ) -from homeassistant.setup import setup_component -import homeassistant.components.datadog as datadog import homeassistant.core as ha +from homeassistant.setup import setup_component -from tests.common import assert_setup_component, get_test_home_assistant, MockDependency +from tests.common import assert_setup_component, get_test_home_assistant class TestDatadog(unittest.TestCase): @@ -33,11 +33,11 @@ class TestDatadog(unittest.TestCase): self.hass, datadog.DOMAIN, {datadog.DOMAIN: {"host1": "host1"}} ) - @MockDependency("datadog", "beer") - def test_datadog_setup_full(self, mock_datadog): + @mock.patch("homeassistant.components.datadog.statsd") + @mock.patch("homeassistant.components.datadog.initialize") + def test_datadog_setup_full(self, mock_connection, mock_client): """Test setup with all data.""" self.hass.bus.listen = mock.MagicMock() - mock_connection = mock_datadog.initialize assert setup_component( self.hass, @@ -54,11 +54,11 @@ class TestDatadog(unittest.TestCase): assert EVENT_LOGBOOK_ENTRY == self.hass.bus.listen.call_args_list[0][0][0] assert EVENT_STATE_CHANGED == self.hass.bus.listen.call_args_list[1][0][0] - @MockDependency("datadog") - def test_datadog_setup_defaults(self, mock_datadog): + @mock.patch("homeassistant.components.datadog.statsd") + @mock.patch("homeassistant.components.datadog.initialize") + def test_datadog_setup_defaults(self, mock_connection, mock_client): """Test setup with defaults.""" self.hass.bus.listen = mock.MagicMock() - mock_connection = mock_datadog.initialize assert setup_component( self.hass, @@ -78,11 +78,11 @@ class TestDatadog(unittest.TestCase): ) assert self.hass.bus.listen.called - @MockDependency("datadog") - def test_logbook_entry(self, mock_datadog): + @mock.patch("homeassistant.components.datadog.statsd") + @mock.patch("homeassistant.components.datadog.initialize") + def test_logbook_entry(self, mock_connection, mock_client): """Test event listener.""" self.hass.bus.listen = mock.MagicMock() - mock_client = mock_datadog.statsd assert setup_component( self.hass, @@ -110,11 +110,11 @@ class TestDatadog(unittest.TestCase): mock_client.event.reset_mock() - @MockDependency("datadog") - def test_state_changed(self, mock_datadog): + @mock.patch("homeassistant.components.datadog.statsd") + @mock.patch("homeassistant.components.datadog.initialize") + def test_state_changed(self, mock_connection, mock_client): """Test event listener.""" self.hass.bus.listen = mock.MagicMock() - mock_client = mock_datadog.statsd assert setup_component( self.hass, diff --git a/tests/components/deconz/test_binary_sensor.py b/tests/components/deconz/test_binary_sensor.py index 2f42193291c..297be46dd27 100644 --- a/tests/components/deconz/test_binary_sensor.py +++ b/tests/components/deconz/test_binary_sensor.py @@ -2,11 +2,10 @@ from copy import deepcopy from homeassistant.components import deconz +import homeassistant.components.binary_sensor as binary_sensor from homeassistant.setup import async_setup_component -import homeassistant.components.binary_sensor as binary_sensor - -from .test_gateway import ENTRY_CONFIG, DECONZ_WEB_REQUEST, setup_deconz_integration +from .test_gateway import DECONZ_WEB_REQUEST, ENTRY_CONFIG, setup_deconz_integration SENSORS = { "1": { @@ -95,7 +94,14 @@ async def test_binary_sensors(hass): vibration_sensor = hass.states.get("binary_sensor.vibration_sensor") assert vibration_sensor.state == "on" - gateway.api.sensors["1"].async_update({"state": {"presence": True}}) + state_changed_event = { + "t": "event", + "e": "changed", + "r": "sensors", + "id": "1", + "state": {"presence": True}, + } + gateway.api.async_event_handler(state_changed_event) await hass.async_block_till_done() presence_sensor = hass.states.get("binary_sensor.presence_sensor") @@ -143,14 +149,14 @@ async def test_add_new_binary_sensor(hass): ) assert len(gateway.deconz_ids) == 0 - state_added = { + state_added_event = { "t": "event", "e": "added", "r": "sensors", "id": "1", "sensor": deepcopy(SENSORS["1"]), } - gateway.api.async_event_handler(state_added) + gateway.api.async_event_handler(state_added_event) await hass.async_block_till_done() assert "binary_sensor.presence_sensor" in gateway.deconz_ids diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py index cee91f00c42..d905c80b2cd 100644 --- a/tests/components/deconz/test_climate.py +++ b/tests/components/deconz/test_climate.py @@ -4,11 +4,10 @@ from copy import deepcopy from asynctest import patch from homeassistant.components import deconz +import homeassistant.components.climate as climate from homeassistant.setup import async_setup_component -import homeassistant.components.climate as climate - -from .test_gateway import ENTRY_CONFIG, DECONZ_WEB_REQUEST, setup_deconz_integration +from .test_gateway import DECONZ_WEB_REQUEST, ENTRY_CONFIG, setup_deconz_integration SENSORS = { "1": { @@ -94,21 +93,41 @@ async def test_climate_devices(hass): clip_thermostat = hass.states.get("climate.clip_thermostat") assert clip_thermostat is None - thermostat_device = gateway.api.sensors["1"] - - thermostat_device.async_update({"config": {"mode": "off"}}) + state_changed_event = { + "t": "event", + "e": "changed", + "r": "sensors", + "id": "1", + "config": {"mode": "off"}, + } + gateway.api.async_event_handler(state_changed_event) await hass.async_block_till_done() thermostat = hass.states.get("climate.thermostat") assert thermostat.state == "off" - thermostat_device.async_update({"config": {"mode": "other"}, "state": {"on": True}}) + state_changed_event = { + "t": "event", + "e": "changed", + "r": "sensors", + "id": "1", + "config": {"mode": "other"}, + "state": {"on": True}, + } + gateway.api.async_event_handler(state_changed_event) await hass.async_block_till_done() thermostat = hass.states.get("climate.thermostat") assert thermostat.state == "heat" - thermostat_device.async_update({"state": {"on": False}}) + state_changed_event = { + "t": "event", + "e": "changed", + "r": "sensors", + "id": "1", + "state": {"on": False}, + } + gateway.api.async_event_handler(state_changed_event) await hass.async_block_till_done() thermostat = hass.states.get("climate.thermostat") @@ -116,9 +135,9 @@ async def test_climate_devices(hass): # Verify service calls - with patch.object( - thermostat_device, "_async_set_callback", return_value=True - ) as set_callback: + thermostat_device = gateway.api.sensors["1"] + + with patch.object(thermostat_device, "_request", return_value=True) as set_callback: await hass.services.async_call( climate.DOMAIN, climate.SERVICE_SET_HVAC_MODE, @@ -126,11 +145,11 @@ async def test_climate_devices(hass): blocking=True, ) await hass.async_block_till_done() - set_callback.assert_called_with("/sensors/1/config", {"mode": "auto"}) + set_callback.assert_called_with( + "put", "/sensors/1/config", json={"mode": "auto"} + ) - with patch.object( - thermostat_device, "_async_set_callback", return_value=True - ) as set_callback: + with patch.object(thermostat_device, "_request", return_value=True) as set_callback: await hass.services.async_call( climate.DOMAIN, climate.SERVICE_SET_HVAC_MODE, @@ -138,29 +157,31 @@ async def test_climate_devices(hass): blocking=True, ) await hass.async_block_till_done() - set_callback.assert_called_with("/sensors/1/config", {"mode": "heat"}) + set_callback.assert_called_with( + "put", "/sensors/1/config", json={"mode": "heat"} + ) - with patch.object( - thermostat_device, "_async_set_callback", return_value=True - ) as set_callback: + with patch.object(thermostat_device, "_request", return_value=True) as set_callback: await hass.services.async_call( climate.DOMAIN, climate.SERVICE_SET_HVAC_MODE, {"entity_id": "climate.thermostat", "hvac_mode": "off"}, blocking=True, ) - set_callback.assert_called_with("/sensors/1/config", {"mode": "off"}) + set_callback.assert_called_with( + "put", "/sensors/1/config", json={"mode": "off"} + ) - with patch.object( - thermostat_device, "_async_set_callback", return_value=True - ) as set_callback: + with patch.object(thermostat_device, "_request", return_value=True) as set_callback: await hass.services.async_call( climate.DOMAIN, climate.SERVICE_SET_TEMPERATURE, {"entity_id": "climate.thermostat", "temperature": 20}, blocking=True, ) - set_callback.assert_called_with("/sensors/1/config", {"heatsetpoint": 2000.0}) + set_callback.assert_called_with( + "put", "/sensors/1/config", json={"heatsetpoint": 2000.0} + ) await gateway.async_reset() @@ -212,14 +233,14 @@ async def test_verify_state_update(hass): thermostat = hass.states.get("climate.thermostat") assert thermostat.state == "auto" - state_update = { + state_changed_event = { "t": "event", "e": "changed", "r": "sensors", "id": "1", "state": {"on": False}, } - gateway.api.async_event_handler(state_update) + gateway.api.async_event_handler(state_changed_event) await hass.async_block_till_done() thermostat = hass.states.get("climate.thermostat") @@ -235,14 +256,14 @@ async def test_add_new_climate_device(hass): ) assert len(gateway.deconz_ids) == 0 - state_added = { + state_added_event = { "t": "event", "e": "added", "r": "sensors", "id": "1", "sensor": deepcopy(SENSORS["1"]), } - gateway.api.async_event_handler(state_added) + gateway.api.async_event_handler(state_added_event) await hass.async_block_till_done() assert "climate.thermostat" in gateway.deconz_ids diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index 4045201bd18..da8b0a8a7f4 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -1,14 +1,14 @@ """Tests for deCONZ config flow.""" +import asyncio from unittest.mock import Mock, patch -import asyncio - -from homeassistant.components.deconz import config_flow -from homeassistant.components.ssdp import ATTR_MANUFACTURERURL, ATTR_SERIAL -from tests.common import MockConfigEntry - import pydeconz +from homeassistant.components import ssdp +from homeassistant.components.deconz import config_flow + +from tests.common import MockConfigEntry + async def test_flow_works(hass, aioclient_mock): """Test that config flow works.""" @@ -213,11 +213,10 @@ async def test_bridge_ssdp_discovery(hass): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, data={ - config_flow.CONF_HOST: "1.2.3.4", - config_flow.CONF_PORT: 80, - ATTR_SERIAL: "id", - ATTR_MANUFACTURERURL: config_flow.DECONZ_MANUFACTURERURL, - config_flow.ATTR_UUID: "uuid:1234", + ssdp.ATTR_SSDP_LOCATION: "http://1.2.3.4:80/", + ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.DECONZ_MANUFACTURERURL, + ssdp.ATTR_UPNP_SERIAL: "id", + ssdp.ATTR_UPNP_UDN: "uuid:1234", }, context={"source": "ssdp"}, ) @@ -230,7 +229,7 @@ async def test_bridge_ssdp_discovery_not_deconz_bridge(hass): """Test a non deconz bridge being discovered over ssdp.""" result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, - data={ATTR_MANUFACTURERURL: "not deconz bridge"}, + data={ssdp.ATTR_UPNP_MANUFACTURER_URL: "not deconz bridge"}, context={"source": "ssdp"}, ) @@ -257,10 +256,10 @@ async def test_bridge_discovery_update_existing_entry(hass): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, data={ - config_flow.CONF_HOST: "mock-deconz", - ATTR_SERIAL: "123ABC", - ATTR_MANUFACTURERURL: config_flow.DECONZ_MANUFACTURERURL, - config_flow.ATTR_UUID: "uuid:456DEF", + ssdp.ATTR_SSDP_LOCATION: "http://mock-deconz/", + ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.DECONZ_MANUFACTURERURL, + ssdp.ATTR_UPNP_SERIAL: "123ABC", + ssdp.ATTR_UPNP_UDN: "uuid:456DEF", }, context={"source": "ssdp"}, ) @@ -270,6 +269,39 @@ async def test_bridge_discovery_update_existing_entry(hass): assert entry.data[config_flow.CONF_HOST] == "mock-deconz" +async def test_bridge_discovery_dont_update_existing_hassio_entry(hass): + """Test to ensure the SSDP discovery does not update an Hass.io entry.""" + entry = MockConfigEntry( + domain=config_flow.DOMAIN, + source="hassio", + data={ + config_flow.CONF_HOST: "core-deconz", + config_flow.CONF_BRIDGEID: "123ABC", + config_flow.CONF_UUID: "456DEF", + }, + ) + entry.add_to_hass(hass) + + gateway = Mock() + gateway.config_entry = entry + hass.data[config_flow.DOMAIN] = {"123ABC": gateway} + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + data={ + ssdp.ATTR_SSDP_LOCATION: "http://mock-deconz/", + ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.DECONZ_MANUFACTURERURL, + ssdp.ATTR_UPNP_SERIAL: "123ABC", + ssdp.ATTR_UPNP_UDN: "uuid:456DEF", + }, + context={"source": "ssdp"}, + ) + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + assert entry.data[config_flow.CONF_HOST] == "core-deconz" + + async def test_create_entry(hass, aioclient_mock): """Test that _create_entry work and that bridgeid can be requested.""" aioclient_mock.get( @@ -323,32 +355,53 @@ async def test_hassio_update_instance(hass): """Test we can update an existing config entry.""" entry = MockConfigEntry( domain=config_flow.DOMAIN, - data={config_flow.CONF_BRIDGEID: "id", config_flow.CONF_HOST: "1.2.3.4"}, + data={ + config_flow.CONF_BRIDGEID: "id", + config_flow.CONF_HOST: "1.2.3.4", + config_flow.CONF_PORT: 40850, + config_flow.CONF_API_KEY: "secret", + }, ) entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, - data={config_flow.CONF_HOST: "mock-deconz", config_flow.CONF_SERIAL: "id"}, + data={ + config_flow.CONF_HOST: "mock-deconz", + config_flow.CONF_PORT: 8080, + config_flow.CONF_API_KEY: "updated", + config_flow.CONF_SERIAL: "id", + }, context={"source": "hassio"}, ) assert result["type"] == "abort" assert result["reason"] == "updated_instance" assert entry.data[config_flow.CONF_HOST] == "mock-deconz" + assert entry.data[config_flow.CONF_PORT] == 8080 + assert entry.data[config_flow.CONF_API_KEY] == "updated" async def test_hassio_dont_update_instance(hass): """Test we can update an existing config entry.""" entry = MockConfigEntry( domain=config_flow.DOMAIN, - data={config_flow.CONF_BRIDGEID: "id", config_flow.CONF_HOST: "1.2.3.4"}, + data={ + config_flow.CONF_BRIDGEID: "id", + config_flow.CONF_HOST: "1.2.3.4", + config_flow.CONF_PORT: 8080, + config_flow.CONF_API_KEY: "secret", + }, ) entry.add_to_hass(hass) - result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, - data={config_flow.CONF_HOST: "1.2.3.4", config_flow.CONF_SERIAL: "id"}, + data={ + config_flow.CONF_HOST: "1.2.3.4", + config_flow.CONF_PORT: 8080, + config_flow.CONF_API_KEY: "secret", + config_flow.CONF_SERIAL: "id", + }, context={"source": "hassio"}, ) diff --git a/tests/components/deconz/test_cover.py b/tests/components/deconz/test_cover.py index 5c7ee48a78a..f6f5f3a23ca 100644 --- a/tests/components/deconz/test_cover.py +++ b/tests/components/deconz/test_cover.py @@ -4,11 +4,10 @@ from copy import deepcopy from asynctest import patch from homeassistant.components import deconz +import homeassistant.components.cover as cover from homeassistant.setup import async_setup_component -import homeassistant.components.cover as cover - -from .test_gateway import ENTRY_CONFIG, DECONZ_WEB_REQUEST, setup_deconz_integration +from .test_gateway import DECONZ_WEB_REQUEST, ENTRY_CONFIG, setup_deconz_integration COVERS = { "1": { @@ -73,16 +72,23 @@ async def test_cover(hass): level_controllable_cover = hass.states.get("cover.level_controllable_cover") assert level_controllable_cover.state == "open" - level_controllable_cover_device = gateway.api.lights["1"] - - level_controllable_cover_device.async_update({"state": {"on": True}}) + state_changed_event = { + "t": "event", + "e": "changed", + "r": "lights", + "id": "1", + "state": {"on": True}, + } + gateway.api.async_event_handler(state_changed_event) await hass.async_block_till_done() level_controllable_cover = hass.states.get("cover.level_controllable_cover") assert level_controllable_cover.state == "closed" + level_controllable_cover_device = gateway.api.lights["1"] + with patch.object( - level_controllable_cover_device, "_async_set_callback", return_value=True + level_controllable_cover_device, "_request", return_value=True ) as set_callback: await hass.services.async_call( cover.DOMAIN, @@ -91,10 +97,10 @@ async def test_cover(hass): blocking=True, ) await hass.async_block_till_done() - set_callback.assert_called_with("/lights/1/state", {"on": False}) + set_callback.assert_called_with("put", "/lights/1/state", json={"on": False}) with patch.object( - level_controllable_cover_device, "_async_set_callback", return_value=True + level_controllable_cover_device, "_request", return_value=True ) as set_callback: await hass.services.async_call( cover.DOMAIN, @@ -103,10 +109,12 @@ async def test_cover(hass): blocking=True, ) await hass.async_block_till_done() - set_callback.assert_called_with("/lights/1/state", {"on": True, "bri": 255}) + set_callback.assert_called_with( + "put", "/lights/1/state", json={"on": True, "bri": 255} + ) with patch.object( - level_controllable_cover_device, "_async_set_callback", return_value=True + level_controllable_cover_device, "_request", return_value=True ) as set_callback: await hass.services.async_call( cover.DOMAIN, @@ -115,7 +123,7 @@ async def test_cover(hass): blocking=True, ) await hass.async_block_till_done() - set_callback.assert_called_with("/lights/1/state", {"bri_inc": 0}) + set_callback.assert_called_with("put", "/lights/1/state", json={"bri_inc": 0}) await gateway.async_reset() diff --git a/tests/components/deconz/test_deconz_event.py b/tests/components/deconz/test_deconz_event.py index ade9aa02ad4..d4f46176208 100644 --- a/tests/components/deconz/test_deconz_event.py +++ b/tests/components/deconz/test_deconz_event.py @@ -5,7 +5,7 @@ from asynctest import Mock from homeassistant.components.deconz.deconz_event import CONF_DECONZ_EVENT -from .test_gateway import ENTRY_CONFIG, DECONZ_WEB_REQUEST, setup_deconz_integration +from .test_gateway import DECONZ_WEB_REQUEST, ENTRY_CONFIG, setup_deconz_integration SENSORS = { "1": { diff --git a/tests/components/deconz/test_device_trigger.py b/tests/components/deconz/test_device_trigger.py index 91714e647bd..0d86fc30730 100644 --- a/tests/components/deconz/test_device_trigger.py +++ b/tests/components/deconz/test_device_trigger.py @@ -3,9 +3,9 @@ from copy import deepcopy from homeassistant.components.deconz import device_trigger -from tests.common import assert_lists_same, async_get_device_automations +from .test_gateway import DECONZ_WEB_REQUEST, ENTRY_CONFIG, setup_deconz_integration -from .test_gateway import ENTRY_CONFIG, DECONZ_WEB_REQUEST, setup_deconz_integration +from tests.common import assert_lists_same, async_get_device_automations SENSORS = { "1": { diff --git a/tests/components/deconz/test_gateway.py b/tests/components/deconz/test_gateway.py index b98681b6fc9..288868f1bec 100644 --- a/tests/components/deconz/test_gateway.py +++ b/tests/components/deconz/test_gateway.py @@ -2,17 +2,14 @@ from copy import deepcopy from asynctest import Mock, patch - +import pydeconz import pytest from homeassistant import config_entries -from homeassistant.components import deconz -from homeassistant.components import ssdp +from homeassistant.components import deconz, ssdp from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.dispatcher import async_dispatcher_connect -import pydeconz - BRIDGEID = "0123456789" ENTRY_CONFIG = { @@ -51,8 +48,12 @@ async def setup_deconz_integration(hass, config, options, get_state_response): entry_id="1", ) + for resource in ("groups", "lights", "sensors"): + if resource not in get_state_response: + get_state_response[resource] = {} + with patch( - "pydeconz.DeconzSession.async_get_state", return_value=get_state_response + "pydeconz.DeconzSession.request", return_value=get_state_response ), patch("pydeconz.DeconzSession.start", return_value=True): await deconz.async_setup_entry(hass, config_entry) await hass.async_block_till_done() @@ -144,11 +145,10 @@ async def test_update_address(hass): await hass.config_entries.flow.async_init( deconz.config_flow.DOMAIN, data={ - deconz.config_flow.CONF_HOST: "2.3.4.5", - deconz.config_flow.CONF_PORT: 80, - ssdp.ATTR_SERIAL: BRIDGEID, - ssdp.ATTR_MANUFACTURERURL: deconz.config_flow.DECONZ_MANUFACTURERURL, - deconz.config_flow.ATTR_UUID: "uuid:456DEF", + ssdp.ATTR_SSDP_LOCATION: "http://2.3.4.5:80/", + ssdp.ATTR_UPNP_MANUFACTURER_URL: deconz.config_flow.DECONZ_MANUFACTURERURL, + ssdp.ATTR_UPNP_SERIAL: BRIDGEID, + ssdp.ATTR_UPNP_UDN: "uuid:456DEF", }, context={"source": "ssdp"}, ) @@ -172,15 +172,14 @@ async def test_reset_after_successful_setup(hass): async def test_get_gateway(hass): """Successful call.""" - with patch("pydeconz.DeconzSession.async_load_parameters", return_value=True): + with patch("pydeconz.DeconzSession.initialize", return_value=True): assert await deconz.gateway.get_gateway(hass, ENTRY_CONFIG, Mock(), Mock()) async def test_get_gateway_fails_unauthorized(hass): """Failed call.""" with patch( - "pydeconz.DeconzSession.async_load_parameters", - side_effect=pydeconz.errors.Unauthorized, + "pydeconz.DeconzSession.initialize", side_effect=pydeconz.errors.Unauthorized, ), pytest.raises(deconz.errors.AuthenticationRequired): assert ( await deconz.gateway.get_gateway(hass, ENTRY_CONFIG, Mock(), Mock()) @@ -191,8 +190,7 @@ async def test_get_gateway_fails_unauthorized(hass): async def test_get_gateway_fails_cannot_connect(hass): """Failed call.""" with patch( - "pydeconz.DeconzSession.async_load_parameters", - side_effect=pydeconz.errors.RequestError, + "pydeconz.DeconzSession.initialize", side_effect=pydeconz.errors.RequestError, ), pytest.raises(deconz.errors.CannotConnect): assert ( await deconz.gateway.get_gateway(hass, ENTRY_CONFIG, Mock(), Mock()) diff --git a/tests/components/deconz/test_init.py b/tests/components/deconz/test_init.py index 986e01a1599..3c6e02ab41c 100644 --- a/tests/components/deconz/test_init.py +++ b/tests/components/deconz/test_init.py @@ -2,11 +2,10 @@ import asyncio from asynctest import Mock, patch - import pytest -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.components import deconz +from homeassistant.exceptions import ConfigEntryNotReady from tests.common import MockConfigEntry @@ -41,7 +40,7 @@ async def test_setup_entry_fails(hass): deconz.config_flow.CONF_PORT: ENTRY1_PORT, deconz.config_flow.CONF_API_KEY: ENTRY1_API_KEY, } - with patch("pydeconz.DeconzSession.async_load_parameters", side_effect=Exception): + with patch("pydeconz.DeconzSession.initialize", side_effect=Exception): await deconz.async_setup_entry(hass, entry) @@ -54,7 +53,7 @@ async def test_setup_entry_no_available_bridge(hass): deconz.config_flow.CONF_API_KEY: ENTRY1_API_KEY, } with patch( - "pydeconz.DeconzSession.async_load_parameters", side_effect=asyncio.TimeoutError + "pydeconz.DeconzSession.initialize", side_effect=asyncio.TimeoutError ), pytest.raises(ConfigEntryNotReady): await deconz.async_setup_entry(hass, entry) diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py index 14dc5cc8eac..0ba4463ab81 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -4,11 +4,10 @@ from copy import deepcopy from asynctest import patch from homeassistant.components import deconz +import homeassistant.components.light as light from homeassistant.setup import async_setup_component -import homeassistant.components.light as light - -from .test_gateway import ENTRY_CONFIG, DECONZ_WEB_REQUEST, setup_deconz_integration +from .test_gateway import DECONZ_WEB_REQUEST, ENTRY_CONFIG, setup_deconz_integration GROUPS = { "1": { @@ -117,17 +116,22 @@ async def test_lights_and_groups(hass): empty_group = hass.states.get("light.empty_group") assert empty_group is None - rgb_light_device = gateway.api.lights["1"] - - rgb_light_device.async_update({"state": {"on": False}}) + state_changed_event = { + "t": "event", + "e": "changed", + "r": "lights", + "id": "1", + "state": {"on": False}, + } + gateway.api.async_event_handler(state_changed_event) await hass.async_block_till_done() rgb_light = hass.states.get("light.rgb_light") assert rgb_light.state == "off" - with patch.object( - rgb_light_device, "_async_set_callback", return_value=True - ) as set_callback: + rgb_light_device = gateway.api.lights["1"] + + with patch.object(rgb_light_device, "_request", return_value=True) as set_callback: await hass.services.async_call( light.DOMAIN, light.SERVICE_TURN_ON, @@ -143,8 +147,9 @@ async def test_lights_and_groups(hass): ) await hass.async_block_till_done() set_callback.assert_called_with( + "put", "/lights/1/state", - { + json={ "ct": 2500, "bri": 200, "transitiontime": 50, @@ -153,9 +158,7 @@ async def test_lights_and_groups(hass): }, ) - with patch.object( - rgb_light_device, "_async_set_callback", return_value=True - ) as set_callback: + with patch.object(rgb_light_device, "_request", return_value=True) as set_callback: await hass.services.async_call( light.DOMAIN, light.SERVICE_TURN_ON, @@ -169,13 +172,12 @@ async def test_lights_and_groups(hass): ) await hass.async_block_till_done() set_callback.assert_called_with( + "put", "/lights/1/state", - {"xy": (0.411, 0.351), "alert": "lselect", "effect": "none"}, + json={"xy": (0.411, 0.351), "alert": "lselect", "effect": "none"}, ) - with patch.object( - rgb_light_device, "_async_set_callback", return_value=True - ) as set_callback: + with patch.object(rgb_light_device, "_request", return_value=True) as set_callback: await hass.services.async_call( light.DOMAIN, light.SERVICE_TURN_OFF, @@ -184,12 +186,12 @@ async def test_lights_and_groups(hass): ) await hass.async_block_till_done() set_callback.assert_called_with( - "/lights/1/state", {"bri": 0, "transitiontime": 50, "alert": "select"} + "put", + "/lights/1/state", + json={"bri": 0, "transitiontime": 50, "alert": "select"}, ) - with patch.object( - rgb_light_device, "_async_set_callback", return_value=True - ) as set_callback: + with patch.object(rgb_light_device, "_request", return_value=True) as set_callback: await hass.services.async_call( light.DOMAIN, light.SERVICE_TURN_OFF, @@ -197,7 +199,9 @@ async def test_lights_and_groups(hass): blocking=True, ) await hass.async_block_till_done() - set_callback.assert_called_with("/lights/1/state", {"alert": "lselect"}) + set_callback.assert_called_with( + "put", "/lights/1/state", json={"alert": "lselect"} + ) await gateway.async_reset() diff --git a/tests/components/deconz/test_scene.py b/tests/components/deconz/test_scene.py index dcc8ba500c3..2b7cbaa55f5 100644 --- a/tests/components/deconz/test_scene.py +++ b/tests/components/deconz/test_scene.py @@ -4,11 +4,10 @@ from copy import deepcopy from asynctest import patch from homeassistant.components import deconz +import homeassistant.components.scene as scene from homeassistant.setup import async_setup_component -import homeassistant.components.scene as scene - -from .test_gateway import ENTRY_CONFIG, DECONZ_WEB_REQUEST, setup_deconz_integration +from .test_gateway import DECONZ_WEB_REQUEST, ENTRY_CONFIG, setup_deconz_integration GROUPS = { "1": { @@ -60,14 +59,12 @@ async def test_scenes(hass): group_scene = gateway.api.groups["1"].scenes["1"] - with patch.object( - group_scene, "_async_set_state_callback", return_value=True - ) as set_callback: + with patch.object(group_scene, "_request", return_value=True) as set_callback: await hass.services.async_call( "scene", "turn_on", {"entity_id": "scene.light_group_scene"}, blocking=True ) await hass.async_block_till_done() - set_callback.assert_called_with("/groups/1/scenes/1/recall", {}) + set_callback.assert_called_with("put", "/groups/1/scenes/1/recall", json={}) await gateway.async_reset() diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index 7b6ae41086b..c42cd82b3d2 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -2,11 +2,10 @@ from copy import deepcopy from homeassistant.components import deconz +import homeassistant.components.sensor as sensor from homeassistant.setup import async_setup_component -import homeassistant.components.sensor as sensor - -from .test_gateway import ENTRY_CONFIG, DECONZ_WEB_REQUEST, setup_deconz_integration +from .test_gateway import DECONZ_WEB_REQUEST, ENTRY_CONFIG, setup_deconz_integration SENSORS = { "1": { @@ -143,8 +142,23 @@ async def test_sensors(hass): consumption_sensor = hass.states.get("sensor.consumption_sensor") assert consumption_sensor.state == "0.002" - gateway.api.sensors["1"].async_update({"state": {"lightlevel": 2000}}) - gateway.api.sensors["4"].async_update({"config": {"battery": 75}}) + state_changed_event = { + "t": "event", + "e": "changed", + "r": "sensors", + "id": "1", + "state": {"lightlevel": 2000}, + } + gateway.api.async_event_handler(state_changed_event) + + state_changed_event = { + "t": "event", + "e": "changed", + "r": "sensors", + "id": "4", + "config": {"battery": 75}, + } + gateway.api.async_event_handler(state_changed_event) await hass.async_block_till_done() light_level_sensor = hass.states.get("sensor.light_level_sensor") @@ -219,14 +233,14 @@ async def test_add_new_sensor(hass): ) assert len(gateway.deconz_ids) == 0 - state_added = { + state_added_event = { "t": "event", "e": "added", "r": "sensors", "id": "1", "sensor": deepcopy(SENSORS["1"]), } - gateway.api.async_event_handler(state_added) + gateway.api.async_event_handler(state_added_event) await hass.async_block_till_done() assert "sensor.light_level_sensor" in gateway.deconz_ids diff --git a/tests/components/deconz/test_services.py b/tests/components/deconz/test_services.py index 533d85eef7c..fad5444aa00 100644 --- a/tests/components/deconz/test_services.py +++ b/tests/components/deconz/test_services.py @@ -2,7 +2,6 @@ from copy import deepcopy from asynctest import Mock, patch - import pytest import voluptuous as vol @@ -10,8 +9,8 @@ from homeassistant.components import deconz from .test_gateway import ( BRIDGEID, - ENTRY_CONFIG, DECONZ_WEB_REQUEST, + ENTRY_CONFIG, setup_deconz_integration, ) @@ -104,14 +103,14 @@ async def test_configure_service_with_field(hass): deconz.services.SERVICE_DATA: {"on": True, "attr1": 10, "attr2": 20}, } - with patch( - "pydeconz.DeconzSession.async_put_state", return_value=Mock(True) - ) as put_state: + with patch("pydeconz.DeconzSession.request", return_value=Mock(True)) as put_state: await hass.services.async_call( deconz.DOMAIN, deconz.services.SERVICE_CONFIGURE_DEVICE, service_data=data ) await hass.async_block_till_done() - put_state.assert_called_with("/light/2", {"on": True, "attr1": 10, "attr2": 20}) + put_state.assert_called_with( + "put", "/light/2", json={"on": True, "attr1": 10, "attr2": 20} + ) async def test_configure_service_with_entity(hass): @@ -127,14 +126,14 @@ async def test_configure_service_with_entity(hass): deconz.services.SERVICE_DATA: {"on": True, "attr1": 10, "attr2": 20}, } - with patch( - "pydeconz.DeconzSession.async_put_state", return_value=Mock(True) - ) as put_state: + with patch("pydeconz.DeconzSession.request", return_value=Mock(True)) as put_state: await hass.services.async_call( deconz.DOMAIN, deconz.services.SERVICE_CONFIGURE_DEVICE, service_data=data ) await hass.async_block_till_done() - put_state.assert_called_with("/light/1", {"on": True, "attr1": 10, "attr2": 20}) + put_state.assert_called_with( + "put", "/light/1", json={"on": True, "attr1": 10, "attr2": 20} + ) async def test_configure_service_with_entity_and_field(hass): @@ -151,15 +150,13 @@ async def test_configure_service_with_entity_and_field(hass): deconz.services.SERVICE_DATA: {"on": True, "attr1": 10, "attr2": 20}, } - with patch( - "pydeconz.DeconzSession.async_put_state", return_value=Mock(True) - ) as put_state: + with patch("pydeconz.DeconzSession.request", return_value=Mock(True)) as put_state: await hass.services.async_call( deconz.DOMAIN, deconz.services.SERVICE_CONFIGURE_DEVICE, service_data=data ) await hass.async_block_till_done() put_state.assert_called_with( - "/light/1/state", {"on": True, "attr1": 10, "attr2": 20} + "put", "/light/1/state", json={"on": True, "attr1": 10, "attr2": 20} ) @@ -191,9 +188,7 @@ async def test_configure_service_with_faulty_entity(hass): deconz.services.SERVICE_DATA: {}, } - with patch( - "pydeconz.DeconzSession.async_put_state", return_value=Mock(True) - ) as put_state: + with patch("pydeconz.DeconzSession.request", return_value=Mock(True)) as put_state: await hass.services.async_call( deconz.DOMAIN, deconz.services.SERVICE_CONFIGURE_DEVICE, service_data=data ) @@ -211,7 +206,7 @@ async def test_service_refresh_devices(hass): data = {deconz.CONF_BRIDGEID: BRIDGEID} with patch( - "pydeconz.DeconzSession.async_get_state", + "pydeconz.DeconzSession.request", return_value={"groups": GROUP, "lights": LIGHT, "sensors": SENSOR}, ): await hass.services.async_call( diff --git a/tests/components/deconz/test_switch.py b/tests/components/deconz/test_switch.py index 262bd7001f5..352ec84c502 100644 --- a/tests/components/deconz/test_switch.py +++ b/tests/components/deconz/test_switch.py @@ -4,11 +4,10 @@ from copy import deepcopy from asynctest import patch from homeassistant.components import deconz +import homeassistant.components.switch as switch from homeassistant.setup import async_setup_component -import homeassistant.components.switch as switch - -from .test_gateway import ENTRY_CONFIG, DECONZ_WEB_REQUEST, setup_deconz_integration +from .test_gateway import DECONZ_WEB_REQUEST, ENTRY_CONFIG, setup_deconz_integration SWITCHES = { "1": { @@ -85,11 +84,22 @@ async def test_switches(hass): warning_device = hass.states.get("switch.warning_device") assert warning_device.state == "on" - on_off_switch_device = gateway.api.lights["1"] - warning_device_device = gateway.api.lights["3"] - - on_off_switch_device.async_update({"state": {"on": False}}) - warning_device_device.async_update({"state": {"alert": None}}) + state_changed_event = { + "t": "event", + "e": "changed", + "r": "lights", + "id": "1", + "state": {"on": False}, + } + gateway.api.async_event_handler(state_changed_event) + state_changed_event = { + "t": "event", + "e": "changed", + "r": "lights", + "id": "3", + "state": {"alert": None}, + } + gateway.api.async_event_handler(state_changed_event) await hass.async_block_till_done() on_off_switch = hass.states.get("switch.on_off_switch") @@ -98,8 +108,10 @@ async def test_switches(hass): warning_device = hass.states.get("switch.warning_device") assert warning_device.state == "off" + on_off_switch_device = gateway.api.lights["1"] + with patch.object( - on_off_switch_device, "_async_set_callback", return_value=True + on_off_switch_device, "_request", return_value=True ) as set_callback: await hass.services.async_call( switch.DOMAIN, @@ -108,10 +120,10 @@ async def test_switches(hass): blocking=True, ) await hass.async_block_till_done() - set_callback.assert_called_with("/lights/1/state", {"on": True}) + set_callback.assert_called_with("put", "/lights/1/state", json={"on": True}) with patch.object( - on_off_switch_device, "_async_set_callback", return_value=True + on_off_switch_device, "_request", return_value=True ) as set_callback: await hass.services.async_call( switch.DOMAIN, @@ -120,10 +132,12 @@ async def test_switches(hass): blocking=True, ) await hass.async_block_till_done() - set_callback.assert_called_with("/lights/1/state", {"on": False}) + set_callback.assert_called_with("put", "/lights/1/state", json={"on": False}) + + warning_device_device = gateway.api.lights["3"] with patch.object( - warning_device_device, "_async_set_callback", return_value=True + warning_device_device, "_request", return_value=True ) as set_callback: await hass.services.async_call( switch.DOMAIN, @@ -132,10 +146,12 @@ async def test_switches(hass): blocking=True, ) await hass.async_block_till_done() - set_callback.assert_called_with("/lights/3/state", {"alert": "lselect"}) + set_callback.assert_called_with( + "put", "/lights/3/state", json={"alert": "lselect"} + ) with patch.object( - warning_device_device, "_async_set_callback", return_value=True + warning_device_device, "_request", return_value=True ) as set_callback: await hass.services.async_call( switch.DOMAIN, @@ -144,7 +160,9 @@ async def test_switches(hass): blocking=True, ) await hass.async_block_till_done() - set_callback.assert_called_with("/lights/3/state", {"alert": "none"}) + set_callback.assert_called_with( + "put", "/lights/3/state", json={"alert": "none"} + ) await gateway.async_reset() diff --git a/tests/components/default_config/test_init.py b/tests/components/default_config/test_init.py index 5fce4b98019..6b9004595bb 100644 --- a/tests/components/default_config/test_init.py +++ b/tests/components/default_config/test_init.py @@ -1,10 +1,10 @@ """Test the default_config init.""" from unittest.mock import patch -from homeassistant.setup import async_setup_component - import pytest +from homeassistant.setup import async_setup_component + from tests.common import MockDependency, mock_coro diff --git a/tests/components/demo/test_camera.py b/tests/components/demo/test_camera.py index 7a3cf426dac..286a1c8ca22 100644 --- a/tests/components/demo/test_camera.py +++ b/tests/components/demo/test_camera.py @@ -4,7 +4,7 @@ from unittest.mock import mock_open, patch import pytest from homeassistant.components import camera -from homeassistant.components.camera import STATE_STREAMING, STATE_IDLE +from homeassistant.components.camera import STATE_IDLE, STATE_STREAMING from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component diff --git a/tests/components/demo/test_cover.py b/tests/components/demo/test_cover.py index 45a194eab7c..e2478f64f3a 100644 --- a/tests/components/demo/test_cover.py +++ b/tests/components/demo/test_cover.py @@ -4,29 +4,29 @@ from datetime import timedelta import pytest from homeassistant.components.cover import ( - ATTR_POSITION, ATTR_CURRENT_POSITION, ATTR_CURRENT_TILT_POSITION, + ATTR_POSITION, ATTR_TILT_POSITION, DOMAIN, ) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, - STATE_OPEN, - STATE_OPENING, - STATE_CLOSED, - STATE_CLOSING, - SERVICE_TOGGLE, SERVICE_CLOSE_COVER, SERVICE_CLOSE_COVER_TILT, - SERVICE_TOGGLE_COVER_TILT, SERVICE_OPEN_COVER, SERVICE_OPEN_COVER_TILT, SERVICE_SET_COVER_POSITION, SERVICE_SET_COVER_TILT_POSITION, SERVICE_STOP_COVER, SERVICE_STOP_COVER_TILT, + SERVICE_TOGGLE, + SERVICE_TOGGLE_COVER_TILT, + STATE_CLOSED, + STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, ) from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util diff --git a/tests/components/demo/test_fan.py b/tests/components/demo/test_fan.py index 3139c2a45db..71ec5c385dc 100644 --- a/tests/components/demo/test_fan.py +++ b/tests/components/demo/test_fan.py @@ -1,9 +1,9 @@ """Test cases around the demo fan platform.""" import pytest -from homeassistant.setup import async_setup_component from homeassistant.components import fan from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.setup import async_setup_component from tests.components.fan import common diff --git a/tests/components/demo/test_geo_location.py b/tests/components/demo/test_geo_location.py index a95d7e4e807..b7b11a7b46d 100644 --- a/tests/components/demo/test_geo_location.py +++ b/tests/components/demo/test_geo_location.py @@ -4,17 +4,18 @@ from unittest.mock import patch from homeassistant.components import geo_location from homeassistant.components.demo.geo_location import ( - NUMBER_OF_DEMO_DEVICES, DEFAULT_UNIT_OF_MEASUREMENT, DEFAULT_UPDATE_INTERVAL, + NUMBER_OF_DEMO_DEVICES, ) from homeassistant.setup import setup_component +import homeassistant.util.dt as dt_util + from tests.common import ( - get_test_home_assistant, assert_setup_component, fire_time_changed, + get_test_home_assistant, ) -import homeassistant.util.dt as dt_util CONFIG = {geo_location.DOMAIN: [{"platform": "demo"}]} diff --git a/tests/components/demo/test_init.py b/tests/components/demo/test_init.py index 5a420f76882..422ca55b399 100644 --- a/tests/components/demo/test_init.py +++ b/tests/components/demo/test_init.py @@ -4,10 +4,10 @@ import os import pytest -from homeassistant.setup import async_setup_component from homeassistant.components import demo from homeassistant.components.device_tracker.legacy import YAML_DEVICES from homeassistant.helpers.json import JSONEncoder +from homeassistant.setup import async_setup_component @pytest.fixture(autouse=True) diff --git a/tests/components/demo/test_light.py b/tests/components/demo/test_light.py index 10b407688af..48409d6cc37 100644 --- a/tests/components/demo/test_light.py +++ b/tests/components/demo/test_light.py @@ -1,8 +1,8 @@ """The tests for the demo light component.""" import pytest -from homeassistant.setup import async_setup_component from homeassistant.components import light +from homeassistant.setup import async_setup_component from tests.components.light import common @@ -62,10 +62,14 @@ async def test_turn_off(hass): async def test_turn_off_without_entity_id(hass): """Test light turn off all lights.""" - await hass.services.async_call("light", "turn_on", {}, blocking=True) + await hass.services.async_call( + "light", "turn_on", {"entity_id": "all"}, blocking=True + ) assert light.is_on(hass, ENTITY_LIGHT) - await hass.services.async_call("light", "turn_off", {}, blocking=True) + await hass.services.async_call( + "light", "turn_off", {"entity_id": "all"}, blocking=True + ) assert not light.is_on(hass, ENTITY_LIGHT) diff --git a/tests/components/demo/test_lock.py b/tests/components/demo/test_lock.py index 1c4264f1b53..279bd35d12a 100644 --- a/tests/components/demo/test_lock.py +++ b/tests/components/demo/test_lock.py @@ -1,8 +1,8 @@ """The tests for the Demo lock platform.""" import unittest -from homeassistant.setup import setup_component from homeassistant.components import lock +from homeassistant.setup import setup_component from tests.common import get_test_home_assistant, mock_service from tests.components.lock import common diff --git a/tests/components/demo/test_media_player.py b/tests/components/demo/test_media_player.py index 8ff96082f25..a70e7ea4b5d 100644 --- a/tests/components/demo/test_media_player.py +++ b/tests/components/demo/test_media_player.py @@ -1,14 +1,14 @@ """The tests for the Demo Media player platform.""" +import asyncio import unittest from unittest.mock import patch -import asyncio import pytest import voluptuous as vol -from homeassistant.setup import setup_component, async_setup_component import homeassistant.components.media_player as mp from homeassistant.helpers.aiohttp_client import DATA_CLIENTSESSION +from homeassistant.setup import async_setup_component, setup_component from tests.common import get_test_home_assistant from tests.components.media_player import common @@ -209,7 +209,7 @@ class TestDemoMediaPlayer(unittest.TestCase): assert "some_id" == state.attributes.get("media_content_id") @patch( - "homeassistant.components.demo.media_player.DemoYoutubePlayer." "media_seek", + "homeassistant.components.demo.media_player.DemoYoutubePlayer.media_seek", autospec=True, ) def test_seek(self, mock_seek): diff --git a/tests/components/demo/test_notify.py b/tests/components/demo/test_notify.py index becfb49d2c1..30fb49be47d 100644 --- a/tests/components/demo/test_notify.py +++ b/tests/components/demo/test_notify.py @@ -5,11 +5,11 @@ from unittest.mock import patch import pytest import voluptuous as vol -import homeassistant.components.notify as notify -from homeassistant.setup import setup_component import homeassistant.components.demo.notify as demo +import homeassistant.components.notify as notify from homeassistant.core import callback from homeassistant.helpers import discovery, script +from homeassistant.setup import setup_component from tests.common import assert_setup_component, get_test_home_assistant from tests.components.notify import common diff --git a/tests/components/demo/test_remote.py b/tests/components/demo/test_remote.py index a1a0e541b05..b83eaca4c9c 100644 --- a/tests/components/demo/test_remote.py +++ b/tests/components/demo/test_remote.py @@ -2,9 +2,9 @@ # pylint: disable=protected-access import unittest -from homeassistant.setup import setup_component import homeassistant.components.remote as remote -from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.setup import setup_component from tests.common import get_test_home_assistant from tests.components.remote import common diff --git a/tests/components/demo/test_stt.py b/tests/components/demo/test_stt.py index 5933b976460..3fe4e223961 100644 --- a/tests/components/demo/test_stt.py +++ b/tests/components/demo/test_stt.py @@ -1,8 +1,8 @@ """The tests for the demo stt component.""" import pytest -from homeassistant.setup import async_setup_component from homeassistant.components import stt +from homeassistant.setup import async_setup_component @pytest.fixture(autouse=True) diff --git a/tests/components/demo/test_vacuum.py b/tests/components/demo/test_vacuum.py index c18241847fc..13f1b1e352c 100644 --- a/tests/components/demo/test_vacuum.py +++ b/tests/components/demo/test_vacuum.py @@ -2,6 +2,15 @@ import unittest from homeassistant.components import vacuum +from homeassistant.components.demo.vacuum import ( + DEMO_VACUUM_BASIC, + DEMO_VACUUM_COMPLETE, + DEMO_VACUUM_MINIMAL, + DEMO_VACUUM_MOST, + DEMO_VACUUM_NONE, + DEMO_VACUUM_STATE, + FAN_SPEEDS, +) from homeassistant.components.vacuum import ( ATTR_BATTERY_LEVEL, ATTR_COMMAND, @@ -13,21 +22,12 @@ from homeassistant.components.vacuum import ( ENTITY_ID_ALL_VACUUMS, SERVICE_SEND_COMMAND, SERVICE_SET_FAN_SPEED, - STATE_DOCKED, STATE_CLEANING, - STATE_PAUSED, + STATE_DOCKED, STATE_IDLE, + STATE_PAUSED, STATE_RETURNING, ) -from homeassistant.components.demo.vacuum import ( - DEMO_VACUUM_BASIC, - DEMO_VACUUM_COMPLETE, - DEMO_VACUUM_MINIMAL, - DEMO_VACUUM_MOST, - DEMO_VACUUM_NONE, - DEMO_VACUUM_STATE, - FAN_SPEEDS, -) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, @@ -40,7 +40,6 @@ from homeassistant.setup import setup_component from tests.common import get_test_home_assistant, mock_service from tests.components.vacuum import common - ENTITY_VACUUM_BASIC = "{}.{}".format(DOMAIN, DEMO_VACUUM_BASIC).lower() ENTITY_VACUUM_COMPLETE = "{}.{}".format(DOMAIN, DEMO_VACUUM_COMPLETE).lower() ENTITY_VACUUM_MINIMAL = "{}.{}".format(DOMAIN, DEMO_VACUUM_MINIMAL).lower() diff --git a/tests/components/demo/test_water_heater.py b/tests/components/demo/test_water_heater.py index 8a57c3c61f0..97efd48be4a 100644 --- a/tests/components/demo/test_water_heater.py +++ b/tests/components/demo/test_water_heater.py @@ -4,14 +4,13 @@ import unittest import pytest import voluptuous as vol -from homeassistant.util.unit_system import IMPERIAL_SYSTEM -from homeassistant.setup import setup_component from homeassistant.components import water_heater +from homeassistant.setup import setup_component +from homeassistant.util.unit_system import IMPERIAL_SYSTEM from tests.common import get_test_home_assistant from tests.components.water_heater import common - ENTITY_WATER_HEATER = "water_heater.demo_water_heater" ENTITY_WATER_HEATER_CELSIUS = "water_heater.demo_water_heater_celsius" diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index 3c0e3b1eca7..5d997a485a5 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -1,12 +1,11 @@ """The test for light device automation.""" import pytest -from homeassistant.setup import async_setup_component import homeassistant.components.automation as automation from homeassistant.components.websocket_api.const import TYPE_RESULT -from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM +from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON from homeassistant.helpers import device_registry - +from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, @@ -184,6 +183,9 @@ async def test_websocket_get_action_capabilities( entity_reg.async_get_or_create( "alarm_control_panel", "test", "5678", device_id=device_entry.id ) + hass.states.async_set( + "alarm_control_panel.test_5678", "attributes", {"supported_features": 15} + ) expected_capabilities = { "arm_away": {"extra_fields": []}, "arm_home": {"extra_fields": []}, diff --git a/tests/components/device_sun_light_trigger/test_init.py b/tests/components/device_sun_light_trigger/test_init.py index 70681a6d150..98b027c6175 100644 --- a/tests/components/device_sun_light_trigger/test_init.py +++ b/tests/components/device_sun_light_trigger/test_init.py @@ -1,20 +1,21 @@ """The tests device sun light trigger component.""" # pylint: disable=protected-access from datetime import datetime + from asynctest import patch import pytest -from homeassistant.setup import async_setup_component -from homeassistant.const import CONF_PLATFORM, STATE_HOME, STATE_NOT_HOME from homeassistant.components import ( - device_tracker, - light, device_sun_light_trigger, + device_tracker, group, + light, ) from homeassistant.components.device_tracker.const import ( ENTITY_ID_FORMAT as DT_ENTITY_ID_FORMAT, ) +from homeassistant.const import CONF_PLATFORM, STATE_HOME, STATE_NOT_HOME +from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util from tests.common import async_fire_time_changed diff --git a/tests/components/device_tracker/common.py b/tests/components/device_tracker/common.py index 3a2df751d6f..1326174c6be 100644 --- a/tests/components/device_tracker/common.py +++ b/tests/components/device_tracker/common.py @@ -4,15 +4,15 @@ All containing methods are legacy helpers that should not be used by new components. Instead call the service directly. """ from homeassistant.components.device_tracker import ( - DOMAIN, ATTR_ATTRIBUTES, ATTR_BATTERY, + ATTR_DEV_ID, ATTR_GPS, ATTR_GPS_ACCURACY, + ATTR_HOST_NAME, ATTR_LOCATION_NAME, ATTR_MAC, - ATTR_DEV_ID, - ATTR_HOST_NAME, + DOMAIN, SERVICE_SEE, ) from homeassistant.core import callback diff --git a/tests/components/device_tracker/test_device_condition.py b/tests/components/device_tracker/test_device_condition.py index 732abccd8ca..15cd28e8fae 100644 --- a/tests/components/device_tracker/test_device_condition.py +++ b/tests/components/device_tracker/test_device_condition.py @@ -1,19 +1,19 @@ """The tests for Device tracker device conditions.""" import pytest +import homeassistant.components.automation as automation from homeassistant.components.device_tracker import DOMAIN from homeassistant.const import STATE_HOME, STATE_NOT_HOME -from homeassistant.setup import async_setup_component -import homeassistant.components.automation as automation from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, assert_lists_same, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, - async_get_device_automations, ) diff --git a/tests/components/device_tracker/test_entities.py b/tests/components/device_tracker/test_entities.py index 031e961cd2d..a0b2553543d 100644 --- a/tests/components/device_tracker/test_entities.py +++ b/tests/components/device_tracker/test_entities.py @@ -6,11 +6,12 @@ from homeassistant.components.device_tracker.config_entry import ( ScannerEntity, ) from homeassistant.components.device_tracker.const import ( - SOURCE_TYPE_ROUTER, ATTR_SOURCE_TYPE, DOMAIN, + SOURCE_TYPE_ROUTER, ) -from homeassistant.const import STATE_HOME, STATE_NOT_HOME, ATTR_BATTERY_LEVEL +from homeassistant.const import ATTR_BATTERY_LEVEL, STATE_HOME, STATE_NOT_HOME + from tests.common import MockConfigEntry diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index e839a88536e..c82f36f92e7 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -77,7 +77,7 @@ async def test_reading_broken_yaml_config(hass): "badkey.yaml": "@:\n name: Device", "noname.yaml": "my_device:\n", "allok.yaml": "My Device:\n name: Device", - "oneok.yaml": ("My Device!:\n name: Device\n" "bad_device:\n nme: Device"), + "oneok.yaml": ("My Device!:\n name: Device\nbad_device:\n nme: Device"), } args = {"hass": hass, "consider_home": timedelta(seconds=60)} with patch_yaml_files(files): @@ -341,7 +341,7 @@ async def test_group_all_devices(hass, mock_device_tracker_conf): assert (entity_id,) == state.attributes.get(ATTR_ENTITY_ID) -@patch("homeassistant.components.device_tracker.legacy." "DeviceTracker.async_see") +@patch("homeassistant.components.device_tracker.legacy.DeviceTracker.async_see") async def test_see_service(mock_see, hass): """Test the see service with a unicode dev_id and NO MAC.""" with assert_setup_component(1, device_tracker.DOMAIN): diff --git a/tests/components/dialogflow/test_init.py b/tests/components/dialogflow/test_init.py index 18a03ff2603..aaec1ee67cf 100644 --- a/tests/components/dialogflow/test_init.py +++ b/tests/components/dialogflow/test_init.py @@ -1,6 +1,6 @@ """The tests for the Dialogflow component.""" -import json import copy +import json from unittest.mock import Mock import pytest diff --git a/tests/components/directv/test_media_player.py b/tests/components/directv/test_media_player.py index 85916cf6159..449147c3648 100644 --- a/tests/components/directv/test_media_player.py +++ b/tests/components/directv/test_media_player.py @@ -1,33 +1,10 @@ """The tests for the DirecTV Media player platform.""" +from datetime import datetime, timedelta from unittest.mock import call, patch -from datetime import datetime, timedelta -import requests import pytest +import requests -from homeassistant.components.media_player.const import ( - ATTR_MEDIA_CONTENT_ID, - ATTR_MEDIA_CONTENT_TYPE, - MEDIA_TYPE_TVSHOW, - ATTR_MEDIA_ENQUEUE, - ATTR_MEDIA_DURATION, - ATTR_MEDIA_TITLE, - ATTR_MEDIA_POSITION, - ATTR_MEDIA_SERIES_TITLE, - ATTR_MEDIA_CHANNEL, - ATTR_INPUT_SOURCE, - ATTR_MEDIA_POSITION_UPDATED_AT, - DOMAIN, - SERVICE_PLAY_MEDIA, - SUPPORT_PAUSE, - SUPPORT_TURN_ON, - SUPPORT_TURN_OFF, - SUPPORT_PLAY_MEDIA, - SUPPORT_STOP, - SUPPORT_NEXT_TRACK, - SUPPORT_PREVIOUS_TRACK, - SUPPORT_PLAY, -) from homeassistant.components.directv.media_player import ( ATTR_MEDIA_CURRENTLY_RECORDING, ATTR_MEDIA_RATING, @@ -36,6 +13,29 @@ from homeassistant.components.directv.media_player import ( DEFAULT_DEVICE, DEFAULT_PORT, ) +from homeassistant.components.media_player.const import ( + ATTR_INPUT_SOURCE, + ATTR_MEDIA_CHANNEL, + ATTR_MEDIA_CONTENT_ID, + ATTR_MEDIA_CONTENT_TYPE, + ATTR_MEDIA_DURATION, + ATTR_MEDIA_ENQUEUE, + ATTR_MEDIA_POSITION, + ATTR_MEDIA_POSITION_UPDATED_AT, + ATTR_MEDIA_SERIES_TITLE, + ATTR_MEDIA_TITLE, + DOMAIN, + MEDIA_TYPE_TVSHOW, + SERVICE_PLAY_MEDIA, + SUPPORT_NEXT_TRACK, + SUPPORT_PAUSE, + SUPPORT_PLAY, + SUPPORT_PLAY_MEDIA, + SUPPORT_PREVIOUS_TRACK, + SUPPORT_STOP, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, +) from homeassistant.const import ( ATTR_ENTITY_ID, CONF_DEVICE, @@ -58,7 +58,7 @@ from homeassistant.helpers.discovery import async_load_platform from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.common import MockDependency, async_fire_time_changed +from tests.common import async_fire_time_changed CLIENT_ENTITY_ID = "media_player.client_dvr" MAIN_ENTITY_ID = "media_player.main_dvr" @@ -179,8 +179,9 @@ def platforms(hass, dtv_side_effect, mock_now): ] } - with MockDependency("DirectPy"), patch( - "DirectPy.DIRECTV", side_effect=dtv_side_effect + with patch( + "homeassistant.components.directv.media_player.DIRECTV", + side_effect=dtv_side_effect, ), patch("homeassistant.util.dt.utcnow", return_value=mock_now): hass.loop.run_until_complete(async_setup_component(hass, DOMAIN, config)) hass.loop.run_until_complete(hass.async_block_till_done()) @@ -309,7 +310,9 @@ class MockDirectvClass: async def test_setup_platform_config(hass): """Test setting up the platform from configuration.""" - with MockDependency("DirectPy"), patch("DirectPy.DIRECTV", new=MockDirectvClass): + with patch( + "homeassistant.components.directv.media_player.DIRECTV", new=MockDirectvClass + ): await async_setup_component(hass, DOMAIN, WORKING_CONFIG) await hass.async_block_till_done() @@ -321,7 +324,9 @@ async def test_setup_platform_config(hass): async def test_setup_platform_discover(hass): """Test setting up the platform from discovery.""" - with MockDependency("DirectPy"), patch("DirectPy.DIRECTV", new=MockDirectvClass): + with patch( + "homeassistant.components.directv.media_player.DIRECTV", new=MockDirectvClass + ): hass.async_create_task( async_load_platform( @@ -337,7 +342,9 @@ async def test_setup_platform_discover(hass): async def test_setup_platform_discover_duplicate(hass): """Test setting up the platform from discovery.""" - with MockDependency("DirectPy"), patch("DirectPy.DIRECTV", new=MockDirectvClass): + with patch( + "homeassistant.components.directv.media_player.DIRECTV", new=MockDirectvClass + ): await async_setup_component(hass, DOMAIN, WORKING_CONFIG) await hass.async_block_till_done() @@ -358,7 +365,9 @@ async def test_setup_platform_discover_client(hass): LOCATIONS.append({"locationName": "Client 1", "clientAddr": "1"}) LOCATIONS.append({"locationName": "Client 2", "clientAddr": "2"}) - with MockDependency("DirectPy"), patch("DirectPy.DIRECTV", new=MockDirectvClass): + with patch( + "homeassistant.components.directv.media_player.DIRECTV", new=MockDirectvClass + ): await async_setup_component(hass, DOMAIN, WORKING_CONFIG) await hass.async_block_till_done() diff --git a/tests/components/discovery/test_init.py b/tests/components/discovery/test_init.py index 8255751517c..1d11bba9e16 100644 --- a/tests/components/discovery/test_init.py +++ b/tests/components/discovery/test_init.py @@ -1,6 +1,5 @@ """The tests for the discovery component.""" -import asyncio -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch import pytest @@ -9,7 +8,7 @@ from homeassistant.bootstrap import async_setup_component from homeassistant.components import discovery from homeassistant.util.dt import utcnow -from tests.common import mock_coro, async_fire_time_changed +from tests.common import async_fire_time_changed, mock_coro # One might consider to "mock" services, but it's easy enough to just use # what is already available. @@ -55,29 +54,27 @@ async def mock_discovery(hass, discoveries, config=BASE_CONFIG): return mock_discover, mock_platform -@asyncio.coroutine -def test_unknown_service(hass): +async def test_unknown_service(hass): """Test that unknown service is ignored.""" def discover(netdisco): """Fake discovery.""" return [("this_service_will_never_be_supported", {"info": "some"})] - mock_discover, mock_platform = yield from mock_discovery(hass, discover) + mock_discover, mock_platform = await mock_discovery(hass, discover) assert not mock_discover.called assert not mock_platform.called -@asyncio.coroutine -def test_load_platform(hass): +async def test_load_platform(hass): """Test load a platform.""" def discover(netdisco): """Fake discovery.""" return [(SERVICE, SERVICE_INFO)] - mock_discover, mock_platform = yield from mock_discovery(hass, discover) + mock_discover, mock_platform = await mock_discovery(hass, discover) assert not mock_discover.called assert mock_platform.called @@ -86,15 +83,14 @@ def test_load_platform(hass): ) -@asyncio.coroutine -def test_load_component(hass): +async def test_load_component(hass): """Test load a component.""" def discover(netdisco): """Fake discovery.""" return [(SERVICE_NO_PLATFORM, SERVICE_INFO)] - mock_discover, mock_platform = yield from mock_discovery(hass, discover) + mock_discover, mock_platform = await mock_discovery(hass, discover) assert mock_discover.called assert not mock_platform.called @@ -107,24 +103,20 @@ def test_load_component(hass): ) -@asyncio.coroutine -def test_ignore_service(hass): +async def test_ignore_service(hass): """Test ignore service.""" def discover(netdisco): """Fake discovery.""" return [(SERVICE_NO_PLATFORM, SERVICE_INFO)] - mock_discover, mock_platform = yield from mock_discovery( - hass, discover, IGNORE_CONFIG - ) + mock_discover, mock_platform = await mock_discovery(hass, discover, IGNORE_CONFIG) assert not mock_discover.called assert not mock_platform.called -@asyncio.coroutine -def test_discover_duplicates(hass): +async def test_discover_duplicates(hass): """Test load a component.""" def discover(netdisco): @@ -134,7 +126,7 @@ def test_discover_duplicates(hass): (SERVICE_NO_PLATFORM, SERVICE_INFO), ] - mock_discover, mock_platform = yield from mock_discovery(hass, discover) + mock_discover, mock_platform = await mock_discovery(hass, discover) assert mock_discover.called assert mock_discover.call_count == 1 diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index 195345dd489..81249c04046 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -27,8 +27,7 @@ def mock_connection_factory(monkeypatch): transport = asynctest.Mock(spec=asyncio.Transport) protocol = asynctest.Mock(spec=DSMRProtocol) - @asyncio.coroutine - def connection_factory(*args, **kwargs): + async def connection_factory(*args, **kwargs): """Return mocked out Asyncio classes.""" return (transport, protocol) @@ -46,8 +45,7 @@ def mock_connection_factory(monkeypatch): return connection_factory, transport, protocol -@asyncio.coroutine -def test_default_setup(hass, mock_connection_factory): +async def test_default_setup(hass, mock_connection_factory): """Test the default setup.""" (connection_factory, transport, protocol) = mock_connection_factory @@ -67,7 +65,7 @@ def test_default_setup(hass, mock_connection_factory): } with assert_setup_component(1): - yield from async_setup_component(hass, "sensor", {"sensor": config}) + await async_setup_component(hass, "sensor", {"sensor": config}) telegram_callback = connection_factory.call_args_list[0][0][2] @@ -80,7 +78,7 @@ def test_default_setup(hass, mock_connection_factory): telegram_callback(telegram) # after receiving telegram entities need to have the chance to update - yield from asyncio.sleep(0) + await asyncio.sleep(0) # ensure entities have new state value after incoming telegram power_consumption = hass.states.get("sensor.power_consumption") @@ -93,15 +91,14 @@ def test_default_setup(hass, mock_connection_factory): assert power_tariff.attributes.get("unit_of_measurement") == "" -@asyncio.coroutine -def test_derivative(): +async def test_derivative(): """Test calculation of derivative value.""" from dsmr_parser.objects import MBusObject config = {"platform": "dsmr"} entity = DerivativeDSMREntity("test", "1.0.0", config) - yield from entity.async_update() + await entity.async_update() assert entity.state is None, "initial state not unknown" @@ -113,7 +110,7 @@ def test_derivative(): ] ) } - yield from entity.async_update() + await entity.async_update() assert entity.state is None, "state after first update should still be unknown" @@ -125,7 +122,7 @@ def test_derivative(): ] ) } - yield from entity.async_update() + await entity.async_update() assert ( abs(entity.state - 0.033) < 0.00001 @@ -134,22 +131,20 @@ def test_derivative(): assert entity.unit_of_measurement == "m3/h" -@asyncio.coroutine -def test_tcp(hass, mock_connection_factory): +async def test_tcp(hass, mock_connection_factory): """If proper config provided TCP connection should be made.""" (connection_factory, transport, protocol) = mock_connection_factory config = {"platform": "dsmr", "host": "localhost", "port": 1234} with assert_setup_component(1): - yield from async_setup_component(hass, "sensor", {"sensor": config}) + await async_setup_component(hass, "sensor", {"sensor": config}) assert connection_factory.call_args_list[0][0][0] == "localhost" assert connection_factory.call_args_list[0][0][1] == "1234" -@asyncio.coroutine -def test_connection_errors_retry(hass, monkeypatch, mock_connection_factory): +async def test_connection_errors_retry(hass, monkeypatch, mock_connection_factory): """Connection should be retried on error during setup.""" (connection_factory, transport, protocol) = mock_connection_factory @@ -164,15 +159,14 @@ def test_connection_errors_retry(hass, monkeypatch, mock_connection_factory): "homeassistant.components.dsmr.sensor.create_dsmr_reader", first_fail_connection_factory, ) - yield from async_setup_component(hass, "sensor", {"sensor": config}) + await async_setup_component(hass, "sensor", {"sensor": config}) # wait for sleep to resolve - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert first_fail_connection_factory.call_count == 2, "connecting not retried" -@asyncio.coroutine -def test_reconnect(hass, monkeypatch, mock_connection_factory): +async def test_reconnect(hass, monkeypatch, mock_connection_factory): """If transport disconnects, the connection should be retried.""" (connection_factory, transport, protocol) = mock_connection_factory config = {"platform": "dsmr", "reconnect_interval": 0} @@ -182,26 +176,25 @@ def test_reconnect(hass, monkeypatch, mock_connection_factory): # Handshake so that `hass.async_block_till_done()` doesn't cycle forever closed2 = asyncio.Event() - @asyncio.coroutine - def wait_closed(): - yield from closed.wait() + async def wait_closed(): + await closed.wait() closed2.set() closed.clear() protocol.wait_closed = wait_closed - yield from async_setup_component(hass, "sensor", {"sensor": config}) + await async_setup_component(hass, "sensor", {"sensor": config}) assert connection_factory.call_count == 1 # indicate disconnect, release wait lock and allow reconnect to happen closed.set() # wait for lock set to resolve - yield from closed2.wait() + await closed2.wait() closed2.clear() assert not closed.is_set() closed.set() - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert connection_factory.call_count >= 2, "connecting not retried" diff --git a/tests/components/duckdns/test_init.py b/tests/components/duckdns/test_init.py index 0213d9aefa6..9bc9b3504e7 100644 --- a/tests/components/duckdns/test_init.py +++ b/tests/components/duckdns/test_init.py @@ -1,13 +1,14 @@ """Test the DuckDNS component.""" from datetime import timedelta import logging + import pytest +from homeassistant.components import duckdns +from homeassistant.components.duckdns import async_track_time_interval_backoff from homeassistant.loader import bind_hass from homeassistant.setup import async_setup_component -from homeassistant.components import duckdns from homeassistant.util.dt import utcnow -from homeassistant.components.duckdns import async_track_time_interval_backoff from tests.common import async_fire_time_changed diff --git a/tests/components/dyson/test_air_quality.py b/tests/components/dyson/test_air_quality.py index d1ef0c73a51..ed2fbed34f3 100644 --- a/tests/components/dyson/test_air_quality.py +++ b/tests/components/dyson/test_air_quality.py @@ -6,14 +6,14 @@ import asynctest from libpurecool.dyson_pure_cool import DysonPureCool from libpurecool.dyson_pure_state_v2 import DysonEnvironmentalSensorV2State -import homeassistant.components.dyson.air_quality as dyson from homeassistant.components import dyson as dyson_parent from homeassistant.components.air_quality import ( - DOMAIN as AIQ_DOMAIN, + ATTR_NO2, ATTR_PM_2_5, ATTR_PM_10, - ATTR_NO2, + DOMAIN as AIQ_DOMAIN, ) +import homeassistant.components.dyson.air_quality as dyson from homeassistant.helpers import discovery from homeassistant.setup import async_setup_component diff --git a/tests/components/dyson/test_climate.py b/tests/components/dyson/test_climate.py index c3786f47507..dbc477203a1 100644 --- a/tests/components/dyson/test_climate.py +++ b/tests/components/dyson/test_climate.py @@ -9,8 +9,9 @@ from libpurecool.dyson_pure_state import DysonPureHotCoolState from homeassistant.components import dyson as dyson_parent from homeassistant.components.dyson import climate as dyson -from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.setup import async_setup_component + from tests.common import get_test_home_assistant diff --git a/tests/components/dyson/test_fan.py b/tests/components/dyson/test_fan.py index 03fa09110a8..5f6b124a3d5 100644 --- a/tests/components/dyson/test_fan.py +++ b/tests/components/dyson/test_fan.py @@ -4,27 +4,28 @@ import unittest from unittest import mock import asynctest -from libpurecool.const import FanSpeed, FanMode, NightMode, Oscillation +from libpurecool.const import FanMode, FanSpeed, NightMode, Oscillation from libpurecool.dyson_pure_cool import DysonPureCool from libpurecool.dyson_pure_cool_link import DysonPureCoolLink from libpurecool.dyson_pure_state import DysonPureCoolState from libpurecool.dyson_pure_state_v2 import DysonPureCoolV2State -import homeassistant.components.dyson.fan as dyson from homeassistant.components import dyson as dyson_parent from homeassistant.components.dyson import DYSON_DEVICES +import homeassistant.components.dyson.fan as dyson from homeassistant.components.fan import ( - DOMAIN, - ATTR_SPEED, ATTR_OSCILLATING, + ATTR_SPEED, + DOMAIN, + SERVICE_OSCILLATE, + SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, - SPEED_HIGH, - SERVICE_OSCILLATE, ) -from homeassistant.const import SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON from homeassistant.helpers import discovery from homeassistant.setup import async_setup_component + from tests.common import get_test_home_assistant diff --git a/tests/components/dyson/test_init.py b/tests/components/dyson/test_init.py index fc4ede75e32..067f6a360ed 100644 --- a/tests/components/dyson/test_init.py +++ b/tests/components/dyson/test_init.py @@ -3,6 +3,7 @@ import unittest from unittest import mock from homeassistant.components import dyson + from tests.common import get_test_home_assistant diff --git a/tests/components/dyson/test_sensor.py b/tests/components/dyson/test_sensor.py index 8ea9d1ff5ec..442ea913b46 100644 --- a/tests/components/dyson/test_sensor.py +++ b/tests/components/dyson/test_sensor.py @@ -8,9 +8,10 @@ from libpurecool.dyson_pure_cool_link import DysonPureCoolLink from homeassistant.components import dyson as dyson_parent from homeassistant.components.dyson import sensor as dyson -from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT, STATE_OFF +from homeassistant.const import STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.helpers import discovery from homeassistant.setup import async_setup_component + from tests.common import get_test_home_assistant diff --git a/tests/components/dyson/test_vacuum.py b/tests/components/dyson/test_vacuum.py index 3e11e929b32..fc801cbe649 100644 --- a/tests/components/dyson/test_vacuum.py +++ b/tests/components/dyson/test_vacuum.py @@ -7,6 +7,7 @@ from libpurecool.dyson_360_eye import Dyson360Eye from homeassistant.components.dyson import vacuum as dyson from homeassistant.components.dyson.vacuum import Dyson360EyeDevice + from tests.common import get_test_home_assistant diff --git a/tests/components/ecobee/test_climate.py b/tests/components/ecobee/test_climate.py index 90a9a641776..0c0ca785026 100644 --- a/tests/components/ecobee/test_climate.py +++ b/tests/components/ecobee/test_climate.py @@ -1,8 +1,9 @@ """The test for the Ecobee thermostat module.""" import unittest from unittest import mock -import homeassistant.const as const + from homeassistant.components.ecobee import climate as ecobee +import homeassistant.const as const from homeassistant.const import STATE_OFF diff --git a/tests/components/ecobee/test_config_flow.py b/tests/components/ecobee/test_config_flow.py index 64f0e3df0e7..6b53af5daa8 100644 --- a/tests/components/ecobee/test_config_flow.py +++ b/tests/components/ecobee/test_config_flow.py @@ -1,8 +1,8 @@ """Tests for the ecobee config flow.""" -import pytest from unittest.mock import patch from pyecobee import ECOBEE_API_KEY, ECOBEE_REFRESH_TOKEN +import pytest from homeassistant import data_entry_flow from homeassistant.components.ecobee import config_flow @@ -12,6 +12,7 @@ from homeassistant.components.ecobee.const import ( DOMAIN, ) from homeassistant.const import CONF_API_KEY + from tests.common import MockConfigEntry, mock_coro diff --git a/tests/components/ee_brightbox/test_device_tracker.py b/tests/components/ee_brightbox/test_device_tracker.py index 6f732896399..f862539f1df 100644 --- a/tests/components/ee_brightbox/test_device_tracker.py +++ b/tests/components/ee_brightbox/test_device_tracker.py @@ -2,6 +2,7 @@ from datetime import datetime from asynctest import patch +from eebrightbox import EEBrightBoxException import pytest from homeassistant.components.device_tracker import DOMAIN @@ -41,8 +42,6 @@ def _configure_mock_get_devices(eebrightbox_mock): def _configure_mock_failed_config_check(eebrightbox_mock): - from eebrightbox import EEBrightBoxException - eebrightbox_instance = eebrightbox_mock.return_value eebrightbox_instance.__enter__.side_effect = EEBrightBoxException( "Failed to connect to the router" @@ -55,7 +54,7 @@ def mock_dev_track(mock_device_tracker_conf): pass -@patch("eebrightbox.EEBrightBox") +@patch("homeassistant.components.ee_brightbox.device_tracker.EEBrightBox") async def test_missing_credentials(eebrightbox_mock, hass): """Test missing credentials.""" _configure_mock_get_devices(eebrightbox_mock) @@ -73,7 +72,7 @@ async def test_missing_credentials(eebrightbox_mock, hass): assert hass.states.get("device_tracker.hostnameff") is None -@patch("eebrightbox.EEBrightBox") +@patch("homeassistant.components.ee_brightbox.device_tracker.EEBrightBox") async def test_invalid_credentials(eebrightbox_mock, hass): """Test invalid credentials.""" _configure_mock_failed_config_check(eebrightbox_mock) @@ -93,7 +92,7 @@ async def test_invalid_credentials(eebrightbox_mock, hass): assert hass.states.get("device_tracker.hostnameff") is None -@patch("eebrightbox.EEBrightBox") +@patch("homeassistant.components.ee_brightbox.device_tracker.EEBrightBox") async def test_get_devices(eebrightbox_mock, hass): """Test valid configuration.""" _configure_mock_get_devices(eebrightbox_mock) diff --git a/tests/components/efergy/test_sensor.py b/tests/components/efergy/test_sensor.py index cbba49d0ed0..18a00005dd5 100644 --- a/tests/components/efergy/test_sensor.py +++ b/tests/components/efergy/test_sensor.py @@ -5,7 +5,7 @@ import requests_mock from homeassistant.setup import setup_component -from tests.common import load_fixture, get_test_home_assistant +from tests.common import get_test_home_assistant, load_fixture token = "9p6QGJ7dpZfO3fqPTBk1fyEmjV1cGoLT" multi_sensor_token = "9r6QGF7dpZfO3fqPTBl1fyRmjV1cGoLT" diff --git a/tests/components/elgato/__init__.py b/tests/components/elgato/__init__.py new file mode 100644 index 00000000000..1dae6cb1dac --- /dev/null +++ b/tests/components/elgato/__init__.py @@ -0,0 +1,49 @@ +"""Tests for the Elgato Key Light integration.""" + +from homeassistant.components.elgato.const import CONF_SERIAL_NUMBER, DOMAIN +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry, load_fixture +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def init_integration( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, skip_setup: bool = False, +) -> MockConfigEntry: + """Set up the Elgato Key Light integration in Home Assistant.""" + + aioclient_mock.get( + "http://example.local:9123/elgato/accessory-info", + text=load_fixture("elgato/info.json"), + headers={"Content-Type": "application/json"}, + ) + + aioclient_mock.put( + "http://example.local:9123/elgato/lights", + text=load_fixture("elgato/state.json"), + headers={"Content-Type": "application/json"}, + ) + + aioclient_mock.get( + "http://example.local:9123/elgato/lights", + text=load_fixture("elgato/state.json"), + headers={"Content-Type": "application/json"}, + ) + + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "example.local", + CONF_PORT: 9123, + CONF_SERIAL_NUMBER: "CN11A1A00001", + }, + ) + + entry.add_to_hass(hass) + + if not skip_setup: + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + return entry diff --git a/tests/components/elgato/test_config_flow.py b/tests/components/elgato/test_config_flow.py new file mode 100644 index 00000000000..f84b82527a2 --- /dev/null +++ b/tests/components/elgato/test_config_flow.py @@ -0,0 +1,238 @@ +"""Tests for the Elgato Key Light config flow.""" +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.config_entries import SOURCE_USER, SOURCE_ZEROCONF +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.core import HomeAssistant + +from . import init_integration + +from tests.common import load_fixture +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.""" + flow = config_flow.ElgatoFlowHandler() + flow.hass = hass + flow.context = {"source": SOURCE_USER} + result = await flow.async_step_user(user_input=None) + + assert result["step_id"] == "user" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + +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["description_placeholders"] == {CONF_SERIAL_NUMBER: "12345"} + assert result["step_id"] == "zeroconf_confirm" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + +async def test_show_zerconf_form( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test that the zeroconf confirmation form is served.""" + aioclient_mock.get( + "http://example.local:9123/elgato/accessory-info", + text=load_fixture("elgato/info.json"), + headers={"Content-Type": "application/json"}, + ) + + flow = config_flow.ElgatoFlowHandler() + flow.hass = hass + flow.context = {"source": SOURCE_ZEROCONF} + result = await flow.async_step_zeroconf( + {"hostname": "example.local.", "port": 9123} + ) + + assert flow.context[CONF_HOST] == "example.local" + 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 + + +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://example.local/elgato/accessory-info", exc=aiohttp.ClientError + ) + + flow = config_flow.ElgatoFlowHandler() + flow.hass = hass + flow.context = {"source": SOURCE_USER} + result = await flow.async_step_user( + user_input={CONF_HOST: "example.local", CONF_PORT: 9123} + ) + + assert result["errors"] == {"base": "connection_error"} + assert result["step_id"] == "user" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + +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://example.local/elgato/accessory-info", exc=aiohttp.ClientError + ) + + flow = config_flow.ElgatoFlowHandler() + flow.hass = hass + flow.context = {"source": SOURCE_ZEROCONF} + result = await flow.async_step_zeroconf( + user_input={"hostname": "example.local.", "port": 9123} + ) + + assert result["reason"] == "connection_error" + 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://example.local/elgato/accessory-info", exc=aiohttp.ClientError + ) + + flow = config_flow.ElgatoFlowHandler() + flow.hass = hass + flow.context = { + "source": SOURCE_ZEROCONF, + CONF_HOST: "example.local", + CONF_PORT: 9123, + } + result = await flow.async_step_zeroconf_confirm( + user_input={CONF_HOST: "example.local", CONF_PORT: 9123} + ) + + assert result["reason"] == "connection_error" + 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"] == "connection_error" + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + + +async def test_user_device_exists_abort( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort zeroconf flow if Elgato Key Light device already configured.""" + await init_integration(hass, aioclient_mock) + + flow = config_flow.ElgatoFlowHandler() + flow.hass = hass + flow.context = {"source": SOURCE_USER} + result = await flow.async_step_user({CONF_HOST: "example.local", CONF_PORT: 9123}) + + assert result["reason"] == "already_configured" + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + + +async def test_zeroconf_device_exists_abort( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort zeroconf flow if Elgato Key Light device already configured.""" + await init_integration(hass, aioclient_mock) + + flow = config_flow.ElgatoFlowHandler() + flow.hass = hass + flow.context = {"source": SOURCE_ZEROCONF} + result = await flow.async_step_zeroconf( + {"hostname": "example.local.", "port": 9123} + ) + + assert result["reason"] == "already_configured" + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + + flow.context = {"source": SOURCE_ZEROCONF, CONF_HOST: "example.local", "port": 9123} + result = await flow.async_step_zeroconf_confirm( + {"hostname": "example.local.", "port": 9123} + ) + + assert result["reason"] == "already_configured" + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + + +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://example.local:9123/elgato/accessory-info", + text=load_fixture("elgato/info.json"), + headers={"Content-Type": "application/json"}, + ) + + flow = config_flow.ElgatoFlowHandler() + flow.hass = hass + flow.context = {"source": SOURCE_USER} + result = await flow.async_step_user(user_input=None) + + assert result["step_id"] == "user" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + result = await flow.async_step_user( + user_input={CONF_HOST: "example.local", CONF_PORT: 9123} + ) + assert result["data"][CONF_HOST] == "example.local" + 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_full_zeroconf_flow_implementation( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the full manual user flow from start to finish.""" + aioclient_mock.get( + "http://example.local:9123/elgato/accessory-info", + text=load_fixture("elgato/info.json"), + headers={"Content-Type": "application/json"}, + ) + + flow = config_flow.ElgatoFlowHandler() + flow.hass = hass + flow.context = {"source": SOURCE_ZEROCONF} + result = await flow.async_step_zeroconf( + {"hostname": "example.local.", "port": 9123} + ) + + assert flow.context[CONF_HOST] == "example.local" + 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: "example.local"} + ) + assert result["data"][CONF_HOST] == "example.local" + 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 diff --git a/tests/components/elgato/test_init.py b/tests/components/elgato/test_init.py new file mode 100644 index 00000000000..fd2f86fe2ea --- /dev/null +++ b/tests/components/elgato/test_init.py @@ -0,0 +1,33 @@ +"""Tests for the Elgato Key Light integration.""" +import aiohttp + +from homeassistant.components.elgato.const import DOMAIN +from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY +from homeassistant.core import HomeAssistant + +from tests.components.elgato import init_integration +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_config_entry_not_ready( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the Elgato Key Light configuration entry not ready.""" + aioclient_mock.get( + "http://example.local:9123/elgato/accessory-info", exc=aiohttp.ClientError + ) + + 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: + """Test the Elgato Key Light configuration entry unloading.""" + entry = await init_integration(hass, aioclient_mock) + assert hass.data[DOMAIN] + + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + assert not hass.data.get(DOMAIN) diff --git a/tests/components/elgato/test_light.py b/tests/components/elgato/test_light.py new file mode 100644 index 00000000000..13898dad757 --- /dev/null +++ b/tests/components/elgato/test_light.py @@ -0,0 +1,104 @@ +"""Tests for the Elgato Key Light light platform.""" +from unittest.mock import patch + +from homeassistant.components.elgato.light import ElgatoError +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, + DOMAIN as LIGHT_DOMAIN, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_ON, + STATE_UNAVAILABLE, +) +from homeassistant.core import HomeAssistant + +from tests.common import mock_coro +from tests.components.elgato import init_integration +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_light_state( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """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() + + # First segment of the strip + state = hass.states.get("light.frenck") + assert state + assert state.attributes.get(ATTR_BRIGHTNESS) == 54 + assert state.attributes.get(ATTR_COLOR_TEMP) == 297 + assert state.state == STATE_ON + + entry = entity_registry.async_get("light.frenck") + assert entry + assert entry.unique_id == "CN11A1A00001" + + +async def test_light_change_state( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the change of state of a Elgato Key Light device.""" + await init_integration(hass, aioclient_mock) + + state = hass.states.get("light.frenck") + assert state.state == STATE_ON + + with patch( + "homeassistant.components.elgato.light.Elgato.light", return_value=mock_coro(), + ) as mock_light: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: "light.frenck", + ATTR_BRIGHTNESS: 255, + ATTR_COLOR_TEMP: 100, + }, + blocking=True, + ) + await hass.async_block_till_done() + assert len(mock_light.mock_calls) == 1 + mock_light.assert_called_with(on=True, brightness=100, temperature=100) + + with patch( + "homeassistant.components.elgato.light.Elgato.light", return_value=mock_coro(), + ) as mock_light: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.frenck"}, + blocking=True, + ) + await hass.async_block_till_done() + assert len(mock_light.mock_calls) == 1 + mock_light.assert_called_with(on=False) + + +async def test_light_unavailable( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test error/unavailable handling of an Elgato Key Light.""" + await init_integration(hass, aioclient_mock) + with patch( + "homeassistant.components.elgato.light.Elgato.light", 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 diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index 02f24f5afba..2fb5c48e768 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -1,41 +1,41 @@ """The tests for the emulated Hue component.""" -import asyncio -import json +from datetime import timedelta from ipaddress import ip_address +import json from unittest.mock import patch from aiohttp.hdrs import CONTENT_TYPE import pytest -from tests.common import get_test_instance_port from homeassistant import const, setup from homeassistant.components import ( + climate, + cover, + emulated_hue, fan, http, light, - script, - emulated_hue, media_player, - cover, - climate, + script, ) from homeassistant.components.emulated_hue import Config from homeassistant.components.emulated_hue.hue_api import ( - HUE_API_STATE_ON, HUE_API_STATE_BRI, HUE_API_STATE_HUE, + HUE_API_STATE_ON, HUE_API_STATE_SAT, - HueUsernameView, - HueOneLightStateView, - HueAllLightsStateView, - HueOneLightChangeView, + HUE_API_USERNAME, HueAllGroupsStateView, + HueAllLightsStateView, + HueFullStateView, + HueOneLightChangeView, + HueOneLightStateView, + HueUsernameView, ) -from homeassistant.const import STATE_ON, STATE_OFF - +from homeassistant.const import STATE_OFF, STATE_ON import homeassistant.util.dt as dt_util -from datetime import timedelta -from tests.common import async_fire_time_changed + +from tests.common import async_fire_time_changed, get_test_instance_port HTTP_SERVER_PORT = get_test_instance_port() BRIDGE_SERVER_PORT = get_test_instance_port() @@ -56,7 +56,7 @@ def hass_hue(loop, hass): ) ) - with patch("homeassistant.components" ".emulated_hue.UPNPResponderThread"): + with patch("homeassistant.components.emulated_hue.UPNPResponderThread"): loop.run_until_complete( setup.async_setup_component( hass, @@ -188,43 +188,42 @@ def hue_client(loop, hass_hue, aiohttp_client): HueOneLightStateView(config).register(web_app, web_app.router) HueOneLightChangeView(config).register(web_app, web_app.router) HueAllGroupsStateView(config).register(web_app, web_app.router) + HueFullStateView(config).register(web_app, web_app.router) return loop.run_until_complete(aiohttp_client(web_app)) -@asyncio.coroutine -def test_discover_lights(hue_client): +async def test_discover_lights(hue_client): """Test the discovery of lights.""" - result = yield from hue_client.get("/api/username/lights") + result = await hue_client.get("/api/username/lights") assert result.status == 200 assert "application/json" in result.headers["content-type"] - result_json = yield from result.json() + result_json = await result.json() devices = set(val["uniqueid"] for val in result_json.values()) # Make sure the lights we added to the config are there - assert "light.ceiling_lights" in devices - assert "light.bed_light" not in devices - assert "script.set_kitchen_light" in devices - assert "light.kitchen_lights" not in devices - assert "media_player.living_room" in devices - assert "media_player.bedroom" in devices - assert "media_player.walkman" in devices - assert "media_player.lounge_room" in devices - assert "fan.living_room_fan" in devices - assert "fan.ceiling_fan" not in devices - assert "cover.living_room_window" in devices - assert "climate.hvac" in devices - assert "climate.heatpump" in devices - assert "climate.ecobee" not in devices + assert "00:2f:d2:31:ce:c5:55:cc-ee" in devices # light.ceiling_lights + assert "00:b6:14:77:34:b7:bb:06-e8" not in devices # light.bed_light + assert "00:95:b7:51:16:58:6c:c0-c5" in devices # script.set_kitchen_light + assert "00:64:7b:e4:96:c3:fe:90-c3" not in devices # light.kitchen_lights + assert "00:7e:8a:42:35:66:db:86-c5" in devices # media_player.living_room + assert "00:05:44:c2:d6:0a:e5:17-b7" in devices # media_player.bedroom + assert "00:f3:5f:fa:31:f3:32:21-a8" in devices # media_player.walkman + assert "00:b4:06:2e:91:95:23:97-fb" in devices # media_player.lounge_room + assert "00:b2:bd:f9:2c:ad:22:ae-58" in devices # fan.living_room_fan + assert "00:77:4c:8a:23:7d:27:4b-7f" not in devices # fan.ceiling_fan + assert "00:02:53:b9:d5:1a:b3:67-b2" in devices # cover.living_room_window + assert "00:42:03:fe:97:58:2d:b1-50" in devices # climate.hvac + assert "00:7b:2a:c7:08:d6:66:bf-80" in devices # climate.heatpump + assert "00:57:77:a1:6a:8e:ef:b3-6c" not in devices # climate.ecobee -@asyncio.coroutine -def test_light_without_brightness_supported(hass_hue, hue_client): +async def test_light_without_brightness_supported(hass_hue, hue_client): """Test that light without brightness is supported.""" - light_without_brightness_json = yield from perform_get_light_state( + light_without_brightness_json = await perform_get_light_state( hue_client, "light.no_brightness", 200 ) @@ -232,11 +231,71 @@ def test_light_without_brightness_supported(hass_hue, hue_client): assert light_without_brightness_json["type"] == "On/off light" -@asyncio.coroutine -def test_get_light_state(hass_hue, hue_client): +@pytest.mark.parametrize( + "state,is_reachable", + [ + (const.STATE_UNAVAILABLE, False), + (const.STATE_OK, True), + (const.STATE_UNKNOWN, True), + ], +) +async def test_reachable_for_state(hass_hue, hue_client, state, is_reachable): + """Test that an entity is reported as unreachable if in unavailable state.""" + entity_id = "light.ceiling_lights" + + hass_hue.states.async_set(entity_id, state) + + state_json = await perform_get_light_state(hue_client, entity_id, 200) + + assert state_json["state"]["reachable"] == is_reachable, state_json + + +async def test_discover_full_state(hue_client): + """Test the discovery of full state.""" + result = await hue_client.get("/api/" + HUE_API_USERNAME) + + assert result.status == 200 + assert "application/json" in result.headers["content-type"] + + result_json = await result.json() + + # Make sure array has correct content + assert "lights" in result_json + assert "lights" not in result_json["config"] + assert "config" in result_json + assert "config" not in result_json["lights"] + + lights_json = result_json["lights"] + config_json = result_json["config"] + + # Make sure array is correct size + assert len(result_json) == 2 + assert len(config_json) == 4 + assert len(lights_json) >= 1 + + # Make sure the config wrapper added to the config is there + assert "mac" in config_json + assert "00:00:00:00:00:00" in config_json["mac"] + + # Make sure the correct version in config + assert "swversion" in config_json + assert "01003542" in config_json["swversion"] + + # Make sure the correct username in config + assert "whitelist" in config_json + assert HUE_API_USERNAME in config_json["whitelist"] + assert "name" in config_json["whitelist"][HUE_API_USERNAME] + assert "HASS BRIDGE" in config_json["whitelist"][HUE_API_USERNAME]["name"] + + # Make sure the correct ip in config + assert "ipaddress" in config_json + assert "127.0.0.1:8300" in config_json["ipaddress"] + + +async def test_get_light_state(hass_hue, hue_client): """Test the getting of light state.""" # Turn office light on and set to 127 brightness, and set light color - yield from hass_hue.services.async_call( + await hass_hue.services.async_call( light.DOMAIN, const.SERVICE_TURN_ON, { @@ -247,9 +306,7 @@ def test_get_light_state(hass_hue, hue_client): blocking=True, ) - office_json = yield from perform_get_light_state( - hue_client, "light.ceiling_lights", 200 - ) + office_json = await perform_get_light_state(hue_client, "light.ceiling_lights", 200) assert office_json["state"][HUE_API_STATE_ON] is True assert office_json["state"][HUE_API_STATE_BRI] == 127 @@ -257,47 +314,44 @@ def test_get_light_state(hass_hue, hue_client): assert office_json["state"][HUE_API_STATE_SAT] == 217 # Check all lights view - result = yield from hue_client.get("/api/username/lights") + result = await hue_client.get("/api/username/lights") assert result.status == 200 assert "application/json" in result.headers["content-type"] - result_json = yield from result.json() + result_json = await result.json() assert "light.ceiling_lights" in result_json assert result_json["light.ceiling_lights"]["state"][HUE_API_STATE_BRI] == 127 # Turn office light off - yield from hass_hue.services.async_call( + await hass_hue.services.async_call( light.DOMAIN, const.SERVICE_TURN_OFF, {const.ATTR_ENTITY_ID: "light.ceiling_lights"}, blocking=True, ) - office_json = yield from perform_get_light_state( - hue_client, "light.ceiling_lights", 200 - ) + office_json = await perform_get_light_state(hue_client, "light.ceiling_lights", 200) assert office_json["state"][HUE_API_STATE_ON] is False - assert office_json["state"][HUE_API_STATE_BRI] == 0 + # Removed assert HUE_API_STATE_BRI == 0 as Hue API states bri must be 1..254 assert office_json["state"][HUE_API_STATE_HUE] == 0 assert office_json["state"][HUE_API_STATE_SAT] == 0 # Make sure bedroom light isn't accessible - yield from perform_get_light_state(hue_client, "light.bed_light", 404) + await perform_get_light_state(hue_client, "light.bed_light", 401) # Make sure kitchen light isn't accessible - yield from perform_get_light_state(hue_client, "light.kitchen_lights", 404) + await perform_get_light_state(hue_client, "light.kitchen_lights", 401) -@asyncio.coroutine -def test_put_light_state(hass_hue, hue_client): +async def test_put_light_state(hass_hue, hue_client): """Test the setting of light states.""" - yield from perform_put_test_on_ceiling_lights(hass_hue, hue_client) + await perform_put_test_on_ceiling_lights(hass_hue, hue_client) # Turn the bedroom light on first - yield from hass_hue.services.async_call( + await hass_hue.services.async_call( light.DOMAIN, const.SERVICE_TURN_ON, {const.ATTR_ENTITY_ID: "light.ceiling_lights", light.ATTR_BRIGHTNESS: 153}, @@ -309,7 +363,7 @@ def test_put_light_state(hass_hue, hue_client): assert ceiling_lights.attributes[light.ATTR_BRIGHTNESS] == 153 # update light state through api - yield from perform_put_light_state( + await perform_put_light_state( hass_hue, hue_client, "light.ceiling_lights", @@ -320,7 +374,7 @@ def test_put_light_state(hass_hue, hue_client): ) # go through api to get the state back - ceiling_json = yield from perform_get_light_state( + ceiling_json = await perform_get_light_state( hue_client, "light.ceiling_lights", 200 ) assert ceiling_json["state"][HUE_API_STATE_BRI] == 123 @@ -328,11 +382,11 @@ def test_put_light_state(hass_hue, hue_client): assert ceiling_json["state"][HUE_API_STATE_SAT] == 127 # Go through the API to turn it off - ceiling_result = yield from perform_put_light_state( + ceiling_result = await perform_put_light_state( hass_hue, hue_client, "light.ceiling_lights", False ) - ceiling_result_json = yield from ceiling_result.json() + ceiling_result_json = await ceiling_result.json() assert ceiling_result.status == 200 assert "application/json" in ceiling_result.headers["content-type"] @@ -342,31 +396,30 @@ def test_put_light_state(hass_hue, hue_client): # Check to make sure the state changed ceiling_lights = hass_hue.states.get("light.ceiling_lights") assert ceiling_lights.state == STATE_OFF - ceiling_json = yield from perform_get_light_state( + ceiling_json = await perform_get_light_state( hue_client, "light.ceiling_lights", 200 ) - assert ceiling_json["state"][HUE_API_STATE_BRI] == 0 + # Removed assert HUE_API_STATE_BRI == 0 as Hue API states bri must be 1..254 assert ceiling_json["state"][HUE_API_STATE_HUE] == 0 assert ceiling_json["state"][HUE_API_STATE_SAT] == 0 # Make sure we can't change the bedroom light state - bedroom_result = yield from perform_put_light_state( + bedroom_result = await perform_put_light_state( hass_hue, hue_client, "light.bed_light", True ) - assert bedroom_result.status == 404 + assert bedroom_result.status == 401 # Make sure we can't change the kitchen light state - kitchen_result = yield from perform_put_light_state( + kitchen_result = await perform_put_light_state( hass_hue, hue_client, "light.kitchen_light", True ) assert kitchen_result.status == 404 -@asyncio.coroutine -def test_put_light_state_script(hass_hue, hue_client): +async def test_put_light_state_script(hass_hue, hue_client): """Test the setting of script variables.""" # Turn the kitchen light off first - yield from hass_hue.services.async_call( + await hass_hue.services.async_call( light.DOMAIN, const.SERVICE_TURN_OFF, {const.ATTR_ENTITY_ID: "light.kitchen_lights"}, @@ -377,11 +430,11 @@ def test_put_light_state_script(hass_hue, hue_client): level = 23 brightness = round(level * 255 / 100) - script_result = yield from perform_put_light_state( + script_result = await perform_put_light_state( hass_hue, hue_client, "script.set_kitchen_light", True, brightness ) - script_result_json = yield from script_result.json() + script_result_json = await script_result.json() assert script_result.status == 200 assert len(script_result_json) == 2 @@ -391,17 +444,16 @@ def test_put_light_state_script(hass_hue, hue_client): assert kitchen_light.attributes[light.ATTR_BRIGHTNESS] == level -@asyncio.coroutine -def test_put_light_state_climate_set_temperature(hass_hue, hue_client): +async def test_put_light_state_climate_set_temperature(hass_hue, hue_client): """Test setting climate temperature.""" brightness = 19 temperature = round(brightness / 255 * 100) - hvac_result = yield from perform_put_light_state( + hvac_result = await perform_put_light_state( hass_hue, hue_client, "climate.hvac", True, brightness ) - hvac_result_json = yield from hvac_result.json() + hvac_result_json = await hvac_result.json() assert hvac_result.status == 200 assert len(hvac_result_json) == 2 @@ -411,17 +463,16 @@ def test_put_light_state_climate_set_temperature(hass_hue, hue_client): assert hvac.attributes[climate.ATTR_TEMPERATURE] == temperature # Make sure we can't change the ecobee temperature since it's not exposed - ecobee_result = yield from perform_put_light_state( + ecobee_result = await perform_put_light_state( hass_hue, hue_client, "climate.ecobee", True ) - assert ecobee_result.status == 404 + assert ecobee_result.status == 401 -@asyncio.coroutine -def test_put_light_state_media_player(hass_hue, hue_client): +async def test_put_light_state_media_player(hass_hue, hue_client): """Test turning on media player and setting volume.""" # Turn the music player off first - yield from hass_hue.services.async_call( + await hass_hue.services.async_call( media_player.DOMAIN, const.SERVICE_TURN_OFF, {const.ATTR_ENTITY_ID: "media_player.walkman"}, @@ -432,11 +483,11 @@ def test_put_light_state_media_player(hass_hue, hue_client): level = 0.25 brightness = round(level * 255) - mp_result = yield from perform_put_light_state( + mp_result = await perform_put_light_state( hass_hue, hue_client, "media_player.walkman", True, brightness ) - mp_result_json = yield from mp_result.json() + mp_result_json = await mp_result.json() assert mp_result.status == 200 assert len(mp_result_json) == 2 @@ -545,11 +596,10 @@ async def test_set_position_cover(hass_hue, hue_client): assert cover_test_2.attributes.get("current_position") == level -@asyncio.coroutine -def test_put_light_state_fan(hass_hue, hue_client): +async def test_put_light_state_fan(hass_hue, hue_client): """Test turning on fan and setting speed.""" # Turn the fan off first - yield from hass_hue.services.async_call( + await hass_hue.services.async_call( fan.DOMAIN, const.SERVICE_TURN_OFF, {const.ATTR_ENTITY_ID: "fan.living_room_fan"}, @@ -560,11 +610,11 @@ def test_put_light_state_fan(hass_hue, hue_client): level = 43 brightness = round(level * 255 / 100) - fan_result = yield from perform_put_light_state( + fan_result = await perform_put_light_state( hass_hue, hue_client, "fan.living_room_fan", True, brightness ) - fan_result_json = yield from fan_result.json() + fan_result_json = await fan_result.json() assert fan_result.status == 200 assert len(fan_result_json) == 2 @@ -575,17 +625,16 @@ def test_put_light_state_fan(hass_hue, hue_client): # pylint: disable=invalid-name -@asyncio.coroutine -def test_put_with_form_urlencoded_content_type(hass_hue, hue_client): +async def test_put_with_form_urlencoded_content_type(hass_hue, hue_client): """Test the form with urlencoded content.""" # Needed for Alexa - yield from perform_put_test_on_ceiling_lights( + await perform_put_test_on_ceiling_lights( hass_hue, hue_client, "application/x-www-form-urlencoded" ) # Make sure we fail gracefully when we can't parse the data data = {"key1": "value1", "key2": "value2"} - result = yield from hue_client.put( + result = await hue_client.put( "/api/username/lights/light.ceiling_lights/state", headers={"content-type": "application/x-www-form-urlencoded"}, data=data, @@ -594,41 +643,36 @@ def test_put_with_form_urlencoded_content_type(hass_hue, hue_client): assert result.status == 400 -@asyncio.coroutine -def test_entity_not_found(hue_client): +async def test_entity_not_found(hue_client): """Test for entity which are not found.""" - result = yield from hue_client.get("/api/username/lights/not.existant_entity") + result = await hue_client.get("/api/username/lights/not.existant_entity") assert result.status == 404 - result = yield from hue_client.put("/api/username/lights/not.existant_entity/state") + result = await hue_client.put("/api/username/lights/not.existant_entity/state") assert result.status == 404 -@asyncio.coroutine -def test_allowed_methods(hue_client): +async def test_allowed_methods(hue_client): """Test the allowed methods.""" - result = yield from hue_client.get( - "/api/username/lights/light.ceiling_lights/state" - ) + result = await hue_client.get("/api/username/lights/light.ceiling_lights/state") assert result.status == 405 - result = yield from hue_client.put("/api/username/lights/light.ceiling_lights") + result = await hue_client.put("/api/username/lights/light.ceiling_lights") assert result.status == 405 - result = yield from hue_client.put("/api/username/lights") + result = await hue_client.put("/api/username/lights") assert result.status == 405 -@asyncio.coroutine -def test_proper_put_state_request(hue_client): +async def test_proper_put_state_request(hue_client): """Test the request to set the state.""" # Test proper on value parsing - result = yield from hue_client.put( + result = await hue_client.put( "/api/username/lights/{}/state".format("light.ceiling_lights"), data=json.dumps({HUE_API_STATE_ON: 1234}), ) @@ -636,7 +680,7 @@ def test_proper_put_state_request(hue_client): assert result.status == 400 # Test proper brightness value parsing - result = yield from hue_client.put( + result = await hue_client.put( "/api/username/lights/{}/state".format("light.ceiling_lights"), data=json.dumps({HUE_API_STATE_ON: True, HUE_API_STATE_BRI: "Hello world!"}), ) @@ -644,15 +688,14 @@ def test_proper_put_state_request(hue_client): assert result.status == 400 -@asyncio.coroutine -def test_get_empty_groups_state(hue_client): +async def test_get_empty_groups_state(hue_client): """Test the request to get groups endpoint.""" # Test proper on value parsing - result = yield from hue_client.get("/api/username/groups") + result = await hue_client.get("/api/username/groups") assert result.status == 200 - result_json = yield from result.json() + result_json = await result.json() assert result_json == {} @@ -691,23 +734,21 @@ async def perform_put_test_on_ceiling_lights( assert ceiling_lights.attributes[light.ATTR_BRIGHTNESS] == 56 -@asyncio.coroutine -def perform_get_light_state(client, entity_id, expected_status): +async def perform_get_light_state(client, entity_id, expected_status): """Test the getting of a light state.""" - result = yield from client.get("/api/username/lights/{}".format(entity_id)) + result = await client.get("/api/username/lights/{}".format(entity_id)) assert result.status == expected_status if expected_status == 200: assert "application/json" in result.headers["content-type"] - return (yield from result.json()) + return await result.json() return None -@asyncio.coroutine -def perform_put_light_state( +async def perform_put_light_state( hass_hue, client, entity_id, @@ -729,24 +770,40 @@ def perform_put_light_state( if saturation is not None: data[HUE_API_STATE_SAT] = saturation - result = yield from client.put( + result = await client.put( "/api/username/lights/{}/state".format(entity_id), headers=req_headers, data=json.dumps(data).encode(), ) # Wait until state change is complete before continuing - yield from hass_hue.async_block_till_done() + await hass_hue.async_block_till_done() return result async def test_external_ip_blocked(hue_client): """Test external IP blocked.""" + getUrls = [ + "/api/username/groups", + "/api/username", + "/api/username/lights", + "/api/username/lights/light.ceiling_lights", + ] + postUrls = ["/api"] + putUrls = ["/api/username/lights/light.ceiling_lights/state"] with patch( "homeassistant.components.http.real_ip.ip_address", return_value=ip_address("45.45.45.45"), ): - result = await hue_client.get("/api/username/lights") + for getUrl in getUrls: + result = await hue_client.get(getUrl) + assert result.status == 401 - assert result.status == 400 + for postUrl in postUrls: + result = await hue_client.post(postUrl) + assert result.status == 401 + + for putUrl in putUrls: + result = await hue_client.put(putUrl) + assert result.status == 401 diff --git a/tests/components/emulated_hue/test_init.py b/tests/components/emulated_hue/test_init.py index a8798daeba2..6fa6d969539 100644 --- a/tests/components/emulated_hue/test_init.py +++ b/tests/components/emulated_hue/test_init.py @@ -1,5 +1,5 @@ """Test the Emulated Hue component.""" -from unittest.mock import patch, Mock, MagicMock +from unittest.mock import MagicMock, Mock, patch from homeassistant.components.emulated_hue import Config @@ -14,7 +14,7 @@ def test_config_google_home_entity_id_to_number(): "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: + with patch("homeassistant.components.emulated_hue.save_json") as json_saver: number = conf.entity_id_to_number("light.test") assert number == "2" @@ -48,7 +48,7 @@ def test_config_google_home_entity_id_to_number_altered(): "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: + 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 @@ -80,7 +80,7 @@ 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: + 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 diff --git a/tests/components/emulated_hue/test_upnp.py b/tests/components/emulated_hue/test_upnp.py index 44f72ba017b..5897b80659a 100644 --- a/tests/components/emulated_hue/test_upnp.py +++ b/tests/components/emulated_hue/test_upnp.py @@ -1,15 +1,15 @@ """The tests for the emulated Hue component.""" import json - import unittest from unittest.mock import patch -import requests -from aiohttp.hdrs import CONTENT_TYPE -from homeassistant import setup, const +from aiohttp.hdrs import CONTENT_TYPE +import requests + +from homeassistant import const, setup from homeassistant.components import emulated_hue, http -from tests.common import get_test_instance_port, get_test_home_assistant +from tests.common import get_test_home_assistant, get_test_instance_port HTTP_SERVER_PORT = get_test_instance_port() BRIDGE_SERVER_PORT = get_test_instance_port() @@ -32,7 +32,7 @@ class TestEmulatedHue(unittest.TestCase): hass, http.DOMAIN, {http.DOMAIN: {http.CONF_SERVER_PORT: HTTP_SERVER_PORT}} ) - with patch("homeassistant.components" ".emulated_hue.UPNPResponderThread"): + with patch("homeassistant.components.emulated_hue.UPNPResponderThread"): setup.setup_component( hass, emulated_hue.DOMAIN, @@ -52,7 +52,7 @@ class TestEmulatedHue(unittest.TestCase): def test_description_xml(self): """Test the description.""" - import xml.etree.ElementTree as ET + import defusedxml.ElementTree as ET result = requests.get(BRIDGE_URL_BASE.format("/description.xml"), timeout=5) @@ -82,6 +82,31 @@ class TestEmulatedHue(unittest.TestCase): assert "success" in success_json assert "username" in success_json["success"] + def test_unauthorized_view(self): + """Test unauthorized view.""" + request_json = {"devicetype": "my_device"} + + result = requests.get( + BRIDGE_URL_BASE.format("/api/unauthorized"), + data=json.dumps(request_json), + timeout=5, + ) + + assert result.status_code == 200 + assert "application/json" in result.headers["content-type"] + + resp_json = result.json() + assert len(resp_json) == 1 + success_json = resp_json[0] + assert len(success_json) == 1 + + assert "error" in success_json + error_json = success_json["error"] + assert len(error_json) == 3 + assert "/" in error_json["address"] + assert "unauthorized user" in error_json["description"] + assert "1" in error_json["type"] + def test_valid_username_request(self): """Test request with a valid username.""" request_json = {"invalid_key": "my_device"} diff --git a/tests/components/emulated_roku/test_binding.py b/tests/components/emulated_roku/test_binding.py index 19b014a5782..53b6217fcbc 100644 --- a/tests/components/emulated_roku/test_binding.py +++ b/tests/components/emulated_roku/test_binding.py @@ -2,16 +2,16 @@ from unittest.mock import Mock, patch from homeassistant.components.emulated_roku.binding import ( - EmulatedRoku, - EVENT_ROKU_COMMAND, - ATTR_SOURCE_NAME, + ATTR_APP_ID, ATTR_COMMAND_TYPE, ATTR_KEY, - ATTR_APP_ID, - ROKU_COMMAND_KEYPRESS, + ATTR_SOURCE_NAME, + EVENT_ROKU_COMMAND, ROKU_COMMAND_KEYDOWN, + ROKU_COMMAND_KEYPRESS, ROKU_COMMAND_KEYUP, ROKU_COMMAND_LAUNCH, + EmulatedRoku, ) from tests.common import mock_coro_func @@ -44,7 +44,9 @@ async def test_events_fired_properly(hass): def listener(event): events.append(event) - with patch("emulated_roku.EmulatedRokuServer", instantiate): + with patch( + "homeassistant.components.emulated_roku.binding.EmulatedRokuServer", instantiate + ): hass.bus.async_listen(EVENT_ROKU_COMMAND, listener) assert await binding.setup() is True diff --git a/tests/components/emulated_roku/test_config_flow.py b/tests/components/emulated_roku/test_config_flow.py index 3cb25f5a7fc..879d95d0cfc 100644 --- a/tests/components/emulated_roku/test_config_flow.py +++ b/tests/components/emulated_roku/test_config_flow.py @@ -1,5 +1,6 @@ """Tests for emulated_roku config flow.""" from homeassistant.components.emulated_roku import config_flow + from tests.common import MockConfigEntry diff --git a/tests/components/emulated_roku/test_init.py b/tests/components/emulated_roku/test_init.py index f83bd2330c4..efdf330a876 100644 --- a/tests/components/emulated_roku/test_init.py +++ b/tests/components/emulated_roku/test_init.py @@ -1,8 +1,8 @@ """Test emulated_roku component setup process.""" from unittest.mock import Mock, patch -from homeassistant.setup import async_setup_component from homeassistant.components import emulated_roku +from homeassistant.setup import async_setup_component from tests.common import mock_coro_func @@ -10,7 +10,7 @@ from tests.common import mock_coro_func async def test_config_required_fields(hass): """Test that configuration is successful with required fields.""" with patch.object(emulated_roku, "configured_servers", return_value=[]), patch( - "emulated_roku.EmulatedRokuServer", + "homeassistant.components.emulated_roku.binding.EmulatedRokuServer", return_value=Mock(start=mock_coro_func(), close=mock_coro_func()), ): assert ( @@ -35,7 +35,7 @@ async def test_config_required_fields(hass): async def test_config_already_registered_not_configured(hass): """Test that an already registered name causes the entry to be ignored.""" with patch( - "emulated_roku.EmulatedRokuServer", + "homeassistant.components.emulated_roku.binding.EmulatedRokuServer", return_value=Mock(start=mock_coro_func(), close=mock_coro_func()), ) as instantiate, patch.object( emulated_roku, "configured_servers", return_value=["Emulated Roku Test"] @@ -74,7 +74,7 @@ async def test_setup_entry_successful(hass): } with patch( - "emulated_roku.EmulatedRokuServer", + "homeassistant.components.emulated_roku.binding.EmulatedRokuServer", return_value=Mock(start=mock_coro_func(), close=mock_coro_func()), ) as instantiate: assert await emulated_roku.async_setup_entry(hass, entry) is True @@ -98,7 +98,7 @@ async def test_unload_entry(hass): entry.data = {"name": "Emulated Roku Test", "listen_port": 8060} with patch( - "emulated_roku.EmulatedRokuServer", + "homeassistant.components.emulated_roku.binding.EmulatedRokuServer", return_value=Mock(start=mock_coro_func(), close=mock_coro_func()), ): assert await emulated_roku.async_setup_entry(hass, entry) is True diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index 8c05b274dd0..4b951f9a369 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -4,23 +4,17 @@ from unittest.mock import MagicMock, patch import pytest -from homeassistant.components.esphome import config_flow, DATA_KEY -from tests.common import mock_coro, MockConfigEntry +from homeassistant.components.esphome import DATA_KEY, config_flow + +from tests.common import MockConfigEntry, mock_coro MockDeviceInfo = namedtuple("DeviceInfo", ["uses_password", "name"]) -@pytest.fixture(autouse=True) -def aioesphomeapi_mock(): - """Mock aioesphomeapi.""" - with patch.dict("sys.modules", {"aioesphomeapi": MagicMock()}): - yield - - @pytest.fixture def mock_client(): """Mock APIClient.""" - with patch("aioesphomeapi.APIClient") as mock_client: + with patch("homeassistant.components.esphome.config_flow.APIClient") as mock_client: def mock_constructor(loop, host, port, password): """Fake the client constructor.""" @@ -40,7 +34,8 @@ def mock_client(): def mock_api_connection_error(): """Mock out the try login method.""" with patch( - "aioesphomeapi.APIConnectionError", new_callable=lambda: OSError + "homeassistant.components.esphome.config_flow.APIConnectionError", + new_callable=lambda: OSError, ) as mock_error: yield mock_error @@ -86,7 +81,8 @@ async def test_user_resolve_error(hass, mock_api_connection_error, mock_client): super().__init__("Error resolving IP address") with patch( - "aioesphomeapi.APIConnectionError", new_callable=lambda: MockResolveError + "homeassistant.components.esphome.config_flow.APIConnectionError", + new_callable=lambda: MockResolveError, ) as exc: mock_client.device_info.side_effect = exc result = await flow.async_step_user( diff --git a/tests/components/facebook/test_notify.py b/tests/components/facebook/test_notify.py index 5cd5f89ef1f..e23cc4f0982 100644 --- a/tests/components/facebook/test_notify.py +++ b/tests/components/facebook/test_notify.py @@ -1,5 +1,6 @@ """The test for the Facebook notify module.""" import unittest + import requests_mock # import homeassistant.components.facebook as facebook diff --git a/tests/components/facebox/test_image_processing.py b/tests/components/facebox/test_image_processing.py index edad90e41c7..6b248ba1c3c 100644 --- a/tests/components/facebox/test_image_processing.py +++ b/tests/components/facebox/test_image_processing.py @@ -5,23 +5,23 @@ import pytest import requests import requests_mock -from homeassistant.core import callback +import homeassistant.components.facebox.image_processing as fb +import homeassistant.components.image_processing as ip from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_NAME, CONF_FRIENDLY_NAME, - CONF_PASSWORD, - CONF_USERNAME, CONF_IP_ADDRESS, + CONF_PASSWORD, CONF_PORT, + CONF_USERNAME, HTTP_BAD_REQUEST, HTTP_OK, HTTP_UNAUTHORIZED, STATE_UNKNOWN, ) +from homeassistant.core import callback from homeassistant.setup import async_setup_component -import homeassistant.components.image_processing as ip -import homeassistant.components.facebox.image_processing as fb MOCK_IP = "192.168.0.1" MOCK_PORT = "8080" @@ -81,7 +81,7 @@ VALID_CONFIG = { def mock_healthybox(): """Mock fb.check_box_health.""" check_box_health = ( - "homeassistant.components.facebox.image_processing." "check_box_health" + "homeassistant.components.facebox.image_processing.check_box_health" ) with patch(check_box_health, return_value=MOCK_BOX_ID) as _mock_healthybox: yield _mock_healthybox @@ -261,7 +261,7 @@ async def test_teach_service( fb.FILE_PATH: MOCK_FILE_PATH, } await hass.services.async_call( - ip.DOMAIN, fb.SERVICE_TEACH_FACE, service_data=data + fb.DOMAIN, fb.SERVICE_TEACH_FACE, service_data=data ) await hass.async_block_till_done() @@ -275,7 +275,7 @@ async def test_teach_service( fb.FILE_PATH: MOCK_FILE_PATH, } await hass.services.async_call( - ip.DOMAIN, fb.SERVICE_TEACH_FACE, service_data=data + fb.DOMAIN, fb.SERVICE_TEACH_FACE, service_data=data ) await hass.async_block_till_done() assert "AuthenticationError on facebox" in caplog.text @@ -290,7 +290,7 @@ async def test_teach_service( fb.FILE_PATH: MOCK_FILE_PATH, } await hass.services.async_call( - ip.DOMAIN, fb.SERVICE_TEACH_FACE, service_data=data + fb.DOMAIN, fb.SERVICE_TEACH_FACE, service_data=data ) await hass.async_block_till_done() assert MOCK_ERROR_NO_FACE in caplog.text @@ -305,7 +305,7 @@ async def test_teach_service( fb.FILE_PATH: MOCK_FILE_PATH, } await hass.services.async_call( - ip.DOMAIN, fb.SERVICE_TEACH_FACE, service_data=data + fb.DOMAIN, fb.SERVICE_TEACH_FACE, service_data=data ) await hass.async_block_till_done() assert "ConnectionError: Is facebox running?" in caplog.text diff --git a/tests/components/fail2ban/test_sensor.py b/tests/components/fail2ban/test_sensor.py index 55195958169..796ddd93d26 100644 --- a/tests/components/fail2ban/test_sensor.py +++ b/tests/components/fail2ban/test_sensor.py @@ -4,15 +4,15 @@ from unittest.mock import Mock, patch from mock_open import MockOpen -from homeassistant.setup import setup_component from homeassistant.components.fail2ban.sensor import ( - BanSensor, - BanLogParser, - STATE_CURRENT_BANS, STATE_ALL_BANS, + STATE_CURRENT_BANS, + BanLogParser, + BanSensor, ) +from homeassistant.setup import setup_component -from tests.common import get_test_home_assistant, assert_setup_component +from tests.common import assert_setup_component, get_test_home_assistant def fake_log(log_key): diff --git a/tests/components/fan/common.py b/tests/components/fan/common.py index 24a6868a372..70a2c7e43d3 100644 --- a/tests/components/fan/common.py +++ b/tests/components/fan/common.py @@ -5,17 +5,22 @@ components. Instead call the service directly. """ from homeassistant.components.fan import ( ATTR_DIRECTION, - ATTR_SPEED, ATTR_OSCILLATING, + ATTR_SPEED, DOMAIN, SERVICE_OSCILLATE, SERVICE_SET_DIRECTION, SERVICE_SET_SPEED, ) -from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF +from homeassistant.const import ( + ATTR_ENTITY_ID, + ENTITY_MATCH_ALL, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) -async def async_turn_on(hass, entity_id: str = None, speed: str = None) -> None: +async def async_turn_on(hass, entity_id=ENTITY_MATCH_ALL, speed: str = None) -> None: """Turn all or specified fan on.""" data = { key: value @@ -26,7 +31,7 @@ async def async_turn_on(hass, entity_id: str = None, speed: str = None) -> None: await hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data, blocking=True) -async def async_turn_off(hass, entity_id: str = None) -> None: +async def async_turn_off(hass, entity_id=ENTITY_MATCH_ALL) -> None: """Turn all or specified fan off.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} @@ -34,7 +39,7 @@ async def async_turn_off(hass, entity_id: str = None) -> None: async def async_oscillate( - hass, entity_id: str = None, should_oscillate: bool = True + hass, entity_id=ENTITY_MATCH_ALL, should_oscillate: bool = True ) -> None: """Set oscillation on all or specified fan.""" data = { @@ -49,7 +54,7 @@ async def async_oscillate( await hass.services.async_call(DOMAIN, SERVICE_OSCILLATE, data, blocking=True) -async def async_set_speed(hass, entity_id: str = None, speed: str = None) -> None: +async def async_set_speed(hass, entity_id=ENTITY_MATCH_ALL, speed: str = None) -> None: """Set speed for all or specified fan.""" data = { key: value @@ -61,7 +66,7 @@ async def async_set_speed(hass, entity_id: str = None, speed: str = None) -> Non async def async_set_direction( - hass, entity_id: str = None, direction: str = None + hass, entity_id=ENTITY_MATCH_ALL, direction: str = None ) -> None: """Set direction for all or specified fan.""" data = { diff --git a/tests/components/fan/test_device_action.py b/tests/components/fan/test_device_action.py index 928fd353dd5..70da4bd1fca 100644 --- a/tests/components/fan/test_device_action.py +++ b/tests/components/fan/test_device_action.py @@ -1,18 +1,18 @@ """The tests for Fan device actions.""" import pytest -from homeassistant.components.fan import DOMAIN -from homeassistant.setup import async_setup_component import homeassistant.components.automation as automation +from homeassistant.components.fan import DOMAIN from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, assert_lists_same, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, - async_get_device_automations, ) diff --git a/tests/components/fan/test_device_condition.py b/tests/components/fan/test_device_condition.py index ea87e36b636..e665f9d5ddc 100644 --- a/tests/components/fan/test_device_condition.py +++ b/tests/components/fan/test_device_condition.py @@ -1,19 +1,19 @@ """The tests for Fan device conditions.""" import pytest -from homeassistant.components.fan import DOMAIN -from homeassistant.const import STATE_ON, STATE_OFF -from homeassistant.setup import async_setup_component import homeassistant.components.automation as automation +from homeassistant.components.fan import DOMAIN +from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, assert_lists_same, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, - async_get_device_automations, ) diff --git a/tests/components/fan/test_device_trigger.py b/tests/components/fan/test_device_trigger.py index fa41749cf36..3d4f4229965 100644 --- a/tests/components/fan/test_device_trigger.py +++ b/tests/components/fan/test_device_trigger.py @@ -1,19 +1,19 @@ """The tests for Fan device triggers.""" import pytest -from homeassistant.components.fan import DOMAIN -from homeassistant.const import STATE_ON, STATE_OFF -from homeassistant.setup import async_setup_component import homeassistant.components.automation as automation +from homeassistant.components.fan import DOMAIN +from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, assert_lists_same, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, - async_get_device_automations, ) diff --git a/tests/components/fan/test_init.py b/tests/components/fan/test_init.py index fec4d5b2495..316504381ec 100644 --- a/tests/components/fan/test_init.py +++ b/tests/components/fan/test_init.py @@ -2,9 +2,10 @@ import unittest -from homeassistant.components.fan import FanEntity import pytest +from homeassistant.components.fan import FanEntity + class BaseFan(FanEntity): """Implementation of the abstract FanEntity.""" diff --git a/tests/components/feedreader/test_init.py b/tests/components/feedreader/test_init.py index eff44c44303..048be11e079 100644 --- a/tests/components/feedreader/test_init.py +++ b/tests/components/feedreader/test_init.py @@ -1,27 +1,27 @@ """The tests for the feedreader component.""" -import time from datetime import timedelta - -import unittest -from genericpath import exists from logging import getLogger from os import remove +from os.path import exists +import time +import unittest from unittest import mock from unittest.mock import patch from homeassistant.components import feedreader from homeassistant.components.feedreader import ( + CONF_MAX_ENTRIES, CONF_URLS, + DEFAULT_MAX_ENTRIES, + DEFAULT_SCAN_INTERVAL, + EVENT_FEEDREADER, FeedManager, StoredData, - EVENT_FEEDREADER, - DEFAULT_SCAN_INTERVAL, - CONF_MAX_ENTRIES, - DEFAULT_MAX_ENTRIES, ) -from homeassistant.const import EVENT_HOMEASSISTANT_START, CONF_SCAN_INTERVAL +from homeassistant.const import CONF_SCAN_INTERVAL, EVENT_HOMEASSISTANT_START from homeassistant.core import callback from homeassistant.setup import setup_component + from tests.common import get_test_home_assistant, load_fixture _LOGGER = getLogger(__name__) @@ -50,7 +50,7 @@ class TestFeedreaderComponent(unittest.TestCase): def test_setup_one_feed(self): """Test the general setup of this component.""" with patch( - "homeassistant.components.feedreader." "track_time_interval" + "homeassistant.components.feedreader.track_time_interval" ) as track_method: assert setup_component(self.hass, feedreader.DOMAIN, VALID_CONFIG_1) track_method.assert_called_once_with( @@ -60,7 +60,7 @@ class TestFeedreaderComponent(unittest.TestCase): def test_setup_scan_interval(self): """Test the setup of this component with scan interval.""" with patch( - "homeassistant.components.feedreader." "track_time_interval" + "homeassistant.components.feedreader.track_time_interval" ) as track_method: assert setup_component(self.hass, feedreader.DOMAIN, VALID_CONFIG_2) track_method.assert_called_once_with( @@ -88,7 +88,7 @@ class TestFeedreaderComponent(unittest.TestCase): data_file = self.hass.config.path("{}.pickle".format(feedreader.DOMAIN)) storage = StoredData(data_file) with patch( - "homeassistant.components.feedreader." "track_time_interval" + "homeassistant.components.feedreader.track_time_interval" ) as track_method: manager = FeedManager( feed_data, DEFAULT_SCAN_INTERVAL, max_entries, self.hass, storage @@ -131,7 +131,7 @@ class TestFeedreaderComponent(unittest.TestCase): # Must patch 'get_timestamp' method because the timestamp is stored # with the URL which in these tests is the raw XML data. with patch( - "homeassistant.components.feedreader.StoredData." "get_timestamp", + "homeassistant.components.feedreader.StoredData.get_timestamp", return_value=time.struct_time((2018, 4, 30, 5, 10, 0, 0, 120, 0)), ): manager2, events2 = self.setup_manager(feed_data2) @@ -139,7 +139,7 @@ class TestFeedreaderComponent(unittest.TestCase): # 3. Run feed_data3 = load_fixture("feedreader1.xml") with patch( - "homeassistant.components.feedreader.StoredData." "get_timestamp", + "homeassistant.components.feedreader.StoredData.get_timestamp", return_value=time.struct_time((2018, 4, 30, 5, 11, 0, 0, 120, 0)), ): manager3, events3 = self.setup_manager(feed_data3) diff --git a/tests/components/ffmpeg/test_init.py b/tests/components/ffmpeg/test_init.py index e0b68cd61b1..3c6a2fbb92d 100644 --- a/tests/components/ffmpeg/test_init.py +++ b/tests/components/ffmpeg/test_init.py @@ -1,5 +1,4 @@ """The tests for Home Assistant ffmpeg.""" -import asyncio from unittest.mock import MagicMock import homeassistant.components.ffmpeg as ffmpeg @@ -11,9 +10,9 @@ from homeassistant.components.ffmpeg import ( ) from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import callback -from homeassistant.setup import setup_component, async_setup_component +from homeassistant.setup import async_setup_component, setup_component -from tests.common import get_test_home_assistant, assert_setup_component +from tests.common import assert_setup_component, get_test_home_assistant @callback @@ -61,14 +60,12 @@ class MockFFmpegDev(ffmpeg.FFmpegBase): self.called_restart = False self.called_entities = None - @asyncio.coroutine - def _async_start_ffmpeg(self, entity_ids): + async def _async_start_ffmpeg(self, entity_ids): """Mock start.""" self.called_start = True self.called_entities = entity_ids - @asyncio.coroutine - def _async_stop_ffmpeg(self, entity_ids): + async def _async_stop_ffmpeg(self, entity_ids): """Mock stop.""" self.called_stop = True self.called_entities = entity_ids @@ -102,91 +99,85 @@ class TestFFmpegSetup: assert self.hass.services.has_service(ffmpeg.DOMAIN, "restart") -@asyncio.coroutine -def test_setup_component_test_register(hass): +async def test_setup_component_test_register(hass): """Set up ffmpeg component test register.""" with assert_setup_component(1): - yield from async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) + await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) hass.bus.async_listen_once = MagicMock() ffmpeg_dev = MockFFmpegDev(hass) - yield from ffmpeg_dev.async_added_to_hass() + await ffmpeg_dev.async_added_to_hass() assert hass.bus.async_listen_once.called assert hass.bus.async_listen_once.call_count == 2 -@asyncio.coroutine -def test_setup_component_test_register_no_startup(hass): +async def test_setup_component_test_register_no_startup(hass): """Set up ffmpeg component test register without startup.""" with assert_setup_component(1): - yield from async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) + await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) hass.bus.async_listen_once = MagicMock() ffmpeg_dev = MockFFmpegDev(hass, False) - yield from ffmpeg_dev.async_added_to_hass() + await ffmpeg_dev.async_added_to_hass() assert hass.bus.async_listen_once.called assert hass.bus.async_listen_once.call_count == 1 -@asyncio.coroutine -def test_setup_component_test_service_start(hass): +async def test_setup_component_test_service_start(hass): """Set up ffmpeg component test service start.""" with assert_setup_component(1): - yield from async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) + await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) ffmpeg_dev = MockFFmpegDev(hass, False) - yield from ffmpeg_dev.async_added_to_hass() + await ffmpeg_dev.async_added_to_hass() async_start(hass) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert ffmpeg_dev.called_start -@asyncio.coroutine -def test_setup_component_test_service_stop(hass): +async def test_setup_component_test_service_stop(hass): """Set up ffmpeg component test service stop.""" with assert_setup_component(1): - yield from async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) + await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) ffmpeg_dev = MockFFmpegDev(hass, False) - yield from ffmpeg_dev.async_added_to_hass() + await ffmpeg_dev.async_added_to_hass() async_stop(hass) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert ffmpeg_dev.called_stop -@asyncio.coroutine -def test_setup_component_test_service_restart(hass): +async def test_setup_component_test_service_restart(hass): """Set up ffmpeg component test service restart.""" with assert_setup_component(1): - yield from async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) + await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) ffmpeg_dev = MockFFmpegDev(hass, False) - yield from ffmpeg_dev.async_added_to_hass() + await ffmpeg_dev.async_added_to_hass() async_restart(hass) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert ffmpeg_dev.called_stop assert ffmpeg_dev.called_start -@asyncio.coroutine -def test_setup_component_test_service_start_with_entity(hass): +async def test_setup_component_test_service_start_with_entity(hass): """Set up ffmpeg component test service start.""" with assert_setup_component(1): - yield from async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) + await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) ffmpeg_dev = MockFFmpegDev(hass, False) - yield from ffmpeg_dev.async_added_to_hass() + await ffmpeg_dev.async_added_to_hass() async_start(hass, "test.ffmpeg_device") - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert ffmpeg_dev.called_start assert ffmpeg_dev.called_entities == ["test.ffmpeg_device"] diff --git a/tests/components/ffmpeg/test_sensor.py b/tests/components/ffmpeg/test_sensor.py index 175da3a1ab0..5a89daa624c 100644 --- a/tests/components/ffmpeg/test_sensor.py +++ b/tests/components/ffmpeg/test_sensor.py @@ -3,7 +3,7 @@ from unittest.mock import patch from homeassistant.setup import setup_component -from tests.common import get_test_home_assistant, assert_setup_component, mock_coro +from tests.common import assert_setup_component, get_test_home_assistant, mock_coro class TestFFmpegNoiseSetup: diff --git a/tests/components/fido/test_sensor.py b/tests/components/fido/test_sensor.py index 510d4321243..1f67a1e2e11 100644 --- a/tests/components/fido/test_sensor.py +++ b/tests/components/fido/test_sensor.py @@ -2,12 +2,12 @@ import asyncio import logging import sys -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch from homeassistant.bootstrap import async_setup_component from homeassistant.components.fido import sensor as fido -from tests.common import assert_setup_component +from tests.common import assert_setup_component CONTRACT = "123456789" @@ -66,29 +66,25 @@ def fake_async_add_entities(component, update_before_add=False): @asyncio.coroutine def test_fido_sensor(loop, hass): """Test the Fido number sensor.""" - sys.modules["pyfido"] = MagicMock() - sys.modules["pyfido.client"] = MagicMock() - sys.modules["pyfido.client.PyFidoError"] = PyFidoErrorMock - import pyfido.client - - pyfido.FidoClient = FidoClientMock - pyfido.client.PyFidoError = PyFidoErrorMock - config = { - "sensor": { - "platform": "fido", - "name": "fido", - "username": "myusername", - "password": "password", - "monitored_variables": ["balance", "data_remaining"], + with patch( + "homeassistant.components.fido.sensor.FidoClient", new=FidoClientMock + ), patch("homeassistant.components.fido.sensor.PyFidoError", new=PyFidoErrorMock): + config = { + "sensor": { + "platform": "fido", + "name": "fido", + "username": "myusername", + "password": "password", + "monitored_variables": ["balance", "data_remaining"], + } } - } - with assert_setup_component(1): - yield from async_setup_component(hass, "sensor", config) - state = hass.states.get("sensor.fido_1112223344_balance") - assert state.state == "160.12" - assert state.attributes.get("number") == "1112223344" - state = hass.states.get("sensor.fido_1112223344_data_remaining") - assert state.state == "100.33" + with assert_setup_component(1): + yield from async_setup_component(hass, "sensor", config) + state = hass.states.get("sensor.fido_1112223344_balance") + assert state.state == "160.12" + assert state.attributes.get("number") == "1112223344" + state = hass.states.get("sensor.fido_1112223344_data_remaining") + assert state.state == "100.33" @asyncio.coroutine diff --git a/tests/components/file/test_notify.py b/tests/components/file/test_notify.py index 16f4c72658e..52524d5b189 100644 --- a/tests/components/file/test_notify.py +++ b/tests/components/file/test_notify.py @@ -3,9 +3,9 @@ import os import unittest from unittest.mock import call, mock_open, patch -from homeassistant.setup import setup_component import homeassistant.components.notify as notify from homeassistant.components.notify import ATTR_TITLE_DEFAULT +from homeassistant.setup import setup_component import homeassistant.util.dt as dt_util from tests.common import assert_setup_component, get_test_home_assistant diff --git a/tests/components/file/test_sensor.py b/tests/components/file/test_sensor.py index 912f11aab3b..3afdd8284fc 100644 --- a/tests/components/file/test_sensor.py +++ b/tests/components/file/test_sensor.py @@ -6,8 +6,8 @@ from unittest.mock import Mock, patch # https://bugs.python.org/issue23004 from mock_open import MockOpen -from homeassistant.setup import setup_component from homeassistant.const import STATE_UNKNOWN +from homeassistant.setup import setup_component from tests.common import get_test_home_assistant, mock_registry diff --git a/tests/components/filesize/test_sensor.py b/tests/components/filesize/test_sensor.py index 2246a74abe5..29bd6a7fb1f 100644 --- a/tests/components/filesize/test_sensor.py +++ b/tests/components/filesize/test_sensor.py @@ -1,11 +1,11 @@ """The tests for the filesize sensor.""" -import unittest import os +import unittest from homeassistant.components.filesize.sensor import CONF_FILE_PATHS from homeassistant.setup import setup_component -from tests.common import get_test_home_assistant +from tests.common import get_test_home_assistant TEST_DIR = os.path.join(os.path.dirname(__file__)) TEST_FILE = os.path.join(TEST_DIR, "mock_file_test_filesize.txt") diff --git a/tests/components/filter/test_sensor.py b/tests/components/filter/test_sensor.py index e9c8f4c35e2..9ae4245ed70 100644 --- a/tests/components/filter/test_sensor.py +++ b/tests/components/filter/test_sensor.py @@ -6,17 +6,18 @@ from unittest.mock import patch from homeassistant.components.filter.sensor import ( LowPassFilter, OutlierFilter, + RangeFilter, ThrottleFilter, TimeSMAFilter, - RangeFilter, TimeThrottleFilter, ) -import homeassistant.util.dt as dt_util -from homeassistant.setup import setup_component import homeassistant.core as ha +from homeassistant.setup import setup_component +import homeassistant.util.dt as dt_util + from tests.common import ( - get_test_home_assistant, assert_setup_component, + get_test_home_assistant, init_recorder_component, ) @@ -27,6 +28,7 @@ class TestFilterSensor(unittest.TestCase): def setup_method(self, method): """Set up things to be run when tests are started.""" self.hass = get_test_home_assistant() + self.hass.config.components.add("history") raw_values = [20, 19, 18, 21, 22, 0] self.values = [] @@ -115,11 +117,11 @@ class TestFilterSensor(unittest.TestCase): } with patch( - "homeassistant.components.history." "state_changes_during_period", + "homeassistant.components.history.state_changes_during_period", return_value=fake_states, ): with patch( - "homeassistant.components.history." "get_last_state_changes", + "homeassistant.components.history.get_last_state_changes", return_value=fake_states, ): with assert_setup_component(1, "sensor"): @@ -163,11 +165,11 @@ class TestFilterSensor(unittest.TestCase): ] } with patch( - "homeassistant.components.history." "state_changes_during_period", + "homeassistant.components.history.state_changes_during_period", return_value=fake_states, ): with patch( - "homeassistant.components.history." "get_last_state_changes", + "homeassistant.components.history.get_last_state_changes", return_value=fake_states, ): with assert_setup_component(1, "sensor"): diff --git a/tests/components/flux/test_switch.py b/tests/components/flux/test_switch.py index 91871666f46..b3d0a008961 100644 --- a/tests/components/flux/test_switch.py +++ b/tests/components/flux/test_switch.py @@ -2,15 +2,15 @@ from asynctest.mock import patch import pytest -from homeassistant.setup import async_setup_component -from homeassistant.components import switch, light +from homeassistant.components import light, switch from homeassistant.const import ( CONF_PLATFORM, - STATE_ON, SERVICE_TURN_ON, + STATE_ON, SUN_EVENT_SUNRISE, ) from homeassistant.core import State +from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from tests.common import ( diff --git a/tests/components/folder/test_sensor.py b/tests/components/folder/test_sensor.py index 97c933fdd23..fc7de1f59c0 100644 --- a/tests/components/folder/test_sensor.py +++ b/tests/components/folder/test_sensor.py @@ -1,11 +1,11 @@ """The tests for the folder sensor.""" -import unittest import os +import unittest from homeassistant.components.folder.sensor import CONF_FOLDER_PATHS from homeassistant.setup import setup_component -from tests.common import get_test_home_assistant +from tests.common import get_test_home_assistant CWD = os.path.join(os.path.dirname(__file__)) TEST_FOLDER = "test_folder" diff --git a/tests/components/folder_watcher/test_init.py b/tests/components/folder_watcher/test_init.py index 69dea7f51ec..dee97afa468 100644 --- a/tests/components/folder_watcher/test_init.py +++ b/tests/components/folder_watcher/test_init.py @@ -1,9 +1,10 @@ """The tests for the folder_watcher component.""" -from unittest.mock import Mock, patch import os +from unittest.mock import Mock, patch from homeassistant.components import folder_watcher from homeassistant.setup import async_setup_component + from tests.common import MockDependency diff --git a/tests/components/foobot/test_sensor.py b/tests/components/foobot/test_sensor.py index 6c601c5ad69..9c6a17264eb 100644 --- a/tests/components/foobot/test_sensor.py +++ b/tests/components/foobot/test_sensor.py @@ -1,16 +1,17 @@ """The tests for the Foobot sensor platform.""" -import re import asyncio +import re from unittest.mock import MagicMock + import pytest - -import homeassistant.components.sensor as sensor from homeassistant.components.foobot import sensor as foobot +import homeassistant.components.sensor as sensor from homeassistant.const import TEMP_CELSIUS from homeassistant.exceptions import PlatformNotReady from homeassistant.setup import async_setup_component + from tests.common import load_fixture VALID_CONFIG = { diff --git a/tests/components/freedns/test_init.py b/tests/components/freedns/test_init.py index 6441a418bfa..b9e59de9ff1 100644 --- a/tests/components/freedns/test_init.py +++ b/tests/components/freedns/test_init.py @@ -1,9 +1,10 @@ """Test the FreeDNS component.""" import asyncio + import pytest -from homeassistant.setup import async_setup_component from homeassistant.components import freedns +from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow from tests.common import async_fire_time_changed diff --git a/tests/components/frontend/test_init.py b/tests/components/frontend/test_init.py index b38c55cd583..f8fd5f1d7e3 100644 --- a/tests/components/frontend/test_init.py +++ b/tests/components/frontend/test_init.py @@ -5,19 +5,18 @@ from unittest.mock import patch import pytest -from homeassistant.setup import async_setup_component from homeassistant.components.frontend import ( - DOMAIN, - CONF_JS_VERSION, - CONF_THEMES, CONF_EXTRA_HTML_URL, CONF_EXTRA_HTML_URL_ES5, + CONF_JS_VERSION, + CONF_THEMES, + DOMAIN, EVENT_PANELS_UPDATED, ) from homeassistant.components.websocket_api.const import TYPE_RESULT +from homeassistant.setup import async_setup_component -from tests.common import mock_coro, async_capture_events - +from tests.common import async_capture_events, mock_coro CONFIG_THEMES = {DOMAIN: {CONF_THEMES: {"happy": {"primary-color": "red"}}}} diff --git a/tests/components/frontend/test_storage.py b/tests/components/frontend/test_storage.py index 76296f743c5..d907f69bbf9 100644 --- a/tests/components/frontend/test_storage.py +++ b/tests/components/frontend/test_storage.py @@ -1,8 +1,8 @@ """The tests for frontend storage.""" import pytest -from homeassistant.setup import async_setup_component from homeassistant.components.frontend import storage +from homeassistant.setup import async_setup_component @pytest.fixture(autouse=True) diff --git a/tests/components/generic/test_camera.py b/tests/components/generic/test_camera.py index 4708725fd3b..c25b4ce9f3d 100644 --- a/tests/components/generic/test_camera.py +++ b/tests/components/generic/test_camera.py @@ -1,6 +1,5 @@ """The tests for generic camera component.""" import asyncio - from unittest import mock from homeassistant.setup import async_setup_component diff --git a/tests/components/geo_json_events/test_geo_location.py b/tests/components/geo_json_events/test_geo_location.py index 7e356cc03f6..38c7200cce1 100644 --- a/tests/components/geo_json_events/test_geo_location.py +++ b/tests/components/geo_json_events/test_geo_location.py @@ -1,30 +1,31 @@ """The tests for the geojson platform.""" -from asynctest.mock import patch, MagicMock, call +from asynctest.mock import MagicMock, call, patch from homeassistant.components import geo_location -from homeassistant.components.geo_location import ATTR_SOURCE from homeassistant.components.geo_json_events.geo_location import ( - SCAN_INTERVAL, ATTR_EXTERNAL_ID, + SCAN_INTERVAL, SIGNAL_DELETE_ENTITY, SIGNAL_UPDATE_ENTITY, ) +from homeassistant.components.geo_location import ATTR_SOURCE from homeassistant.const import ( - CONF_URL, - EVENT_HOMEASSISTANT_START, - CONF_RADIUS, + ATTR_FRIENDLY_NAME, ATTR_LATITUDE, ATTR_LONGITUDE, - ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, CONF_LATITUDE, CONF_LONGITUDE, + CONF_RADIUS, + CONF_URL, + EVENT_HOMEASSISTANT_START, ) from homeassistant.helpers.dispatcher import DATA_DISPATCHER from homeassistant.setup import async_setup_component -from tests.common import assert_setup_component, async_fire_time_changed import homeassistant.util.dt as dt_util +from tests.common import assert_setup_component, async_fire_time_changed + URL = "http://geo.json.local/geo_json_events.json" CONFIG = { geo_location.DOMAIN: [ diff --git a/tests/components/geo_rss_events/test_sensor.py b/tests/components/geo_rss_events/test_sensor.py index 492290b9519..25243afea78 100644 --- a/tests/components/geo_rss_events/test_sensor.py +++ b/tests/components/geo_rss_events/test_sensor.py @@ -4,20 +4,21 @@ from unittest import mock from unittest.mock import MagicMock, patch from homeassistant.components import sensor +import homeassistant.components.geo_rss_events.sensor as geo_rss_events from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, ATTR_FRIENDLY_NAME, - EVENT_HOMEASSISTANT_START, ATTR_ICON, + ATTR_UNIT_OF_MEASUREMENT, + EVENT_HOMEASSISTANT_START, ) from homeassistant.setup import setup_component +import homeassistant.util.dt as dt_util + from tests.common import ( - get_test_home_assistant, assert_setup_component, fire_time_changed, + get_test_home_assistant, ) -import homeassistant.components.geo_rss_events.sensor as geo_rss_events -import homeassistant.util.dt as dt_util URL = "http://geo.rss.local/geo_rss_events.xml" VALID_CONFIG_WITH_CATEGORIES = { diff --git a/tests/components/geofency/test_init.py b/tests/components/geofency/test_init.py index 6254fd4a504..319a79966fd 100644 --- a/tests/components/geofency/test_init.py +++ b/tests/components/geofency/test_init.py @@ -1,6 +1,6 @@ """The tests for the Geofency device tracker platform.""" # pylint: disable=redefined-outer-name -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch import pytest diff --git a/tests/components/geonetnz_quakes/test_config_flow.py b/tests/components/geonetnz_quakes/test_config_flow.py index 2d8e3750648..494ceaa542d 100644 --- a/tests/components/geonetnz_quakes/test_config_flow.py +++ b/tests/components/geonetnz_quakes/test_config_flow.py @@ -1,26 +1,27 @@ """Define tests for the GeoNet NZ Quakes config flow.""" from datetime import timedelta +from asynctest import CoroutineMock, patch import pytest -from asynctest import patch, CoroutineMock from homeassistant import data_entry_flow from homeassistant.components.geonetnz_quakes import ( - async_setup_entry, - config_flow, - CONF_MMI, CONF_MINIMUM_MAGNITUDE, + CONF_MMI, DOMAIN, - async_unload_entry, FEED, + async_setup_entry, + async_unload_entry, + config_flow, ) from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS, - CONF_UNIT_SYSTEM, CONF_SCAN_INTERVAL, + CONF_UNIT_SYSTEM, ) + from tests.common import MockConfigEntry diff --git a/tests/components/geonetnz_quakes/test_geo_location.py b/tests/components/geonetnz_quakes/test_geo_location.py index 04bbdc9dcf0..0132a07c745 100644 --- a/tests/components/geonetnz_quakes/test_geo_location.py +++ b/tests/components/geonetnz_quakes/test_geo_location.py @@ -1,34 +1,35 @@ """The tests for the GeoNet NZ Quakes Feed integration.""" import datetime -from asynctest import patch, CoroutineMock +from asynctest import CoroutineMock, patch from homeassistant.components import geonetnz_quakes from homeassistant.components.geo_location import ATTR_SOURCE from homeassistant.components.geonetnz_quakes import DEFAULT_SCAN_INTERVAL from homeassistant.components.geonetnz_quakes.geo_location import ( - ATTR_EXTERNAL_ID, - ATTR_MAGNITUDE, - ATTR_LOCALITY, - ATTR_MMI, ATTR_DEPTH, + ATTR_EXTERNAL_ID, + ATTR_LOCALITY, + ATTR_MAGNITUDE, + ATTR_MMI, ATTR_QUALITY, ) from homeassistant.const import ( - EVENT_HOMEASSISTANT_START, - CONF_RADIUS, + ATTR_ATTRIBUTION, + ATTR_FRIENDLY_NAME, + ATTR_ICON, ATTR_LATITUDE, ATTR_LONGITUDE, - ATTR_FRIENDLY_NAME, - ATTR_UNIT_OF_MEASUREMENT, - ATTR_ATTRIBUTION, ATTR_TIME, - ATTR_ICON, + ATTR_UNIT_OF_MEASUREMENT, + CONF_RADIUS, + EVENT_HOMEASSISTANT_START, ) from homeassistant.setup import async_setup_component -from homeassistant.util.unit_system import IMPERIAL_SYSTEM -from tests.common import async_fire_time_changed import homeassistant.util.dt as dt_util +from homeassistant.util.unit_system import IMPERIAL_SYSTEM + +from tests.common import async_fire_time_changed from tests.components.geonetnz_quakes import _generate_mock_feed_entry CONFIG = {geonetnz_quakes.DOMAIN: {CONF_RADIUS: 200}} diff --git a/tests/components/geonetnz_quakes/test_sensor.py b/tests/components/geonetnz_quakes/test_sensor.py index 518e08f02bb..aecd012ba1c 100644 --- a/tests/components/geonetnz_quakes/test_sensor.py +++ b/tests/components/geonetnz_quakes/test_sensor.py @@ -1,27 +1,28 @@ """The tests for the GeoNet NZ Quakes Feed integration.""" import datetime -from asynctest import patch, CoroutineMock +from asynctest import CoroutineMock, patch from homeassistant.components import geonetnz_quakes from homeassistant.components.geonetnz_quakes import DEFAULT_SCAN_INTERVAL from homeassistant.components.geonetnz_quakes.sensor import ( - ATTR_STATUS, - ATTR_LAST_UPDATE, ATTR_CREATED, - ATTR_UPDATED, - ATTR_REMOVED, + ATTR_LAST_UPDATE, ATTR_LAST_UPDATE_SUCCESSFUL, + ATTR_REMOVED, + ATTR_STATUS, + ATTR_UPDATED, ) from homeassistant.const import ( - EVENT_HOMEASSISTANT_START, - CONF_RADIUS, - ATTR_UNIT_OF_MEASUREMENT, ATTR_ICON, + ATTR_UNIT_OF_MEASUREMENT, + CONF_RADIUS, + EVENT_HOMEASSISTANT_START, ) from homeassistant.setup import async_setup_component -from tests.common import async_fire_time_changed import homeassistant.util.dt as dt_util + +from tests.common import async_fire_time_changed from tests.components.geonetnz_quakes import _generate_mock_feed_entry CONFIG = {geonetnz_quakes.DOMAIN: {CONF_RADIUS: 200}} diff --git a/tests/components/geonetnz_volcano/__init__.py b/tests/components/geonetnz_volcano/__init__.py new file mode 100644 index 00000000000..708b69e0031 --- /dev/null +++ b/tests/components/geonetnz_volcano/__init__.py @@ -0,0 +1,25 @@ +"""The tests for the GeoNet NZ Volcano Feed integration.""" +from unittest.mock import MagicMock + + +def _generate_mock_feed_entry( + external_id, + title, + alert_level, + distance_to_home, + coordinates, + attribution=None, + activity=None, + hazards=None, +): + """Construct a mock feed entry for testing purposes.""" + feed_entry = MagicMock() + feed_entry.external_id = external_id + feed_entry.title = title + feed_entry.alert_level = alert_level + feed_entry.distance_to_home = distance_to_home + feed_entry.coordinates = coordinates + feed_entry.attribution = attribution + feed_entry.activity = activity + feed_entry.hazards = hazards + return feed_entry diff --git a/tests/components/geonetnz_volcano/conftest.py b/tests/components/geonetnz_volcano/conftest.py new file mode 100644 index 00000000000..33a299eeb79 --- /dev/null +++ b/tests/components/geonetnz_volcano/conftest.py @@ -0,0 +1,29 @@ +"""Configuration for GeoNet NZ Volcano tests.""" +import pytest + +from homeassistant.components.geonetnz_volcano import DOMAIN +from homeassistant.const import ( + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_RADIUS, + CONF_SCAN_INTERVAL, + CONF_UNIT_SYSTEM, +) + +from tests.common import MockConfigEntry + + +@pytest.fixture +def config_entry(): + """Create a mock GeoNet NZ Volcano config entry.""" + return MockConfigEntry( + domain=DOMAIN, + data={ + CONF_LATITUDE: -41.2, + CONF_LONGITUDE: 174.7, + CONF_RADIUS: 25, + CONF_UNIT_SYSTEM: "metric", + CONF_SCAN_INTERVAL: 300.0, + }, + title="-41.2, 174.7", + ) diff --git a/tests/components/geonetnz_volcano/test_config_flow.py b/tests/components/geonetnz_volcano/test_config_flow.py new file mode 100644 index 00000000000..8f589aded90 --- /dev/null +++ b/tests/components/geonetnz_volcano/test_config_flow.py @@ -0,0 +1,81 @@ +"""Define tests for the GeoNet NZ Volcano config flow.""" +from datetime import timedelta + +from homeassistant import data_entry_flow +from homeassistant.components.geonetnz_volcano import config_flow +from homeassistant.const import ( + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_RADIUS, + CONF_SCAN_INTERVAL, + CONF_UNIT_SYSTEM, +) + + +async def test_duplicate_error(hass, config_entry): + """Test that errors are shown when duplicates are added.""" + conf = {CONF_LATITUDE: -41.2, CONF_LONGITUDE: 174.7, CONF_RADIUS: 25} + + config_entry.add_to_hass(hass) + flow = config_flow.GeonetnzVolcanoFlowHandler() + flow.hass = hass + + result = await flow.async_step_user(user_input=conf) + assert result["errors"] == {"base": "identifier_exists"} + + +async def test_show_form(hass): + """Test that the form is served with no input.""" + flow = config_flow.GeonetnzVolcanoFlowHandler() + flow.hass = hass + + result = await flow.async_step_user(user_input=None) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + +async def test_step_import(hass): + """Test that the import step works.""" + conf = { + CONF_LATITUDE: -41.2, + CONF_LONGITUDE: 174.7, + CONF_RADIUS: 25, + CONF_UNIT_SYSTEM: "metric", + CONF_SCAN_INTERVAL: timedelta(minutes=4), + } + + flow = config_flow.GeonetnzVolcanoFlowHandler() + flow.hass = hass + + result = await flow.async_step_import(import_config=conf) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "-41.2, 174.7" + assert result["data"] == { + CONF_LATITUDE: -41.2, + CONF_LONGITUDE: 174.7, + CONF_RADIUS: 25, + CONF_UNIT_SYSTEM: "metric", + CONF_SCAN_INTERVAL: 240.0, + } + + +async def test_step_user(hass): + """Test that the user step works.""" + hass.config.latitude = -41.2 + hass.config.longitude = 174.7 + conf = {CONF_RADIUS: 25} + + flow = config_flow.GeonetnzVolcanoFlowHandler() + flow.hass = hass + + result = await flow.async_step_user(user_input=conf) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "-41.2, 174.7" + assert result["data"] == { + CONF_LATITUDE: -41.2, + CONF_LONGITUDE: 174.7, + CONF_RADIUS: 25, + CONF_UNIT_SYSTEM: "metric", + CONF_SCAN_INTERVAL: 300.0, + } diff --git a/tests/components/geonetnz_volcano/test_init.py b/tests/components/geonetnz_volcano/test_init.py new file mode 100644 index 00000000000..3e2566ffb81 --- /dev/null +++ b/tests/components/geonetnz_volcano/test_init.py @@ -0,0 +1,22 @@ +"""Define tests for the GeoNet NZ Volcano general setup.""" +from asynctest import CoroutineMock, patch + +from homeassistant.components.geonetnz_volcano import DOMAIN, FEED + + +async def test_component_unload_config_entry(hass, config_entry): + """Test that loading and unloading of a config entry works.""" + config_entry.add_to_hass(hass) + with patch( + "aio_geojson_geonetnz_volcano.GeonetnzVolcanoFeedManager.update", + new_callable=CoroutineMock, + ) as mock_feed_manager_update: + # Load config entry. + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert mock_feed_manager_update.call_count == 1 + assert hass.data[DOMAIN][FEED][config_entry.entry_id] is not None + # Unload config entry. + assert await hass.config_entries.async_unload(config_entry.entry_id) + await hass.async_block_till_done() + assert hass.data[DOMAIN][FEED].get(config_entry.entry_id) is None diff --git a/tests/components/geonetnz_volcano/test_sensor.py b/tests/components/geonetnz_volcano/test_sensor.py new file mode 100644 index 00000000000..8f71e3c4757 --- /dev/null +++ b/tests/components/geonetnz_volcano/test_sensor.py @@ -0,0 +1,168 @@ +"""The tests for the GeoNet NZ Volcano Feed integration.""" +from asynctest import CoroutineMock, patch + +from homeassistant.components import geonetnz_volcano +from homeassistant.components.geo_location import ATTR_DISTANCE +from homeassistant.components.geonetnz_volcano import DEFAULT_SCAN_INTERVAL +from homeassistant.components.geonetnz_volcano.const import ( + ATTR_ACTIVITY, + ATTR_EXTERNAL_ID, + ATTR_HAZARDS, +) +from homeassistant.const import ( + ATTR_ATTRIBUTION, + ATTR_FRIENDLY_NAME, + ATTR_ICON, + ATTR_LATITUDE, + ATTR_LONGITUDE, + ATTR_UNIT_OF_MEASUREMENT, + CONF_RADIUS, + EVENT_HOMEASSISTANT_START, +) +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util +from homeassistant.util.unit_system import IMPERIAL_SYSTEM + +from tests.common import async_fire_time_changed +from tests.components.geonetnz_volcano import _generate_mock_feed_entry + +CONFIG = {geonetnz_volcano.DOMAIN: {CONF_RADIUS: 200}} + + +async def test_setup(hass): + """Test the general setup of the integration.""" + # Set up some mock feed entries for this test. + mock_entry_1 = _generate_mock_feed_entry( + "1234", + "Title 1", + 1, + 15.5, + (38.0, -3.0), + attribution="Attribution 1", + activity="Activity 1", + hazards="Hazards 1", + ) + mock_entry_2 = _generate_mock_feed_entry("2345", "Title 2", 0, 20.5, (38.1, -3.1)) + mock_entry_3 = _generate_mock_feed_entry("3456", "Title 3", 2, 25.5, (38.2, -3.2)) + mock_entry_4 = _generate_mock_feed_entry("4567", "Title 4", 1, 12.5, (38.3, -3.3)) + + # Patching 'utcnow' to gain more control over the timed update. + utcnow = dt_util.utcnow() + with patch("homeassistant.util.dt.utcnow", return_value=utcnow), patch( + "aio_geojson_client.feed.GeoJsonFeed.update", new_callable=CoroutineMock + ) as mock_feed_update: + mock_feed_update.return_value = "OK", [mock_entry_1, mock_entry_2, mock_entry_3] + assert await async_setup_component(hass, geonetnz_volcano.DOMAIN, CONFIG) + # Artificially trigger update and collect events. + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + all_states = hass.states.async_all() + # 3 sensor entities + assert len(all_states) == 3 + + state = hass.states.get("sensor.volcano_title_1") + assert state is not None + assert state.name == "Volcano Title 1" + assert int(state.state) == 1 + assert state.attributes[ATTR_EXTERNAL_ID] == "1234" + assert state.attributes[ATTR_LATITUDE] == 38.0 + assert state.attributes[ATTR_LONGITUDE] == -3.0 + assert state.attributes[ATTR_DISTANCE] == 15.5 + assert state.attributes[ATTR_FRIENDLY_NAME] == "Volcano Title 1" + assert state.attributes[ATTR_ATTRIBUTION] == "Attribution 1" + assert state.attributes[ATTR_ACTIVITY] == "Activity 1" + assert state.attributes[ATTR_HAZARDS] == "Hazards 1" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "alert level" + assert state.attributes[ATTR_ICON] == "mdi:image-filter-hdr" + + state = hass.states.get("sensor.volcano_title_2") + assert state is not None + assert state.name == "Volcano Title 2" + assert int(state.state) == 0 + assert state.attributes[ATTR_EXTERNAL_ID] == "2345" + assert state.attributes[ATTR_LATITUDE] == 38.1 + assert state.attributes[ATTR_LONGITUDE] == -3.1 + assert state.attributes[ATTR_DISTANCE] == 20.5 + assert state.attributes[ATTR_FRIENDLY_NAME] == "Volcano Title 2" + + state = hass.states.get("sensor.volcano_title_3") + assert state is not None + assert state.name == "Volcano Title 3" + assert int(state.state) == 2 + assert state.attributes[ATTR_EXTERNAL_ID] == "3456" + assert state.attributes[ATTR_LATITUDE] == 38.2 + assert state.attributes[ATTR_LONGITUDE] == -3.2 + assert state.attributes[ATTR_DISTANCE] == 25.5 + assert state.attributes[ATTR_FRIENDLY_NAME] == "Volcano Title 3" + + # Simulate an update - two existing, one new entry, one outdated entry + mock_feed_update.return_value = "OK", [mock_entry_1, mock_entry_4, mock_entry_3] + async_fire_time_changed(hass, utcnow + DEFAULT_SCAN_INTERVAL) + await hass.async_block_till_done() + + all_states = hass.states.async_all() + assert len(all_states) == 4 + + # Simulate an update - empty data, but successful update, + # so no changes to entities. + mock_feed_update.return_value = "OK_NO_DATA", None + async_fire_time_changed(hass, utcnow + 2 * DEFAULT_SCAN_INTERVAL) + await hass.async_block_till_done() + + all_states = hass.states.async_all() + assert len(all_states) == 4 + + # Simulate an update - empty data, keep all entities + mock_feed_update.return_value = "ERROR", None + async_fire_time_changed(hass, utcnow + 3 * DEFAULT_SCAN_INTERVAL) + await hass.async_block_till_done() + + all_states = hass.states.async_all() + assert len(all_states) == 4 + + # Simulate an update - regular data for 3 entries + mock_feed_update.return_value = "OK", [mock_entry_1, mock_entry_2, mock_entry_3] + async_fire_time_changed(hass, utcnow + 4 * DEFAULT_SCAN_INTERVAL) + await hass.async_block_till_done() + + all_states = hass.states.async_all() + assert len(all_states) == 4 + + +async def test_setup_imperial(hass): + """Test the setup of the integration using imperial unit system.""" + hass.config.units = IMPERIAL_SYSTEM + # Set up some mock feed entries for this test. + mock_entry_1 = _generate_mock_feed_entry("1234", "Title 1", 1, 15.5, (38.0, -3.0)) + + # Patching 'utcnow' to gain more control over the timed update. + utcnow = dt_util.utcnow() + with patch("homeassistant.util.dt.utcnow", return_value=utcnow), patch( + "aio_geojson_client.feed.GeoJsonFeed.update", new_callable=CoroutineMock + ) as mock_feed_update, patch( + "aio_geojson_client.feed.GeoJsonFeed.__init__" + ) as mock_feed_init: + mock_feed_update.return_value = "OK", [mock_entry_1] + assert await async_setup_component(hass, geonetnz_volcano.DOMAIN, CONFIG) + # Artificially trigger update and collect events. + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + all_states = hass.states.async_all() + assert len(all_states) == 1 + + # Test conversion of 200 miles to kilometers. + assert mock_feed_init.call_args[1].get("filter_radius") == 321.8688 + + state = hass.states.get("sensor.volcano_title_1") + assert state is not None + assert state.name == "Volcano Title 1" + assert int(state.state) == 1 + assert state.attributes[ATTR_EXTERNAL_ID] == "1234" + assert state.attributes[ATTR_LATITUDE] == 38.0 + assert state.attributes[ATTR_LONGITUDE] == -3.0 + assert state.attributes[ATTR_DISTANCE] == 9.6 + assert state.attributes[ATTR_FRIENDLY_NAME] == "Volcano Title 1" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "alert level" + assert state.attributes[ATTR_ICON] == "mdi:image-filter-hdr" diff --git a/tests/components/gios/__init__.py b/tests/components/gios/__init__.py new file mode 100644 index 00000000000..98528fda9f9 --- /dev/null +++ b/tests/components/gios/__init__.py @@ -0,0 +1 @@ +"""Tests for GIOS.""" diff --git a/tests/components/gios/test_config_flow.py b/tests/components/gios/test_config_flow.py new file mode 100644 index 00000000000..3a4aff6d9ad --- /dev/null +++ b/tests/components/gios/test_config_flow.py @@ -0,0 +1,104 @@ +"""Define tests for the GIOS config flow.""" +from asynctest import patch +from gios import ApiError + +from homeassistant import data_entry_flow +from homeassistant.components.gios import config_flow +from homeassistant.components.gios.const import CONF_STATION_ID +from homeassistant.const import CONF_NAME + +CONFIG = { + CONF_NAME: "Foo", + CONF_STATION_ID: 123, +} + +VALID_STATIONS = [ + {"id": 123, "stationName": "Test Name 1", "gegrLat": "99.99", "gegrLon": "88.88"}, + {"id": 321, "stationName": "Test Name 2", "gegrLat": "77.77", "gegrLon": "66.66"}, +] + +VALID_STATION = [ + {"id": 3764, "param": {"paramName": "particulate matter PM10", "paramCode": "PM10"}} +] + +VALID_INDEXES = { + "stIndexLevel": {"id": 1, "indexLevelName": "Good"}, + "pm10IndexLevel": {"id": 0, "indexLevelName": "Very good"}, +} + +VALID_SENSOR = {"key": "PM10", "values": [{"value": 11.11}]} + + +async def test_show_form(hass): + """Test that the form is served with no input.""" + flow = config_flow.GiosFlowHandler() + flow.hass = hass + + result = await flow.async_step_user(user_input=None) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + +async def test_invalid_station_id(hass): + """Test that errors are shown when measuring station ID is invalid.""" + with patch("gios.Gios._get_stations", return_value=VALID_STATIONS): + flow = config_flow.GiosFlowHandler() + flow.hass = hass + flow.context = {} + + result = await flow.async_step_user( + user_input={CONF_NAME: "Foo", CONF_STATION_ID: 0} + ) + + assert result["errors"] == {CONF_STATION_ID: "wrong_station_id"} + + +async def test_invalid_sensor_data(hass): + """Test that errors are shown when sensor data is invalid.""" + with patch("gios.Gios._get_stations", return_value=VALID_STATIONS), patch( + "gios.Gios._get_station", return_value=VALID_STATION + ), patch("gios.Gios._get_station", return_value=VALID_STATION), patch( + "gios.Gios._get_sensor", return_value={} + ): + flow = config_flow.GiosFlowHandler() + flow.hass = hass + flow.context = {} + + result = await flow.async_step_user(user_input=CONFIG) + + assert result["errors"] == {CONF_STATION_ID: "invalid_sensors_data"} + + +async def test_cannot_connect(hass): + """Test that errors are shown when cannot connect to GIOS server.""" + with patch("gios.Gios._async_get", side_effect=ApiError("error")): + flow = config_flow.GiosFlowHandler() + flow.hass = hass + flow.context = {} + + result = await flow.async_step_user(user_input=CONFIG) + + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_create_entry(hass): + """Test that the user step works.""" + with patch("gios.Gios._get_stations", return_value=VALID_STATIONS), patch( + "gios.Gios._get_station", return_value=VALID_STATION + ), patch("gios.Gios._get_station", return_value=VALID_STATION), patch( + "gios.Gios._get_sensor", return_value=VALID_SENSOR + ), patch( + "gios.Gios._get_indexes", return_value=VALID_INDEXES + ): + flow = config_flow.GiosFlowHandler() + flow.hass = hass + flow.context = {} + + result = await flow.async_step_user(user_input=CONFIG) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == CONFIG[CONF_STATION_ID] + assert result["data"][CONF_STATION_ID] == CONFIG[CONF_STATION_ID] + + assert flow.context["unique_id"] == CONFIG[CONF_STATION_ID] diff --git a/tests/components/google/test_calendar.py b/tests/components/google/test_calendar.py index 496570ca468..4aace6f5484 100644 --- a/tests/components/google/test_calendar.py +++ b/tests/components/google/test_calendar.py @@ -25,7 +25,6 @@ import homeassistant.util.dt as dt_util from tests.common import async_mock_service - GOOGLE_CONFIG = {CONF_CLIENT_ID: "client_id", CONF_CLIENT_SECRET: "client_secret"} TEST_ENTITY = "calendar.we_are_we_are_a_test_calendar" TEST_ENTITY_NAME = "We are, we are, a... Test Calendar" diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py index 09522e9c86f..db3b2e68f20 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -1,7 +1,19 @@ """Tests for the Google Assistant integration.""" +from asynctest.mock import MagicMock + from homeassistant.components.google_assistant import helpers +def mock_google_config_store(agent_user_ids=None): + """Fake a storage for google assistant.""" + store = MagicMock(spec=helpers.GoogleConfigStore) + if agent_user_ids is not None: + store.agent_user_ids = agent_user_ids + else: + store.agent_user_ids = {} + return store + + class MockConfig(helpers.AbstractConfig): """Fake config that always exposes everything.""" @@ -15,6 +27,7 @@ class MockConfig(helpers.AbstractConfig): local_sdk_webhook_id=None, local_sdk_user_id=None, enabled=True, + agent_user_ids=None, ): """Initialize config.""" super().__init__(hass) @@ -24,6 +37,7 @@ class MockConfig(helpers.AbstractConfig): self._local_sdk_webhook_id = local_sdk_webhook_id self._local_sdk_user_id = local_sdk_user_id self._enabled = enabled + self._store = mock_google_config_store(agent_user_ids) @property def enabled(self): diff --git a/tests/components/google_assistant/test_google_assistant.py b/tests/components/google_assistant/test_google_assistant.py index b43e913ab27..3be97013e4d 100644 --- a/tests/components/google_assistant/test_google_assistant.py +++ b/tests/components/google_assistant/test_google_assistant.py @@ -1,24 +1,23 @@ """The tests for the Google Assistant component.""" # pylint: disable=protected-access -import asyncio import json from aiohttp.hdrs import AUTHORIZATION import pytest -from homeassistant import core, const, setup +from homeassistant import const, core, setup from homeassistant.components import ( - fan, + alarm_control_panel, cover, + fan, + google_assistant as ga, light, - switch, lock, media_player, - alarm_control_panel, + switch, ) from homeassistant.components.climate import const as climate from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES -from homeassistant.components import google_assistant as ga from . import DEMO_DEVICES @@ -115,18 +114,17 @@ def hass_fixture(loop, hass): # pylint: disable=redefined-outer-name -@asyncio.coroutine -def test_sync_request(hass_fixture, assistant_client, auth_header): +async def test_sync_request(hass_fixture, assistant_client, auth_header): """Test a sync request.""" reqid = "5711642932632160983" data = {"requestId": reqid, "inputs": [{"intent": "action.devices.SYNC"}]} - result = yield from assistant_client.post( + result = await assistant_client.post( ga.const.GOOGLE_ASSISTANT_API_ENDPOINT, data=json.dumps(data), headers=auth_header, ) assert result.status == 200 - body = yield from result.json() + body = await result.json() assert body.get("requestId") == reqid devices = body["payload"]["devices"] assert sorted([dev["id"] for dev in devices]) == sorted( @@ -145,8 +143,7 @@ def test_sync_request(hass_fixture, assistant_client, auth_header): assert dev["type"] == demo["type"] -@asyncio.coroutine -def test_query_request(hass_fixture, assistant_client, auth_header): +async def test_query_request(hass_fixture, assistant_client, auth_header): """Test a query request.""" reqid = "5711642932632160984" data = { @@ -165,13 +162,13 @@ def test_query_request(hass_fixture, assistant_client, auth_header): } ], } - result = yield from assistant_client.post( + result = await assistant_client.post( ga.const.GOOGLE_ASSISTANT_API_ENDPOINT, data=json.dumps(data), headers=auth_header, ) assert result.status == 200 - body = yield from result.json() + body = await result.json() assert body.get("requestId") == reqid devices = body["payload"]["devices"] assert len(devices) == 4 @@ -187,8 +184,7 @@ def test_query_request(hass_fixture, assistant_client, auth_header): assert devices["media_player.lounge_room"]["on"] is True -@asyncio.coroutine -def test_query_climate_request(hass_fixture, assistant_client, auth_header): +async def test_query_climate_request(hass_fixture, assistant_client, auth_header): """Test a query request.""" reqid = "5711642932632160984" data = { @@ -206,13 +202,13 @@ def test_query_climate_request(hass_fixture, assistant_client, auth_header): } ], } - result = yield from assistant_client.post( + result = await assistant_client.post( ga.const.GOOGLE_ASSISTANT_API_ENDPOINT, data=json.dumps(data), headers=auth_header, ) assert result.status == 200 - body = yield from result.json() + body = await result.json() assert body.get("requestId") == reqid devices = body["payload"]["devices"] assert len(devices) == 3 @@ -238,8 +234,7 @@ def test_query_climate_request(hass_fixture, assistant_client, auth_header): } -@asyncio.coroutine -def test_query_climate_request_f(hass_fixture, assistant_client, auth_header): +async def test_query_climate_request_f(hass_fixture, assistant_client, auth_header): """Test a query request.""" # Mock demo devices as fahrenheit to see if we convert to celsius hass_fixture.config.units.temperature_unit = const.TEMP_FAHRENHEIT @@ -264,13 +259,13 @@ def test_query_climate_request_f(hass_fixture, assistant_client, auth_header): } ], } - result = yield from assistant_client.post( + result = await assistant_client.post( ga.const.GOOGLE_ASSISTANT_API_ENDPOINT, data=json.dumps(data), headers=auth_header, ) assert result.status == 200 - body = yield from result.json() + body = await result.json() assert body.get("requestId") == reqid devices = body["payload"]["devices"] assert len(devices) == 3 @@ -297,8 +292,7 @@ def test_query_climate_request_f(hass_fixture, assistant_client, auth_header): hass_fixture.config.units.temperature_unit = const.TEMP_CELSIUS -@asyncio.coroutine -def test_execute_request(hass_fixture, assistant_client, auth_header): +async def test_execute_request(hass_fixture, assistant_client, auth_header): """Test an execute request.""" reqid = "5711642932632160985" data = { @@ -357,13 +351,13 @@ def test_execute_request(hass_fixture, assistant_client, auth_header): } ], } - result = yield from assistant_client.post( + result = await assistant_client.post( ga.const.GOOGLE_ASSISTANT_API_ENDPOINT, data=json.dumps(data), headers=auth_header, ) assert result.status == 200 - body = yield from result.json() + body = await result.json() assert body.get("requestId") == reqid commands = body["payload"]["commands"] assert len(commands) == 6 diff --git a/tests/components/google_assistant/test_helpers.py b/tests/components/google_assistant/test_helpers.py index 497b7b1f0ae..9c8a868e68d 100644 --- a/tests/components/google_assistant/test_helpers.py +++ b/tests/components/google_assistant/test_helpers.py @@ -1,11 +1,23 @@ """Test Google Assistant helpers.""" -from unittest.mock import Mock -from homeassistant.setup import async_setup_component +from datetime import timedelta + +from asynctest.mock import Mock, call, patch +import pytest + from homeassistant.components.google_assistant import helpers -from homeassistant.components.google_assistant.const import EVENT_COMMAND_RECEIVED +from homeassistant.components.google_assistant.const import ( # noqa: F401 + EVENT_COMMAND_RECEIVED, +) +from homeassistant.setup import async_setup_component +from homeassistant.util import dt + from . import MockConfig -from tests.common import async_capture_events, async_mock_service +from tests.common import ( + async_capture_events, + async_fire_time_changed, + async_mock_service, +) async def test_google_entity_sync_serialize_with_local_sdk(hass): @@ -19,13 +31,13 @@ async def test_google_entity_sync_serialize_with_local_sdk(hass): ) entity = helpers.GoogleEntity(hass, config, hass.states.get("light.ceiling_lights")) - serialized = await entity.sync_serialize() + serialized = await entity.sync_serialize(None) assert "otherDeviceIds" not in serialized assert "customData" not in serialized config.async_enable_local_sdk() - serialized = await entity.sync_serialize() + serialized = await entity.sync_serialize(None) assert serialized["otherDeviceIds"] == [{"deviceId": "light.ceiling_lights"}] assert serialized["customData"] == { "httpPort": 1234, @@ -128,3 +140,84 @@ async def test_config_local_sdk_if_disabled(hass, hass_client): resp = await client.post("/api/webhook/mock-webhook-id") assert resp.status == 200 assert await resp.read() == b"" + + +async def test_agent_user_id_storage(hass, hass_storage): + """Test a disconnect message.""" + + hass_storage["google_assistant"] = { + "version": 1, + "key": "google_assistant", + "data": {"agent_user_ids": {"agent_1": {}}}, + } + + store = helpers.GoogleConfigStore(hass) + await store.async_load() + + assert hass_storage["google_assistant"] == { + "version": 1, + "key": "google_assistant", + "data": {"agent_user_ids": {"agent_1": {}}}, + } + + async def _check_after_delay(data): + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=2)) + await hass.async_block_till_done() + + assert hass_storage["google_assistant"] == { + "version": 1, + "key": "google_assistant", + "data": data, + } + + store.add_agent_user_id("agent_2") + await _check_after_delay({"agent_user_ids": {"agent_1": {}, "agent_2": {}}}) + + store.pop_agent_user_id("agent_1") + await _check_after_delay({"agent_user_ids": {"agent_2": {}}}) + + +async def test_agent_user_id_connect(): + """Test the connection and disconnection of users.""" + config = MockConfig() + store = config._store + + await config.async_connect_agent_user("agent_2") + assert store.add_agent_user_id.call_args == call("agent_2") + + await config.async_connect_agent_user("agent_1") + assert store.add_agent_user_id.call_args == call("agent_1") + + await config.async_disconnect_agent_user("agent_2") + assert store.pop_agent_user_id.call_args == call("agent_2") + + await config.async_disconnect_agent_user("agent_1") + assert store.pop_agent_user_id.call_args == call("agent_1") + + +@pytest.mark.parametrize("agents", [{}, {"1"}, {"1", "2"}]) +async def test_report_state_all(agents): + """Test a disconnect message.""" + config = MockConfig(agent_user_ids=agents) + data = {} + with patch.object(config, "async_report_state") as mock: + await config.async_report_state_all(data) + assert sorted(mock.mock_calls) == sorted( + [call(data, agent) for agent in agents] + ) + + +@pytest.mark.parametrize( + "agents, result", [({}, 204), ({"1": 200}, 200), ({"1": 200, "2": 300}, 300)], +) +async def test_sync_entities_all(agents, result): + """Test sync entities .""" + config = MockConfig(agent_user_ids=set(agents.keys())) + with patch.object( + config, + "async_sync_entities", + side_effect=lambda agent_user_id: agents[agent_user_id], + ) as mock: + res = await config.async_sync_entities_all() + assert sorted(mock.mock_calls) == sorted([call(agent) for agent in agents]) + assert res == result diff --git a/tests/components/google_assistant/test_http.py b/tests/components/google_assistant/test_http.py index 4b26bbeba7f..112935f0160 100644 --- a/tests/components/google_assistant/test_http.py +++ b/tests/components/google_assistant/test_http.py @@ -1,18 +1,18 @@ """Test Google http services.""" -from datetime import datetime, timezone, timedelta -from asynctest import patch, ANY +from datetime import datetime, timedelta, timezone +from asynctest import ANY, patch + +from homeassistant.components.google_assistant import GOOGLE_ASSISTANT_SCHEMA +from homeassistant.components.google_assistant.const import ( + HOMEGRAPH_TOKEN_URL, + REPORT_STATE_BASE_URL, +) from homeassistant.components.google_assistant.http import ( GoogleConfig, _get_homegraph_jwt, _get_homegraph_token, ) -from homeassistant.components.google_assistant import GOOGLE_ASSISTANT_SCHEMA -from homeassistant.components.google_assistant.const import ( - REPORT_STATE_BASE_URL, - HOMEGRAPH_TOKEN_URL, -) -from homeassistant.auth.models import User DUMMY_CONFIG = GOOGLE_ASSISTANT_SCHEMA( { @@ -67,6 +67,7 @@ async def test_update_access_token(hass): jwt = "dummyjwt" config = GoogleConfig(hass, DUMMY_CONFIG) + await config.async_initialize() base_time = datetime(2019, 10, 14, tzinfo=timezone.utc) with patch( @@ -99,6 +100,8 @@ async def test_update_access_token(hass): async def test_call_homegraph_api(hass, aioclient_mock, hass_storage): """Test the function to call the homegraph api.""" config = GoogleConfig(hass, DUMMY_CONFIG) + await config.async_initialize() + with patch( "homeassistant.components.google_assistant.http._get_homegraph_token" ) as mock_get_token: @@ -106,7 +109,8 @@ async def test_call_homegraph_api(hass, aioclient_mock, hass_storage): aioclient_mock.post(MOCK_URL, status=200, json={}) - await config.async_call_homegraph_api(MOCK_URL, MOCK_JSON) + res = await config.async_call_homegraph_api(MOCK_URL, MOCK_JSON) + assert res == 200 assert mock_get_token.call_count == 1 assert aioclient_mock.call_count == 1 @@ -119,6 +123,8 @@ async def test_call_homegraph_api(hass, aioclient_mock, hass_storage): async def test_call_homegraph_api_retry(hass, aioclient_mock, hass_storage): """Test the that the calls get retried with new token on 401.""" config = GoogleConfig(hass, DUMMY_CONFIG) + await config.async_initialize() + with patch( "homeassistant.components.google_assistant.http._get_homegraph_token" ) as mock_get_token: @@ -139,19 +145,50 @@ async def test_call_homegraph_api_retry(hass, aioclient_mock, hass_storage): assert call[3] == MOCK_HEADER +async def test_call_homegraph_api_key(hass, aioclient_mock, hass_storage): + """Test the function to call the homegraph api.""" + config = GoogleConfig( + hass, GOOGLE_ASSISTANT_SCHEMA({"project_id": "1234", "api_key": "dummy_key"}), + ) + await config.async_initialize() + + aioclient_mock.post(MOCK_URL, status=200, json={}) + + res = await config.async_call_homegraph_api_key(MOCK_URL, MOCK_JSON) + assert res == 200 + assert aioclient_mock.call_count == 1 + + call = aioclient_mock.mock_calls[0] + assert call[1].query == {"key": "dummy_key"} + assert call[2] == MOCK_JSON + + +async def test_call_homegraph_api_key_fail(hass, aioclient_mock, hass_storage): + """Test the function to call the homegraph api.""" + config = GoogleConfig( + hass, GOOGLE_ASSISTANT_SCHEMA({"project_id": "1234", "api_key": "dummy_key"}), + ) + await config.async_initialize() + + aioclient_mock.post(MOCK_URL, status=666, json={}) + + res = await config.async_call_homegraph_api_key(MOCK_URL, MOCK_JSON) + assert res == 666 + assert aioclient_mock.call_count == 1 + + async def test_report_state(hass, aioclient_mock, hass_storage): """Test the report state function.""" + agent_user_id = "user" config = GoogleConfig(hass, DUMMY_CONFIG) + await config.async_initialize() + + await config.async_connect_agent_user(agent_user_id) message = {"devices": {}} - owner = User(name="Test User", perm_lookup=None, groups=[], is_owner=True) - with patch.object(config, "async_call_homegraph_api") as mock_call, patch.object( - hass.auth, "async_get_owner" - ) as mock_get_owner: - mock_get_owner.return_value = owner - - await config.async_report_state(message) + with patch.object(config, "async_call_homegraph_api") as mock_call: + await config.async_report_state(message, agent_user_id) mock_call.assert_called_once_with( REPORT_STATE_BASE_URL, - {"requestId": ANY, "agentUserId": owner.id, "payload": message}, + {"requestId": ANY, "agentUserId": agent_user_id, "payload": message}, ) diff --git a/tests/components/google_assistant/test_init.py b/tests/components/google_assistant/test_init.py index 9a8b9643cfe..2773f3c3329 100644 --- a/tests/components/google_assistant/test_init.py +++ b/tests/components/google_assistant/test_init.py @@ -1,26 +1,23 @@ """The tests for google-assistant init.""" -import asyncio - +from homeassistant.components import google_assistant as ga from homeassistant.core import Context from homeassistant.setup import async_setup_component -from homeassistant.components import google_assistant as ga GA_API_KEY = "Agdgjsj399sdfkosd932ksd" -@asyncio.coroutine -def test_request_sync_service(aioclient_mock, hass): +async def test_request_sync_service(aioclient_mock, hass): """Test that it posts to the request_sync url.""" aioclient_mock.post(ga.const.REQUEST_SYNC_BASE_URL, status=200) - yield from async_setup_component( + await async_setup_component( hass, "google_assistant", {"google_assistant": {"project_id": "test_project", "api_key": GA_API_KEY}}, ) assert aioclient_mock.call_count == 0 - yield from hass.services.async_call( + await hass.services.async_call( ga.const.DOMAIN, ga.const.SERVICE_REQUEST_SYNC, blocking=True, diff --git a/tests/components/google_assistant/test_report_state.py b/tests/components/google_assistant/test_report_state.py index 6ab88286a69..fd9cad27ffa 100644 --- a/tests/components/google_assistant/test_report_state.py +++ b/tests/components/google_assistant/test_report_state.py @@ -1,13 +1,12 @@ """Test Google report state.""" from unittest.mock import patch -from homeassistant.components.google_assistant import report_state, error +from homeassistant.components.google_assistant import error, report_state from homeassistant.util.dt import utcnow from . import BASIC_CONFIG - -from tests.common import mock_coro, async_fire_time_changed +from tests.common import async_fire_time_changed, mock_coro async def test_report_state(hass, caplog): @@ -16,7 +15,7 @@ async def test_report_state(hass, caplog): hass.states.async_set("switch.ac", "on") with patch.object( - BASIC_CONFIG, "async_report_state", side_effect=mock_coro + BASIC_CONFIG, "async_report_state_all", side_effect=mock_coro ) as mock_report, patch.object(report_state, "INITIAL_REPORT_DELAY", 0): unsub = report_state.async_enable_report_state(hass, BASIC_CONFIG) @@ -35,7 +34,7 @@ async def test_report_state(hass, caplog): } with patch.object( - BASIC_CONFIG, "async_report_state", side_effect=mock_coro + BASIC_CONFIG, "async_report_state_all", side_effect=mock_coro ) as mock_report: hass.states.async_set("light.kitchen", "on") await hass.async_block_till_done() @@ -48,7 +47,7 @@ async def test_report_state(hass, caplog): # Test that state changes that change something that Google doesn't care about # do not trigger a state report. with patch.object( - BASIC_CONFIG, "async_report_state", side_effect=mock_coro + BASIC_CONFIG, "async_report_state_all", side_effect=mock_coro ) as mock_report: hass.states.async_set( "light.kitchen", "on", {"irrelevant": "should_be_ignored"} @@ -59,7 +58,7 @@ async def test_report_state(hass, caplog): # Test that entities that we can't query don't report a state with patch.object( - BASIC_CONFIG, "async_report_state", side_effect=mock_coro + BASIC_CONFIG, "async_report_state_all", side_effect=mock_coro ) as mock_report, patch( "homeassistant.components.google_assistant.report_state.GoogleEntity.query_serialize", side_effect=error.SmartHomeError("mock-error", "mock-msg"), @@ -73,7 +72,7 @@ async def test_report_state(hass, caplog): unsub() with patch.object( - BASIC_CONFIG, "async_report_state", side_effect=mock_coro + BASIC_CONFIG, "async_report_state_all", side_effect=mock_coro ) as mock_report: hass.states.async_set("light.kitchen", "on") await hass.async_block_till_done() diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 250d611b602..7ffe9cda477 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -1,40 +1,41 @@ """Test Google Smart Home.""" -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch + import pytest -from homeassistant.core import State, EVENT_CALL_SERVICE -from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, __version__ -from homeassistant.setup import async_setup_component from homeassistant.components import camera from homeassistant.components.climate.const import ( - ATTR_MIN_TEMP, ATTR_MAX_TEMP, + ATTR_MIN_TEMP, HVAC_MODE_HEAT, ) -from homeassistant.components.google_assistant import ( - const, - trait, - smart_home as sh, - EVENT_COMMAND_RECEIVED, - EVENT_QUERY_RECEIVED, - EVENT_SYNC_RECEIVED, -) from homeassistant.components.demo.binary_sensor import DemoBinarySensor from homeassistant.components.demo.cover import DemoCover from homeassistant.components.demo.light import DemoLight from homeassistant.components.demo.media_player import AbstractDemoPlayer from homeassistant.components.demo.switch import DemoSwitch - -from homeassistant.helpers import device_registry -from tests.common import ( - mock_device_registry, - mock_registry, - mock_area_registry, - mock_coro, +from homeassistant.components.google_assistant import ( + EVENT_COMMAND_RECEIVED, + EVENT_QUERY_RECEIVED, + EVENT_SYNC_RECEIVED, + const, + smart_home as sh, + trait, ) +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, __version__ +from homeassistant.core import EVENT_CALL_SERVICE, State +from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component from . import BASIC_CONFIG, MockConfig +from tests.common import ( + mock_area_registry, + mock_coro, + mock_device_registry, + mock_registry, +) + REQ_ID = "ff36a3cc-ec34-11e6-b1a0-64510650abcf" @@ -455,7 +456,7 @@ async def test_serialize_input_boolean(hass): state = State("input_boolean.bla", "on") # pylint: disable=protected-access entity = sh.GoogleEntity(hass, BASIC_CONFIG, state) - result = await entity.sync_serialize() + result = await entity.sync_serialize(None) assert result == { "id": "input_boolean.bla", "attributes": {}, @@ -466,14 +467,17 @@ async def test_serialize_input_boolean(hass): } -async def test_unavailable_state_doesnt_sync(hass): - """Test that an unavailable entity does not sync over.""" - light = DemoLight(None, "Demo Light", state=False) +async def test_unavailable_state_does_sync(hass): + """Test that an unavailable entity does sync over.""" + light = DemoLight(None, "Demo Light", state=False, hs_color=(180, 75)) light.hass = hass light.entity_id = "light.demo_light" light._available = False # pylint: disable=protected-access await light.async_update_ha_state() + events = [] + hass.bus.async_listen(EVENT_SYNC_RECEIVED, events.append) + result = await sh.async_handle_message( hass, BASIC_CONFIG, @@ -483,8 +487,35 @@ async def test_unavailable_state_doesnt_sync(hass): assert result == { "requestId": REQ_ID, - "payload": {"agentUserId": "test-agent", "devices": []}, + "payload": { + "agentUserId": "test-agent", + "devices": [ + { + "id": "light.demo_light", + "name": {"name": "Demo Light"}, + "traits": [ + trait.TRAIT_BRIGHTNESS, + trait.TRAIT_ONOFF, + trait.TRAIT_COLOR_SETTING, + ], + "type": const.TYPE_LIGHT, + "willReportState": False, + "attributes": { + "colorModel": "hsv", + "colorTemperatureRange": { + "temperatureMinK": 2000, + "temperatureMaxK": 6535, + }, + }, + } + ], + }, } + await hass.async_block_till_done() + + assert len(events) == 1 + assert events[0].event_type == EVENT_SYNC_RECEIVED + assert events[0].data == {"request_id": REQ_ID} @pytest.mark.parametrize( @@ -664,8 +695,8 @@ async def test_query_disconnect(hass): config.async_enable_report_state() assert config._unsub_report_state is not None with patch.object( - config, "async_deactivate_report_state", side_effect=mock_coro - ) as mock_deactivate: + config, "async_disconnect_agent_user", side_effect=mock_coro + ) as mock_disconnect: result = await sh.async_handle_message( hass, config, @@ -673,7 +704,7 @@ async def test_query_disconnect(hass): {"inputs": [{"intent": "action.devices.DISCONNECT"}], "requestId": REQ_ID}, ) assert result is None - assert len(mock_deactivate.mock_calls) == 1 + assert len(mock_disconnect.mock_calls) == 1 async def test_trait_execute_adding_query_data(hass): @@ -741,10 +772,12 @@ async def test_trait_execute_adding_query_data(hass): async def test_identify(hass): """Test identify message.""" + user_agent_id = "mock-user-id" + proxy_device_id = user_agent_id result = await sh.async_handle_message( hass, BASIC_CONFIG, - None, + user_agent_id, { "requestId": REQ_ID, "inputs": [ @@ -778,7 +811,7 @@ async def test_identify(hass): "customData": { "httpPort": 8123, "httpSSL": False, - "proxyDeviceId": BASIC_CONFIG.agent_user_id, + "proxyDeviceId": proxy_device_id, "webhookId": "dde3b9800a905e886cc4d38e226a6e7e3f2a6993d2b9b9f63d13e42ee7de3219", }, } @@ -790,7 +823,7 @@ async def test_identify(hass): "requestId": REQ_ID, "payload": { "device": { - "id": BASIC_CONFIG.agent_user_id, + "id": proxy_device_id, "isLocalOnly": True, "isProxy": True, "deviceInfo": { @@ -822,10 +855,13 @@ async def test_reachable_devices(hass): should_expose=lambda state: state.entity_id != "light.not_expose" ) + user_agent_id = "mock-user-id" + proxy_device_id = user_agent_id + result = await sh.async_handle_message( hass, config, - None, + user_agent_id, { "requestId": REQ_ID, "inputs": [ @@ -834,7 +870,7 @@ async def test_reachable_devices(hass): "payload": { "device": { "proxyDevice": { - "id": "6a04f0f7-6125-4356-a846-861df7e01497", + "id": proxy_device_id, "customData": "{}", "proxyData": "{}", } @@ -849,7 +885,7 @@ async def test_reachable_devices(hass): "customData": { "httpPort": 8123, "httpSSL": False, - "proxyDeviceId": BASIC_CONFIG.agent_user_id, + "proxyDeviceId": proxy_device_id, "webhookId": "dde3b9800a905e886cc4d38e226a6e7e3f2a6993d2b9b9f63d13e42ee7de3219", }, }, @@ -858,11 +894,11 @@ async def test_reachable_devices(hass): "customData": { "httpPort": 8123, "httpSSL": False, - "proxyDeviceId": BASIC_CONFIG.agent_user_id, + "proxyDeviceId": proxy_device_id, "webhookId": "dde3b9800a905e886cc4d38e226a6e7e3f2a6993d2b9b9f63d13e42ee7de3219", }, }, - {"id": BASIC_CONFIG.agent_user_id, "customData": {}}, + {"id": proxy_device_id, "customData": {}}, ], }, ) diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index d6ec24a7867..98e5149de1d 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -1,13 +1,16 @@ """Tests for the Google Assistant traits.""" -from unittest.mock import patch, Mock import logging +from unittest.mock import Mock, patch + import pytest from homeassistant.components import ( + alarm_control_panel, binary_sensor, camera, cover, fan, + group, input_boolean, light, lock, @@ -17,33 +20,33 @@ from homeassistant.components import ( sensor, switch, vacuum, - group, - alarm_control_panel, ) from homeassistant.components.climate import const as climate -from homeassistant.components.google_assistant import trait, helpers, const, error +from homeassistant.components.google_assistant import const, error, helpers, trait from homeassistant.const import ( - STATE_ON, - STATE_OFF, + ATTR_ASSUMED_STATE, + ATTR_DEVICE_CLASS, + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + ATTR_TEMPERATURE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, STATE_ALARM_ARMED_AWAY, STATE_ALARM_DISARMED, STATE_ALARM_PENDING, - ATTR_ENTITY_ID, - SERVICE_TURN_ON, - SERVICE_TURN_OFF, + STATE_OFF, + STATE_ON, + STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT, - ATTR_SUPPORTED_FEATURES, - ATTR_TEMPERATURE, - ATTR_DEVICE_CLASS, - ATTR_ASSUMED_STATE, - STATE_UNKNOWN, ) -from homeassistant.core import State, DOMAIN as HA_DOMAIN, EVENT_CALL_SERVICE +from homeassistant.core import DOMAIN as HA_DOMAIN, EVENT_CALL_SERVICE, State from homeassistant.util import color -from tests.common import async_mock_service, mock_coro + from . import BASIC_CONFIG, MockConfig +from tests.common import async_mock_service, mock_coro + _LOGGER = logging.getLogger(__name__) REQ_ID = "ff36a3cc-ec34-11e6-b1a0-64510650abcf" @@ -1612,20 +1615,70 @@ async def test_temperature_setting_sensor(hass): sensor.DOMAIN, 0, sensor.DEVICE_CLASS_TEMPERATURE ) - hass.config.units.temperature_unit = TEMP_FAHRENHEIT + +@pytest.mark.parametrize( + "unit_in,unit_out,state,ambient", + [ + (TEMP_FAHRENHEIT, "F", "70", 21.1), + (TEMP_CELSIUS, "C", "21.1", 21.1), + (TEMP_FAHRENHEIT, "F", "unavailable", None), + (TEMP_FAHRENHEIT, "F", "unknown", None), + ], +) +async def test_temperature_setting_sensor_data(hass, unit_in, unit_out, state, ambient): + """Test TemperatureSetting trait support for temperature sensor.""" + hass.config.units.temperature_unit = unit_in trt = trait.TemperatureSettingTrait( hass, State( - "sensor.test", "70", {ATTR_DEVICE_CLASS: sensor.DEVICE_CLASS_TEMPERATURE} + "sensor.test", state, {ATTR_DEVICE_CLASS: sensor.DEVICE_CLASS_TEMPERATURE} ), BASIC_CONFIG, ) assert trt.sync_attributes() == { "queryOnlyTemperatureSetting": True, - "thermostatTemperatureUnit": "F", + "thermostatTemperatureUnit": unit_out, } - assert trt.query_attributes() == {"thermostatTemperatureAmbient": 21.1} + if ambient: + assert trt.query_attributes() == {"thermostatTemperatureAmbient": ambient} + else: + assert trt.query_attributes() == {} hass.config.units.temperature_unit = TEMP_CELSIUS + + +async def test_humidity_setting_sensor(hass): + """Test HumiditySetting trait support for humidity sensor.""" + assert ( + helpers.get_google_type(sensor.DOMAIN, sensor.DEVICE_CLASS_HUMIDITY) is not None + ) + assert not trait.HumiditySettingTrait.supported( + sensor.DOMAIN, 0, sensor.DEVICE_CLASS_TEMPERATURE + ) + assert trait.HumiditySettingTrait.supported( + sensor.DOMAIN, 0, sensor.DEVICE_CLASS_HUMIDITY + ) + + +@pytest.mark.parametrize( + "state,ambient", [("70", 70), ("unavailable", None), ("unknown", None)] +) +async def test_humidity_setting_sensor_data(hass, state, ambient): + """Test HumiditySetting trait support for humidity sensor.""" + trt = trait.HumiditySettingTrait( + hass, + State("sensor.test", state, {ATTR_DEVICE_CLASS: sensor.DEVICE_CLASS_HUMIDITY}), + BASIC_CONFIG, + ) + + assert trt.sync_attributes() == {"queryOnlyHumiditySetting": True} + if ambient: + assert trt.query_attributes() == {"humidityAmbientPercent": ambient} + else: + assert trt.query_attributes() == {} + + with pytest.raises(helpers.SmartHomeError) as err: + await trt.execute(trait.COMMAND_ONOFF, BASIC_DATA, {"on": False}, {}) + assert err.value.code == const.ERR_NOT_SUPPORTED diff --git a/tests/components/google_domains/test_init.py b/tests/components/google_domains/test_init.py index 1334e46b96f..66e334d342f 100644 --- a/tests/components/google_domains/test_init.py +++ b/tests/components/google_domains/test_init.py @@ -1,11 +1,10 @@ """Test the Google Domains component.""" -import asyncio from datetime import timedelta import pytest -from homeassistant.setup import async_setup_component from homeassistant.components import google_domains +from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow from tests.common import async_fire_time_changed @@ -37,12 +36,11 @@ def setup_google_domains(hass, aioclient_mock): ) -@asyncio.coroutine -def test_setup(hass, aioclient_mock): +async def test_setup(hass, aioclient_mock): """Test setup works if update passes.""" aioclient_mock.get(UPDATE_URL, params={"hostname": DOMAIN}, text="nochg 0.0.0.0") - result = yield from async_setup_component( + result = await async_setup_component( hass, google_domains.DOMAIN, { @@ -57,16 +55,15 @@ def test_setup(hass, aioclient_mock): assert aioclient_mock.call_count == 1 async_fire_time_changed(hass, utcnow() + timedelta(minutes=5)) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert aioclient_mock.call_count == 2 -@asyncio.coroutine -def test_setup_fails_if_update_fails(hass, aioclient_mock): +async def test_setup_fails_if_update_fails(hass, aioclient_mock): """Test setup fails if first update fails.""" aioclient_mock.get(UPDATE_URL, params={"hostname": DOMAIN}, text="nohost") - result = yield from async_setup_component( + result = await async_setup_component( hass, google_domains.DOMAIN, { diff --git a/tests/components/google_translate/test_tts.py b/tests/components/google_translate/test_tts.py index 13f9eb88fce..15e84b384c0 100644 --- a/tests/components/google_translate/test_tts.py +++ b/tests/components/google_translate/test_tts.py @@ -4,16 +4,15 @@ import os import shutil from unittest.mock import patch -import homeassistant.components.tts as tts from homeassistant.components.media_player.const import ( - SERVICE_PLAY_MEDIA, ATTR_MEDIA_CONTENT_ID, DOMAIN as DOMAIN_MP, + SERVICE_PLAY_MEDIA, ) +import homeassistant.components.tts as tts from homeassistant.setup import setup_component -from tests.common import get_test_home_assistant, assert_setup_component, mock_service - +from tests.common import assert_setup_component, get_test_home_assistant, mock_service from tests.components.tts.test_init import mutagen_mock # noqa: F401 diff --git a/tests/components/google_wifi/test_sensor.py b/tests/components/google_wifi/test_sensor.py index a9883c6be66..8a529f93f72 100644 --- a/tests/components/google_wifi/test_sensor.py +++ b/tests/components/google_wifi/test_sensor.py @@ -1,17 +1,17 @@ """The tests for the Google Wifi platform.""" -import unittest -from unittest.mock import patch, Mock from datetime import datetime, timedelta +import unittest +from unittest.mock import Mock, patch import requests_mock from homeassistant import core as ha -from homeassistant.setup import setup_component import homeassistant.components.google_wifi.sensor as google_wifi from homeassistant.const import STATE_UNKNOWN +from homeassistant.setup import setup_component from homeassistant.util import dt as dt_util -from tests.common import get_test_home_assistant, assert_setup_component +from tests.common import assert_setup_component, get_test_home_assistant NAME = "foo" diff --git a/tests/components/graphite/test_init.py b/tests/components/graphite/test_init.py index f6484f08245..dd60f03cd58 100644 --- a/tests/components/graphite/test_init.py +++ b/tests/components/graphite/test_init.py @@ -4,16 +4,17 @@ import unittest from unittest import mock from unittest.mock import patch -from homeassistant.setup import setup_component -import homeassistant.core as ha import homeassistant.components.graphite as graphite from homeassistant.const import ( - EVENT_STATE_CHANGED, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, - STATE_ON, + EVENT_STATE_CHANGED, STATE_OFF, + STATE_ON, ) +import homeassistant.core as ha +from homeassistant.setup import setup_component + from tests.common import get_test_home_assistant diff --git a/tests/components/group/test_cover.py b/tests/components/group/test_cover.py index e687f105e20..42345536120 100644 --- a/tests/components/group/test_cover.py +++ b/tests/components/group/test_cover.py @@ -19,16 +19,16 @@ from homeassistant.const import ( CONF_ENTITIES, SERVICE_CLOSE_COVER, SERVICE_CLOSE_COVER_TILT, - SERVICE_TOGGLE, - SERVICE_TOGGLE_COVER_TILT, SERVICE_OPEN_COVER, SERVICE_OPEN_COVER_TILT, SERVICE_SET_COVER_POSITION, SERVICE_SET_COVER_TILT_POSITION, SERVICE_STOP_COVER, SERVICE_STOP_COVER_TILT, - STATE_OPEN, + SERVICE_TOGGLE, + SERVICE_TOGGLE_COVER_TILT, STATE_CLOSED, + STATE_OPEN, ) from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util diff --git a/tests/components/group/test_init.py b/tests/components/group/test_init.py index 44f5448ec8f..5c826dbe85d 100644 --- a/tests/components/group/test_init.py +++ b/tests/components/group/test_init.py @@ -5,21 +5,21 @@ from collections import OrderedDict import unittest from unittest.mock import patch -from homeassistant.setup import setup_component, async_setup_component -from homeassistant.const import ( - STATE_ON, - STATE_OFF, - STATE_HOME, - STATE_UNKNOWN, - ATTR_ICON, - ATTR_HIDDEN, - ATTR_ASSUMED_STATE, - STATE_NOT_HOME, - ATTR_FRIENDLY_NAME, -) import homeassistant.components.group as group +from homeassistant.const import ( + ATTR_ASSUMED_STATE, + ATTR_FRIENDLY_NAME, + ATTR_HIDDEN, + ATTR_ICON, + STATE_HOME, + STATE_NOT_HOME, + STATE_OFF, + STATE_ON, + STATE_UNKNOWN, +) +from homeassistant.setup import async_setup_component, setup_component -from tests.common import get_test_home_assistant, assert_setup_component +from tests.common import assert_setup_component, get_test_home_assistant from tests.components.group import common diff --git a/tests/components/group/test_notify.py b/tests/components/group/test_notify.py index d7b7496573b..f029ec9d2fa 100644 --- a/tests/components/group/test_notify.py +++ b/tests/components/group/test_notify.py @@ -3,10 +3,10 @@ import asyncio import unittest from unittest.mock import MagicMock, patch -from homeassistant.setup import setup_component -import homeassistant.components.notify as notify -import homeassistant.components.group.notify as group import homeassistant.components.demo.notify as demo +import homeassistant.components.group.notify as group +import homeassistant.components.notify as notify +from homeassistant.setup import setup_component from tests.common import assert_setup_component, get_test_home_assistant diff --git a/tests/components/group/test_reproduce_state.py b/tests/components/group/test_reproduce_state.py index 502ea9e51fc..5cc1c862cd3 100644 --- a/tests/components/group/test_reproduce_state.py +++ b/tests/components/group/test_reproduce_state.py @@ -2,6 +2,7 @@ from asyncio import Future from unittest.mock import patch + from homeassistant.components.group.reproduce_state import async_reproduce_states from homeassistant.core import Context, State @@ -21,7 +22,9 @@ async def test_reproduce_group(hass): context=state.context, ) - with patch("homeassistant.helpers.state.async_reproduce_state") as fun: + with patch( + "homeassistant.components.group.reproduce_state.async_reproduce_state" + ) as fun: fun.return_value = Future() fun.return_value.set_result(None) diff --git a/tests/components/hangouts/test_config_flow.py b/tests/components/hangouts/test_config_flow.py index 29585db5f61..93f909d3bd4 100644 --- a/tests/components/hangouts/test_config_flow.py +++ b/tests/components/hangouts/test_config_flow.py @@ -4,6 +4,10 @@ from unittest.mock import patch from homeassistant import data_entry_flow from homeassistant.components.hangouts import config_flow +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD + +EMAIL = "test@test.com" +PASSWORD = "1232456" async def test_flow_works(hass, aioclient_mock): @@ -12,12 +16,12 @@ async def test_flow_works(hass, aioclient_mock): flow.hass = hass - with patch("hangups.get_auth"): + with patch("homeassistant.components.hangouts.config_flow.get_auth"): result = await flow.async_step_user( - {"email": "test@test.com", "password": "1232456"} + {CONF_EMAIL: EMAIL, CONF_PASSWORD: PASSWORD} ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "test@test.com" + assert result["title"] == EMAIL async def test_flow_works_with_authcode(hass, aioclient_mock): @@ -26,16 +30,16 @@ async def test_flow_works_with_authcode(hass, aioclient_mock): flow.hass = hass - with patch("hangups.get_auth"): + with patch("homeassistant.components.hangouts.config_flow.get_auth"): result = await flow.async_step_user( { - "email": "test@test.com", - "password": "1232456", + CONF_EMAIL: EMAIL, + CONF_PASSWORD: PASSWORD, "authorization_code": "c29tZXJhbmRvbXN0cmluZw==", } ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "test@test.com" + assert result["title"] == EMAIL async def test_flow_works_with_2fa(hass, aioclient_mock): @@ -46,17 +50,20 @@ async def test_flow_works_with_2fa(hass, aioclient_mock): flow.hass = hass - with patch("hangups.get_auth", side_effect=Google2FAError): + with patch( + "homeassistant.components.hangouts.config_flow.get_auth", + side_effect=Google2FAError, + ): result = await flow.async_step_user( - {"email": "test@test.com", "password": "1232456"} + {CONF_EMAIL: EMAIL, CONF_PASSWORD: PASSWORD} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "2fa" - with patch("hangups.get_auth"): + with patch("homeassistant.components.hangouts.config_flow.get_auth"): result = await flow.async_step_2fa({"2fa": 123456}) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "test@test.com" + assert result["title"] == EMAIL async def test_flow_with_unknown_2fa(hass, aioclient_mock): @@ -68,11 +75,11 @@ async def test_flow_with_unknown_2fa(hass, aioclient_mock): flow.hass = hass with patch( - "hangups.get_auth", + "homeassistant.components.hangouts.config_flow.get_auth", side_effect=GoogleAuthError("Unknown verification code input"), ): result = await flow.async_step_user( - {"email": "test@test.com", "password": "1232456"} + {CONF_EMAIL: EMAIL, CONF_PASSWORD: PASSWORD} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"]["base"] == "invalid_2fa_method" @@ -86,9 +93,12 @@ async def test_flow_invalid_login(hass, aioclient_mock): flow.hass = hass - with patch("hangups.get_auth", side_effect=GoogleAuthError): + with patch( + "homeassistant.components.hangouts.config_flow.get_auth", + side_effect=GoogleAuthError, + ): result = await flow.async_step_user( - {"email": "test@test.com", "password": "1232456"} + {CONF_EMAIL: EMAIL, CONF_PASSWORD: PASSWORD} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"]["base"] == "invalid_login" @@ -102,14 +112,20 @@ async def test_flow_invalid_2fa(hass, aioclient_mock): flow.hass = hass - with patch("hangups.get_auth", side_effect=Google2FAError): + with patch( + "homeassistant.components.hangouts.config_flow.get_auth", + side_effect=Google2FAError, + ): result = await flow.async_step_user( - {"email": "test@test.com", "password": "1232456"} + {CONF_EMAIL: EMAIL, CONF_PASSWORD: PASSWORD} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "2fa" - with patch("hangups.get_auth", side_effect=Google2FAError): + with patch( + "homeassistant.components.hangouts.config_flow.get_auth", + side_effect=Google2FAError, + ): result = await flow.async_step_2fa({"2fa": 123456}) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM diff --git a/tests/components/hassio/conftest.py b/tests/components/hassio/conftest.py index 0e246cf1b46..091270c12c4 100644 --- a/tests/components/hassio/conftest.py +++ b/tests/components/hassio/conftest.py @@ -1,15 +1,16 @@ """Fixtures for Hass.io.""" import os -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch import pytest +from homeassistant.components.hassio.handler import HassIO, HassioAPIError from homeassistant.core import CoreState from homeassistant.setup import async_setup_component -from homeassistant.components.hassio.handler import HassIO, HassioAPIError + +from . import HASSIO_TOKEN from tests.common import mock_coro -from . import HASSIO_TOKEN @pytest.fixture @@ -19,7 +20,7 @@ def hassio_env(): "homeassistant.components.hassio.HassIO.is_connected", Mock(return_value=mock_coro({"result": "ok", "data": {}})), ), patch.dict(os.environ, {"HASSIO_TOKEN": "123456"}), patch( - "homeassistant.components.hassio.HassIO." "get_homeassistant_info", + "homeassistant.components.hassio.HassIO.get_homeassistant_info", Mock(side_effect=HassioAPIError()), ): yield diff --git a/tests/components/hassio/test_addon_panel.py b/tests/components/hassio/test_addon_panel.py index 480df508968..d2ad673111d 100644 --- a/tests/components/hassio/test_addon_panel.py +++ b/tests/components/hassio/test_addon_panel.py @@ -1,5 +1,5 @@ """Test add-on panel.""" -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch import pytest diff --git a/tests/components/hassio/test_auth.py b/tests/components/hassio/test_auth.py index 1fb6d32ccf7..c7fe3459e41 100644 --- a/tests/components/hassio/test_auth.py +++ b/tests/components/hassio/test_auth.py @@ -1,5 +1,5 @@ """The tests for the hassio component.""" -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch from homeassistant.exceptions import HomeAssistantError diff --git a/tests/components/hassio/test_discovery.py b/tests/components/hassio/test_discovery.py index a1b4ae2e900..a0d64440041 100644 --- a/tests/components/hassio/test_discovery.py +++ b/tests/components/hassio/test_discovery.py @@ -1,9 +1,9 @@ """Test config flow.""" -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch -from homeassistant.setup import async_setup_component from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.const import EVENT_HOMEASSISTANT_START +from homeassistant.setup import async_setup_component from tests.common import mock_coro @@ -40,7 +40,7 @@ async def test_hassio_discovery_startup(hass, aioclient_mock, hassio_client): assert aioclient_mock.call_count == 0 with patch( - "homeassistant.components.mqtt." "config_flow.FlowHandler.async_step_hassio", + "homeassistant.components.mqtt.config_flow.FlowHandler.async_step_hassio", Mock(return_value=mock_coro({"type": "abort"})), ) as mock_mqtt: hass.bus.async_fire(EVENT_HOMEASSISTANT_START) @@ -93,10 +93,10 @@ async def test_hassio_discovery_startup_done(hass, aioclient_mock, hassio_client "homeassistant.components.hassio.HassIO.update_hass_api", Mock(return_value=mock_coro({"result": "ok"})), ), patch( - "homeassistant.components.hassio.HassIO." "get_homeassistant_info", + "homeassistant.components.hassio.HassIO.get_homeassistant_info", Mock(side_effect=HassioAPIError()), ), patch( - "homeassistant.components.mqtt." "config_flow.FlowHandler.async_step_hassio", + "homeassistant.components.mqtt.config_flow.FlowHandler.async_step_hassio", Mock(return_value=mock_coro({"type": "abort"})), ) as mock_mqtt: await hass.async_start() @@ -143,7 +143,7 @@ async def test_hassio_discovery_webhook(hass, aioclient_mock, hassio_client): ) with patch( - "homeassistant.components.mqtt." "config_flow.FlowHandler.async_step_hassio", + "homeassistant.components.mqtt.config_flow.FlowHandler.async_step_hassio", Mock(return_value=mock_coro({"type": "abort"})), ) as mock_mqtt: resp = await hassio_client.post( diff --git a/tests/components/hassio/test_http.py b/tests/components/hassio/test_http.py index 96d53f93c3a..52cb3232ca6 100644 --- a/tests/components/hassio/test_http.py +++ b/tests/components/hassio/test_http.py @@ -5,35 +5,32 @@ from unittest.mock import patch import pytest -@asyncio.coroutine -def test_forward_request(hassio_client, aioclient_mock): +async def test_forward_request(hassio_client, aioclient_mock): """Test fetching normal path.""" aioclient_mock.post("http://127.0.0.1/beer", text="response") - resp = yield from hassio_client.post("/api/hassio/beer") + resp = await hassio_client.post("/api/hassio/beer") # Check we got right response assert resp.status == 200 - body = yield from resp.text() + body = await resp.text() assert body == "response" # Check we forwarded command assert len(aioclient_mock.mock_calls) == 1 -@asyncio.coroutine @pytest.mark.parametrize( "build_type", ["supervisor/info", "homeassistant/update", "host/info"] ) -def test_auth_required_forward_request(hassio_noauth_client, build_type): +async def test_auth_required_forward_request(hassio_noauth_client, build_type): """Test auth required for normal request.""" - resp = yield from hassio_noauth_client.post("/api/hassio/{}".format(build_type)) + resp = await hassio_noauth_client.post("/api/hassio/{}".format(build_type)) # Check we got right response assert resp.status == 401 -@asyncio.coroutine @pytest.mark.parametrize( "build_type", [ @@ -45,61 +42,60 @@ def test_auth_required_forward_request(hassio_noauth_client, build_type): "app/app.js", ], ) -def test_forward_request_no_auth_for_panel(hassio_client, build_type, aioclient_mock): +async def test_forward_request_no_auth_for_panel( + hassio_client, build_type, aioclient_mock +): """Test no auth needed for .""" aioclient_mock.get("http://127.0.0.1/{}".format(build_type), text="response") - resp = yield from hassio_client.get("/api/hassio/{}".format(build_type)) + resp = await hassio_client.get("/api/hassio/{}".format(build_type)) # Check we got right response assert resp.status == 200 - body = yield from resp.text() + body = await resp.text() assert body == "response" # Check we forwarded command assert len(aioclient_mock.mock_calls) == 1 -@asyncio.coroutine -def test_forward_request_no_auth_for_logo(hassio_client, aioclient_mock): +async def test_forward_request_no_auth_for_logo(hassio_client, aioclient_mock): """Test no auth needed for .""" aioclient_mock.get("http://127.0.0.1/addons/bl_b392/logo", text="response") - resp = yield from hassio_client.get("/api/hassio/addons/bl_b392/logo") + resp = await hassio_client.get("/api/hassio/addons/bl_b392/logo") # Check we got right response assert resp.status == 200 - body = yield from resp.text() + body = await resp.text() assert body == "response" # Check we forwarded command assert len(aioclient_mock.mock_calls) == 1 -@asyncio.coroutine -def test_forward_log_request(hassio_client, aioclient_mock): +async def test_forward_log_request(hassio_client, aioclient_mock): """Test fetching normal log path doesn't remove ANSI color escape codes.""" aioclient_mock.get("http://127.0.0.1/beer/logs", text="\033[32mresponse\033[0m") - resp = yield from hassio_client.get("/api/hassio/beer/logs") + resp = await hassio_client.get("/api/hassio/beer/logs") # Check we got right response assert resp.status == 200 - body = yield from resp.text() + body = await resp.text() assert body == "\033[32mresponse\033[0m" # Check we forwarded command assert len(aioclient_mock.mock_calls) == 1 -@asyncio.coroutine -def test_bad_gateway_when_cannot_find_supervisor(hassio_client): +async def test_bad_gateway_when_cannot_find_supervisor(hassio_client): """Test we get a bad gateway error if we can't find supervisor.""" with patch( "homeassistant.components.hassio.http.async_timeout.timeout", side_effect=asyncio.TimeoutError, ): - resp = yield from hassio_client.get("/api/hassio/addons/test/info") + resp = await hassio_client.get("/api/hassio/addons/test/info") assert resp.status == 502 diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index 3d27931896f..1e227f943ed 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -1,18 +1,16 @@ """The tests for the hassio component.""" -import asyncio import os -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch import pytest from homeassistant.auth.const import GROUP_ID_ADMIN -from homeassistant.setup import async_setup_component -from homeassistant.components.hassio import STORAGE_KEY from homeassistant.components import frontend +from homeassistant.components.hassio import STORAGE_KEY +from homeassistant.setup import async_setup_component from tests.common import mock_coro - MOCK_ENVIRON = {"HASSIO": "127.0.0.1", "HASSIO_TOKEN": "abcdefgh"} @@ -31,11 +29,10 @@ def mock_all(aioclient_mock): ) -@asyncio.coroutine -def test_setup_api_ping(hass, aioclient_mock): +async def test_setup_api_ping(hass, aioclient_mock): """Test setup with API ping.""" with patch.dict(os.environ, MOCK_ENVIRON): - result = yield from async_setup_component(hass, "hassio", {}) + result = await async_setup_component(hass, "hassio", {}) assert result assert aioclient_mock.call_count == 5 @@ -69,11 +66,10 @@ async def test_setup_api_panel(hass, aioclient_mock): } -@asyncio.coroutine -def test_setup_api_push_api_data(hass, aioclient_mock): +async def test_setup_api_push_api_data(hass, aioclient_mock): """Test setup with API push.""" with patch.dict(os.environ, MOCK_ENVIRON): - result = yield from async_setup_component( + result = await async_setup_component( hass, "hassio", {"http": {"server_port": 9999}, "hassio": {}} ) assert result @@ -84,11 +80,10 @@ def test_setup_api_push_api_data(hass, aioclient_mock): assert aioclient_mock.mock_calls[1][2]["watchdog"] -@asyncio.coroutine -def test_setup_api_push_api_data_server_host(hass, aioclient_mock): +async def test_setup_api_push_api_data_server_host(hass, aioclient_mock): """Test setup with API push with active server host.""" with patch.dict(os.environ, MOCK_ENVIRON): - result = yield from async_setup_component( + result = await async_setup_component( hass, "hassio", {"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}}, @@ -176,45 +171,41 @@ async def test_setup_core_push_timezone(hass, aioclient_mock): assert aioclient_mock.mock_calls[-1][2]["timezone"] == "America/New_York" -@asyncio.coroutine -def test_setup_hassio_no_additional_data(hass, aioclient_mock): +async def test_setup_hassio_no_additional_data(hass, aioclient_mock): """Test setup with API push default data.""" with patch.dict(os.environ, MOCK_ENVIRON), patch.dict( os.environ, {"HASSIO_TOKEN": "123456"} ): - result = yield from async_setup_component(hass, "hassio", {"hassio": {}}) + result = await async_setup_component(hass, "hassio", {"hassio": {}}) assert result assert aioclient_mock.call_count == 5 assert aioclient_mock.mock_calls[-1][3]["X-Hassio-Key"] == "123456" -@asyncio.coroutine -def test_fail_setup_without_environ_var(hass): +async def test_fail_setup_without_environ_var(hass): """Fail setup if no environ variable set.""" with patch.dict(os.environ, {}, clear=True): - result = yield from async_setup_component(hass, "hassio", {}) + result = await async_setup_component(hass, "hassio", {}) assert not result -@asyncio.coroutine -def test_warn_when_cannot_connect(hass, caplog): +async def test_warn_when_cannot_connect(hass, caplog): """Fail warn when we cannot connect.""" with patch.dict(os.environ, MOCK_ENVIRON), patch( "homeassistant.components.hassio.HassIO.is_connected", Mock(return_value=mock_coro(None)), ): - result = yield from async_setup_component(hass, "hassio", {}) + result = await async_setup_component(hass, "hassio", {}) assert result assert hass.components.hassio.is_hassio() assert "Not connected with Hass.io / system to busy!" in caplog.text -@asyncio.coroutine -def test_service_register(hassio_env, hass): +async def test_service_register(hassio_env, hass): """Check if service will be setup.""" - assert (yield from async_setup_component(hass, "hassio", {})) + assert await async_setup_component(hass, "hassio", {}) assert hass.services.has_service("hassio", "addon_start") assert hass.services.has_service("hassio", "addon_stop") assert hass.services.has_service("hassio", "addon_restart") @@ -228,10 +219,9 @@ def test_service_register(hassio_env, hass): assert hass.services.has_service("hassio", "restore_partial") -@asyncio.coroutine -def test_service_calls(hassio_env, hass, aioclient_mock): +async def test_service_calls(hassio_env, hass, aioclient_mock): """Call service and check the API calls behind that.""" - assert (yield from async_setup_component(hass, "hassio", {})) + assert await async_setup_component(hass, "hassio", {}) 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"}) @@ -248,30 +238,30 @@ def test_service_calls(hassio_env, hass, aioclient_mock): "http://127.0.0.1/snapshots/test/restore/partial", json={"result": "ok"} ) - yield from hass.services.async_call("hassio", "addon_start", {"addon": "test"}) - yield from hass.services.async_call("hassio", "addon_stop", {"addon": "test"}) - yield from hass.services.async_call("hassio", "addon_restart", {"addon": "test"}) - yield from hass.services.async_call( + 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_stdin", {"addon": "test", "input": "test"} ) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert aioclient_mock.call_count == 7 assert aioclient_mock.mock_calls[-1][2] == "test" - yield from hass.services.async_call("hassio", "host_shutdown", {}) - yield from hass.services.async_call("hassio", "host_reboot", {}) - yield from hass.async_block_till_done() + 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 - yield from hass.services.async_call("hassio", "snapshot_full", {}) - yield from hass.services.async_call( + await hass.services.async_call("hassio", "snapshot_full", {}) + await hass.services.async_call( "hassio", "snapshot_partial", {"addons": ["test"], "folders": ["ssl"], "password": "123456"}, ) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert aioclient_mock.call_count == 11 assert aioclient_mock.mock_calls[-1][2] == { @@ -280,8 +270,8 @@ def test_service_calls(hassio_env, hass, aioclient_mock): "password": "123456", } - yield from hass.services.async_call("hassio", "restore_full", {"snapshot": "test"}) - yield from hass.services.async_call( + await hass.services.async_call("hassio", "restore_full", {"snapshot": "test"}) + await hass.services.async_call( "hassio", "restore_partial", { @@ -292,7 +282,7 @@ def test_service_calls(hassio_env, hass, aioclient_mock): "password": "123456", }, ) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert aioclient_mock.call_count == 13 assert aioclient_mock.mock_calls[-1][2] == { @@ -303,29 +293,28 @@ def test_service_calls(hassio_env, hass, aioclient_mock): } -@asyncio.coroutine -def test_service_calls_core(hassio_env, hass, aioclient_mock): +async def test_service_calls_core(hassio_env, hass, aioclient_mock): """Call core service and check the API calls behind that.""" - assert (yield from async_setup_component(hass, "hassio", {})) + assert await async_setup_component(hass, "hassio", {}) aioclient_mock.post("http://127.0.0.1/homeassistant/restart", json={"result": "ok"}) aioclient_mock.post("http://127.0.0.1/homeassistant/stop", json={"result": "ok"}) - yield from hass.services.async_call("homeassistant", "stop") - yield from hass.async_block_till_done() + await hass.services.async_call("homeassistant", "stop") + await hass.async_block_till_done() assert aioclient_mock.call_count == 4 - yield from hass.services.async_call("homeassistant", "check_config") - yield from hass.async_block_till_done() + await hass.services.async_call("homeassistant", "check_config") + await hass.async_block_till_done() assert aioclient_mock.call_count == 4 with patch( "homeassistant.config.async_check_ha_config_file", return_value=mock_coro() ) as mock_check_config: - yield from hass.services.async_call("homeassistant", "restart") - yield from hass.async_block_till_done() + await hass.services.async_call("homeassistant", "restart") + await hass.async_block_till_done() assert mock_check_config.called assert aioclient_mock.call_count == 5 diff --git a/tests/components/hddtemp/test_sensor.py b/tests/components/hddtemp/test_sensor.py index aeb4ed2ab9b..0b4d2d0d4c6 100644 --- a/tests/components/hddtemp/test_sensor.py +++ b/tests/components/hddtemp/test_sensor.py @@ -1,6 +1,5 @@ """The tests for the hddtemp platform.""" import socket - import unittest from unittest.mock import patch diff --git a/tests/components/heos/conftest.py b/tests/components/heos/conftest.py index c4610fdc1e7..5201b7f7b8a 100644 --- a/tests/components/heos/conftest.py +++ b/tests/components/heos/conftest.py @@ -5,6 +5,7 @@ from asynctest.mock import Mock, patch as patch from pyheos import Dispatcher, Heos, HeosPlayer, HeosSource, InputSource, const import pytest +from homeassistant.components import ssdp from homeassistant.components.heos import DOMAIN from homeassistant.const import CONF_HOST @@ -118,16 +119,14 @@ def dispatcher_fixture() -> Dispatcher: def discovery_data_fixture() -> dict: """Return mock discovery data for testing.""" return { - "host": "127.0.0.1", - "manufacturer": "Denon", - "model_name": "HEOS Drive", - "model_number": "DWSA-10 4.0", - "name": "Office", - "port": 60006, - "serial": None, - "ssdp_description": "http://127.0.0.1:60006/upnp/desc/aios_device/aios_device.xml", - "udn": "uuid:e61de70c-2250-1c22-0080-0005cdf512be", - "upnp_device_type": "urn:schemas-denon-com:device:AiosDevice:1", + ssdp.ATTR_SSDP_LOCATION: "http://127.0.0.1:60006/upnp/desc/aios_device/aios_device.xml", + ssdp.ATTR_UPNP_DEVICE_TYPE: "urn:schemas-denon-com:device:AiosDevice:1", + ssdp.ATTR_UPNP_FRIENDLY_NAME: "Office", + ssdp.ATTR_UPNP_MANUFACTURER: "Denon", + ssdp.ATTR_UPNP_MODEL_NAME: "HEOS Drive", + ssdp.ATTR_UPNP_MODEL_NUMBER: "DWSA-10 4.0", + ssdp.ATTR_UPNP_SERIAL: None, + ssdp.ATTR_UPNP_UDN: "uuid:e61de70c-2250-1c22-0080-0005cdf512be", } diff --git a/tests/components/heos/test_config_flow.py b/tests/components/heos/test_config_flow.py index df021fea55d..84e5dce1f1c 100644 --- a/tests/components/heos/test_config_flow.py +++ b/tests/components/heos/test_config_flow.py @@ -1,10 +1,13 @@ """Tests for the Heos config flow module.""" +from urllib.parse import urlparse + from pyheos import HeosError from homeassistant import data_entry_flow +from homeassistant.components import ssdp from homeassistant.components.heos.config_flow import HeosFlowHandler from homeassistant.components.heos.const import DATA_DISCOVERED_HOSTS, DOMAIN -from homeassistant.const import CONF_HOST, CONF_NAME +from homeassistant.const import CONF_HOST async def test_flow_aborts_already_setup(hass, config_entry): @@ -79,8 +82,9 @@ async def test_discovery_shows_create_form(hass, controller, discovery_data): assert len(hass.config_entries.flow.async_progress()) == 1 assert hass.data[DATA_DISCOVERED_HOSTS] == {"Office (127.0.0.1)": "127.0.0.1"} - discovery_data[CONF_HOST] = "127.0.0.2" - discovery_data[CONF_NAME] = "Bedroom" + port = urlparse(discovery_data[ssdp.ATTR_SSDP_LOCATION]).port + discovery_data[ssdp.ATTR_SSDP_LOCATION] = f"http://127.0.0.2:{port}/" + discovery_data[ssdp.ATTR_UPNP_FRIENDLY_NAME] = "Bedroom" await hass.config_entries.flow.async_init( DOMAIN, context={"source": "ssdp"}, data=discovery_data ) diff --git a/tests/components/here_travel_time/test_sensor.py b/tests/components/here_travel_time/test_sensor.py index 1d788c93c66..4456b256f6e 100644 --- a/tests/components/here_travel_time/test_sensor.py +++ b/tests/components/here_travel_time/test_sensor.py @@ -48,8 +48,7 @@ DOMAIN = "sensor" PLATFORM = "here_travel_time" -APP_ID = "test" -APP_CODE = "test" +API_KEY = "test" TRUCK_ORIGIN_LATITUDE = "41.9798" TRUCK_ORIGIN_LONGITUDE = "-87.8801" @@ -67,15 +66,14 @@ CAR_DESTINATION_LATITUDE = "39.0" CAR_DESTINATION_LONGITUDE = "-77.1" -def _build_mock_url(origin, destination, modes, app_id, app_code, departure): +def _build_mock_url(origin, destination, modes, api_key, departure): """Construct a url for HERE.""" - base_url = "https://route.cit.api.here.com/routing/7.2/calculateroute.json?" + base_url = "https://route.ls.hereapi.com/routing/7.2/calculateroute.json?" parameters = { - "waypoint0": origin, - "waypoint1": destination, + "waypoint0": f"geo!{origin}", + "waypoint1": f"geo!{destination}", "mode": ";".join(str(herepy.RouteMode[mode]) for mode in modes), - "app_id": app_id, - "app_code": app_code, + "apikey": api_key, "departure": departure, } url = base_url + urllib.parse.urlencode(parameters) @@ -118,8 +116,7 @@ def requests_mock_credentials_check(requests_mock): ",".join([CAR_ORIGIN_LATITUDE, CAR_ORIGIN_LONGITUDE]), ",".join([CAR_DESTINATION_LATITUDE, CAR_DESTINATION_LONGITUDE]), modes, - APP_ID, - APP_CODE, + API_KEY, "now", ) requests_mock.get( @@ -136,8 +133,7 @@ def requests_mock_truck_response(requests_mock_credentials_check): ",".join([TRUCK_ORIGIN_LATITUDE, TRUCK_ORIGIN_LONGITUDE]), ",".join([TRUCK_DESTINATION_LATITUDE, TRUCK_DESTINATION_LONGITUDE]), modes, - APP_ID, - APP_CODE, + API_KEY, "now", ) requests_mock_credentials_check.get( @@ -153,8 +149,7 @@ def requests_mock_car_disabled_response(requests_mock_credentials_check): ",".join([CAR_ORIGIN_LATITUDE, CAR_ORIGIN_LONGITUDE]), ",".join([CAR_DESTINATION_LATITUDE, CAR_DESTINATION_LONGITUDE]), modes, - APP_ID, - APP_CODE, + API_KEY, "now", ) requests_mock_credentials_check.get( @@ -172,8 +167,7 @@ async def test_car(hass, requests_mock_car_disabled_response): "origin_longitude": CAR_ORIGIN_LONGITUDE, "destination_latitude": CAR_DESTINATION_LATITUDE, "destination_longitude": CAR_DESTINATION_LONGITUDE, - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, } } assert await async_setup_component(hass, DOMAIN, config) @@ -219,8 +213,7 @@ async def test_traffic_mode_enabled(hass, requests_mock_credentials_check): ",".join([CAR_ORIGIN_LATITUDE, CAR_ORIGIN_LONGITUDE]), ",".join([CAR_DESTINATION_LATITUDE, CAR_DESTINATION_LONGITUDE]), modes, - APP_ID, - APP_CODE, + API_KEY, "now", ) requests_mock_credentials_check.get( @@ -235,8 +228,7 @@ async def test_traffic_mode_enabled(hass, requests_mock_credentials_check): "origin_longitude": CAR_ORIGIN_LONGITUDE, "destination_latitude": CAR_DESTINATION_LATITUDE, "destination_longitude": CAR_DESTINATION_LONGITUDE, - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, "traffic_mode": True, } } @@ -262,8 +254,7 @@ async def test_imperial(hass, requests_mock_car_disabled_response): "origin_longitude": CAR_ORIGIN_LONGITUDE, "destination_latitude": CAR_DESTINATION_LATITUDE, "destination_longitude": CAR_DESTINATION_LONGITUDE, - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, "unit_system": "imperial", } } @@ -281,7 +272,7 @@ async def test_route_mode_shortest(hass, requests_mock_credentials_check): origin = "38.902981,-77.048338" destination = "39.042158,-77.119116" modes = [ROUTE_MODE_SHORTEST, TRAVEL_MODE_CAR, TRAFFIC_MODE_DISABLED] - response_url = _build_mock_url(origin, destination, modes, APP_ID, APP_CODE, "now") + response_url = _build_mock_url(origin, destination, modes, API_KEY, "now") requests_mock_credentials_check.get( response_url, text=load_fixture("here_travel_time/car_shortest_response.json") ) @@ -294,8 +285,7 @@ async def test_route_mode_shortest(hass, requests_mock_credentials_check): "origin_longitude": origin.split(",")[1], "destination_latitude": destination.split(",")[0], "destination_longitude": destination.split(",")[1], - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, "route_mode": ROUTE_MODE_SHORTEST, } } @@ -313,7 +303,7 @@ async def test_route_mode_fastest(hass, requests_mock_credentials_check): origin = "38.902981,-77.048338" destination = "39.042158,-77.119116" modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_CAR, TRAFFIC_MODE_ENABLED] - response_url = _build_mock_url(origin, destination, modes, APP_ID, APP_CODE, "now") + response_url = _build_mock_url(origin, destination, modes, API_KEY, "now") requests_mock_credentials_check.get( response_url, text=load_fixture("here_travel_time/car_enabled_response.json") ) @@ -326,8 +316,7 @@ async def test_route_mode_fastest(hass, requests_mock_credentials_check): "origin_longitude": origin.split(",")[1], "destination_latitude": destination.split(",")[0], "destination_longitude": destination.split(",")[1], - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, "traffic_mode": True, } } @@ -350,8 +339,7 @@ async def test_truck(hass, requests_mock_truck_response): "origin_longitude": TRUCK_ORIGIN_LONGITUDE, "destination_latitude": TRUCK_DESTINATION_LATITUDE, "destination_longitude": TRUCK_DESTINATION_LONGITUDE, - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, "mode": TRAVEL_MODE_TRUCK, } } @@ -369,7 +357,7 @@ async def test_public_transport(hass, requests_mock_credentials_check): origin = "41.9798,-87.8801" destination = "41.9043,-87.9216" modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_PUBLIC, TRAFFIC_MODE_DISABLED] - response_url = _build_mock_url(origin, destination, modes, APP_ID, APP_CODE, "now") + response_url = _build_mock_url(origin, destination, modes, API_KEY, "now") requests_mock_credentials_check.get( response_url, text=load_fixture("here_travel_time/public_response.json") ) @@ -382,8 +370,7 @@ async def test_public_transport(hass, requests_mock_credentials_check): "origin_longitude": origin.split(",")[1], "destination_latitude": destination.split(",")[0], "destination_longitude": destination.split(",")[1], - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, "mode": TRAVEL_MODE_PUBLIC, } } @@ -400,7 +387,7 @@ async def test_public_transport(hass, requests_mock_credentials_check): assert sensor.attributes.get(ATTR_DURATION) == 89.16666666666667 assert sensor.attributes.get(ATTR_DISTANCE) == 22.325 assert sensor.attributes.get(ATTR_ROUTE) == ( - "332 - Palmer/Schiller; 332 - Cargo Rd./Delta Cargo; " "332 - Palmer/Schiller" + "332 - Palmer/Schiller; 332 - Cargo Rd./Delta Cargo; 332 - Palmer/Schiller" ) assert sensor.attributes.get(CONF_UNIT_SYSTEM) == "metric" assert sensor.attributes.get(ATTR_DURATION_IN_TRAFFIC) == 89.16666666666667 @@ -419,7 +406,7 @@ async def test_public_transport_time_table(hass, requests_mock_credentials_check origin = "41.9798,-87.8801" destination = "41.9043,-87.9216" modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_PUBLIC_TIME_TABLE, TRAFFIC_MODE_DISABLED] - response_url = _build_mock_url(origin, destination, modes, APP_ID, APP_CODE, "now") + response_url = _build_mock_url(origin, destination, modes, API_KEY, "now") requests_mock_credentials_check.get( response_url, text=load_fixture("here_travel_time/public_time_table_response.json"), @@ -433,8 +420,7 @@ async def test_public_transport_time_table(hass, requests_mock_credentials_check "origin_longitude": origin.split(",")[1], "destination_latitude": destination.split(",")[0], "destination_longitude": destination.split(",")[1], - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, "mode": TRAVEL_MODE_PUBLIC_TIME_TABLE, } } @@ -470,7 +456,7 @@ async def test_pedestrian(hass, requests_mock_credentials_check): origin = "41.9798,-87.8801" destination = "41.9043,-87.9216" modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_PEDESTRIAN, TRAFFIC_MODE_DISABLED] - response_url = _build_mock_url(origin, destination, modes, APP_ID, APP_CODE, "now") + response_url = _build_mock_url(origin, destination, modes, API_KEY, "now") requests_mock_credentials_check.get( response_url, text=load_fixture("here_travel_time/pedestrian_response.json") ) @@ -483,8 +469,7 @@ async def test_pedestrian(hass, requests_mock_credentials_check): "origin_longitude": origin.split(",")[1], "destination_latitude": destination.split(",")[0], "destination_longitude": destination.split(",")[1], - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, "mode": TRAVEL_MODE_PEDESTRIAN, } } @@ -523,7 +508,7 @@ async def test_bicycle(hass, requests_mock_credentials_check): origin = "41.9798,-87.8801" destination = "41.9043,-87.9216" modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_BICYCLE, TRAFFIC_MODE_DISABLED] - response_url = _build_mock_url(origin, destination, modes, APP_ID, APP_CODE, "now") + response_url = _build_mock_url(origin, destination, modes, API_KEY, "now") requests_mock_credentials_check.get( response_url, text=load_fixture("here_travel_time/bike_response.json") ) @@ -536,8 +521,7 @@ async def test_bicycle(hass, requests_mock_credentials_check): "origin_longitude": origin.split(",")[1], "destination_latitude": destination.split(",")[0], "destination_longitude": destination.split(",")[1], - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, "mode": TRAVEL_MODE_BICYCLE, } } @@ -599,8 +583,7 @@ async def test_location_zone(hass, requests_mock_truck_response): "name": "test", "origin_entity_id": "zone.origin", "destination_entity_id": "zone.destination", - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, "mode": TRAVEL_MODE_TRUCK, } } @@ -640,8 +623,7 @@ async def test_location_sensor(hass, requests_mock_truck_response): "name": "test", "origin_entity_id": "sensor.origin", "destination_entity_id": "sensor.destination", - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, "mode": TRAVEL_MODE_TRUCK, } } @@ -689,8 +671,7 @@ async def test_location_person(hass, requests_mock_truck_response): "name": "test", "origin_entity_id": "person.origin", "destination_entity_id": "person.destination", - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, "mode": TRAVEL_MODE_TRUCK, } } @@ -738,8 +719,7 @@ async def test_location_device_tracker(hass, requests_mock_truck_response): "name": "test", "origin_entity_id": "device_tracker.origin", "destination_entity_id": "device_tracker.destination", - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, "mode": TRAVEL_MODE_TRUCK, } } @@ -773,8 +753,7 @@ async def test_location_device_tracker_added_after_update( "name": "test", "origin_entity_id": "device_tracker.origin", "destination_entity_id": "device_tracker.destination", - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, "mode": TRAVEL_MODE_TRUCK, } } @@ -842,8 +821,7 @@ async def test_location_device_tracker_in_zone( "origin_entity_id": "device_tracker.origin", "destination_latitude": TRUCK_DESTINATION_LATITUDE, "destination_longitude": TRUCK_DESTINATION_LONGITUDE, - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, "mode": TRAVEL_MODE_TRUCK, } } @@ -863,7 +841,7 @@ async def test_route_not_found(hass, requests_mock_credentials_check, caplog): origin = "52.516,13.3779" destination = "47.013399,-10.171986" modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_CAR, TRAFFIC_MODE_DISABLED] - response_url = _build_mock_url(origin, destination, modes, APP_ID, APP_CODE, "now") + response_url = _build_mock_url(origin, destination, modes, API_KEY, "now") requests_mock_credentials_check.get( response_url, text=load_fixture("here_travel_time/routing_error_no_route_found.json"), @@ -877,8 +855,7 @@ async def test_route_not_found(hass, requests_mock_credentials_check, caplog): "origin_longitude": origin.split(",")[1], "destination_latitude": destination.split(",")[0], "destination_longitude": destination.split(",")[1], - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, } } assert await async_setup_component(hass, DOMAIN, config) @@ -901,8 +878,7 @@ async def test_pattern_origin(hass, caplog): "origin_longitude": "-77.04833", "destination_latitude": CAR_DESTINATION_LATITUDE, "destination_longitude": CAR_DESTINATION_LONGITUDE, - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, } } assert await async_setup_component(hass, DOMAIN, config) @@ -921,8 +897,7 @@ async def test_pattern_destination(hass, caplog): "origin_longitude": CAR_ORIGIN_LONGITUDE, "destination_latitude": "139.0", "destination_longitude": "-77.1", - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, } } assert await async_setup_component(hass, DOMAIN, config) @@ -938,8 +913,7 @@ async def test_invalid_credentials(hass, requests_mock, caplog): ",".join([CAR_ORIGIN_LATITUDE, CAR_ORIGIN_LONGITUDE]), ",".join([CAR_DESTINATION_LATITUDE, CAR_DESTINATION_LONGITUDE]), modes, - APP_ID, - APP_CODE, + API_KEY, "now", ) requests_mock.get( @@ -955,8 +929,7 @@ async def test_invalid_credentials(hass, requests_mock, caplog): "origin_longitude": CAR_ORIGIN_LONGITUDE, "destination_latitude": CAR_DESTINATION_LATITUDE, "destination_longitude": CAR_DESTINATION_LONGITUDE, - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, } } assert await async_setup_component(hass, DOMAIN, config) @@ -969,7 +942,7 @@ async def test_attribution(hass, requests_mock_credentials_check): origin = "50.037751372637686,14.39233448220898" destination = "50.07993838201255,14.42582157361062" modes = [ROUTE_MODE_SHORTEST, TRAVEL_MODE_PUBLIC_TIME_TABLE, TRAFFIC_MODE_ENABLED] - response_url = _build_mock_url(origin, destination, modes, APP_ID, APP_CODE, "now") + response_url = _build_mock_url(origin, destination, modes, API_KEY, "now") requests_mock_credentials_check.get( response_url, text=load_fixture("here_travel_time/attribution_response.json") ) @@ -982,8 +955,7 @@ async def test_attribution(hass, requests_mock_credentials_check): "origin_longitude": origin.split(",")[1], "destination_latitude": destination.split(",")[0], "destination_longitude": destination.split(",")[1], - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, "traffic_mode": True, "route_mode": ROUTE_MODE_SHORTEST, "mode": TRAVEL_MODE_PUBLIC_TIME_TABLE, @@ -1013,8 +985,7 @@ async def test_pattern_entity_state(hass, requests_mock_truck_response, caplog): "origin_entity_id": "sensor.origin", "destination_latitude": TRUCK_DESTINATION_LATITUDE, "destination_longitude": TRUCK_DESTINATION_LONGITUDE, - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, "mode": TRAVEL_MODE_TRUCK, } } @@ -1040,8 +1011,7 @@ async def test_pattern_entity_state_with_space(hass, requests_mock_truck_respons "origin_entity_id": "sensor.origin", "destination_latitude": TRUCK_DESTINATION_LATITUDE, "destination_longitude": TRUCK_DESTINATION_LONGITUDE, - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, "mode": TRAVEL_MODE_TRUCK, } } @@ -1059,8 +1029,7 @@ async def test_delayed_update(hass, requests_mock_truck_response, caplog): "origin_entity_id": "sensor.origin", "destination_latitude": TRUCK_DESTINATION_LATITUDE, "destination_longitude": TRUCK_DESTINATION_LONGITUDE, - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, "mode": TRAVEL_MODE_TRUCK, } } diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index 68bc9c5371f..051024999e4 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -4,15 +4,15 @@ from datetime import timedelta import unittest from unittest.mock import patch, sentinel -from homeassistant.setup import setup_component, async_setup_component -import homeassistant.core as ha -import homeassistant.util.dt as dt_util from homeassistant.components import history, recorder +import homeassistant.core as ha +from homeassistant.setup import async_setup_component, setup_component +import homeassistant.util.dt as dt_util from tests.common import ( + get_test_home_assistant, init_recorder_component, mock_state_change_event, - get_test_home_assistant, ) diff --git a/tests/components/history_graph/test_init.py b/tests/components/history_graph/test_init.py index d46bdd02843..ef41f70aaa7 100644 --- a/tests/components/history_graph/test_init.py +++ b/tests/components/history_graph/test_init.py @@ -3,7 +3,8 @@ import unittest from homeassistant.setup import setup_component -from tests.common import init_recorder_component, get_test_home_assistant + +from tests.common import get_test_home_assistant, init_recorder_component class TestGraph(unittest.TestCase): diff --git a/tests/components/history_stats/test_sensor.py b/tests/components/history_stats/test_sensor.py index 178abf2b152..588e0df81db 100644 --- a/tests/components/history_stats/test_sensor.py +++ b/tests/components/history_stats/test_sensor.py @@ -3,17 +3,18 @@ from datetime import datetime, timedelta import unittest from unittest.mock import patch + import pytest import pytz -from homeassistant.const import STATE_UNKNOWN -from homeassistant.setup import setup_component from homeassistant.components.history_stats.sensor import HistoryStatsSensor +from homeassistant.const import STATE_UNKNOWN import homeassistant.core as ha from homeassistant.helpers.template import Template +from homeassistant.setup import setup_component import homeassistant.util.dt as dt_util -from tests.common import init_recorder_component, get_test_home_assistant +from tests.common import get_test_home_assistant, init_recorder_component class TestHistoryStatsSensor(unittest.TestCase): @@ -49,7 +50,7 @@ class TestHistoryStatsSensor(unittest.TestCase): assert state.state == STATE_UNKNOWN @patch( - "homeassistant.helpers.template.TemplateEnvironment." "is_safe_callable", + "homeassistant.helpers.template.TemplateEnvironment.is_safe_callable", return_value=True, ) def test_period_parsing(self, mock): @@ -57,7 +58,7 @@ class TestHistoryStatsSensor(unittest.TestCase): now = datetime(2019, 1, 1, 23, 30, 0, tzinfo=pytz.utc) with patch("homeassistant.util.dt.now", return_value=now): today = Template( - "{{ now().replace(hour=0).replace(minute=0)" ".replace(second=0) }}", + "{{ now().replace(hour=0).replace(minute=0).replace(second=0) }}", self.hass, ) duration = timedelta(hours=2, minutes=1) @@ -136,7 +137,7 @@ class TestHistoryStatsSensor(unittest.TestCase): assert sensor4._type == "ratio" with patch( - "homeassistant.components.history." "state_changes_during_period", + "homeassistant.components.history.state_changes_during_period", return_value=fake_states, ): with patch("homeassistant.components.history.get_state", return_value=None): diff --git a/tests/components/homeassistant/test_init.py b/tests/components/homeassistant/test_init.py index 7a97de0f68e..6c2b7f78e24 100644 --- a/tests/components/homeassistant/test_init.py +++ b/tests/components/homeassistant/test_init.py @@ -2,40 +2,40 @@ # pylint: disable=protected-access import asyncio import unittest -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch import yaml -import homeassistant.core as ha from homeassistant import config -from homeassistant.const import ( - ATTR_ENTITY_ID, - STATE_ON, - STATE_OFF, - SERVICE_HOMEASSISTANT_RESTART, - SERVICE_HOMEASSISTANT_STOP, - SERVICE_TURN_ON, - SERVICE_TURN_OFF, - SERVICE_TOGGLE, - EVENT_CORE_CONFIG_UPDATE, -) import homeassistant.components as comps -from homeassistant.setup import async_setup_component from homeassistant.components.homeassistant import ( SERVICE_CHECK_CONFIG, SERVICE_RELOAD_CORE_CONFIG, ) -import homeassistant.helpers.intent as intent +from homeassistant.const import ( + ATTR_ENTITY_ID, + EVENT_CORE_CONFIG_UPDATE, + SERVICE_HOMEASSISTANT_RESTART, + SERVICE_HOMEASSISTANT_STOP, + SERVICE_TOGGLE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, +) +import homeassistant.core as ha from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity +import homeassistant.helpers.intent as intent +from homeassistant.setup import async_setup_component from tests.common import ( + async_capture_events, + async_mock_service, get_test_home_assistant, + mock_coro, mock_service, patch_yaml_files, - mock_coro, - async_mock_service, - async_capture_events, ) diff --git a/tests/components/homeassistant/test_scene.py b/tests/components/homeassistant/test_scene.py index 25ce6088a51..d3bbac44df8 100644 --- a/tests/components/homeassistant/test_scene.py +++ b/tests/components/homeassistant/test_scene.py @@ -1,8 +1,13 @@ """Test Home Assistant scenes.""" from unittest.mock import patch +import pytest +import voluptuous as vol + from homeassistant.setup import async_setup_component +from tests.common import async_mock_service + async def test_reload_config_service(hass): """Test the reload config service.""" @@ -63,6 +68,16 @@ async def test_create_service(hass, caplog): assert hass.states.get("scene.hallo") is None assert hass.states.get("scene.hallo_2") is not None + assert await hass.services.async_call( + "scene", + "create", + {"scene_id": "hallo", "entities": {}, "snapshot_entities": []}, + blocking=True, + ) + await hass.async_block_till_done() + assert "Empty scenes are not allowed" in caplog.text + assert hass.states.get("scene.hallo") is None + assert await hass.services.async_call( "scene", "create", @@ -117,3 +132,80 @@ async def test_create_service(hass, caplog): assert scene.name == "hallo_2" assert scene.state == "scening" assert scene.attributes.get("entity_id") == ["light.kitchen"] + + +async def test_snapshot_service(hass, caplog): + """Test the snapshot option.""" + assert await async_setup_component(hass, "scene", {"scene": {}}) + hass.states.async_set("light.my_light", "on", {"hs_color": (345, 75)}) + assert hass.states.get("scene.hallo") is None + + assert await hass.services.async_call( + "scene", + "create", + {"scene_id": "hallo", "snapshot_entities": ["light.my_light"]}, + blocking=True, + ) + await hass.async_block_till_done() + scene = hass.states.get("scene.hallo") + assert scene is not None + assert scene.attributes.get("entity_id") == ["light.my_light"] + + hass.states.async_set("light.my_light", "off", {"hs_color": (123, 45)}) + turn_on_calls = async_mock_service(hass, "light", "turn_on") + assert await hass.services.async_call( + "scene", "turn_on", {"entity_id": "scene.hallo"}, blocking=True + ) + await hass.async_block_till_done() + assert len(turn_on_calls) == 1 + assert turn_on_calls[0].data.get("entity_id") == "light.my_light" + assert turn_on_calls[0].data.get("hs_color") == (345, 75) + + assert await hass.services.async_call( + "scene", + "create", + {"scene_id": "hallo_2", "snapshot_entities": ["light.not_existent"]}, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.get("scene.hallo_2") is None + assert ( + "Entity light.not_existent does not exist and therefore cannot be snapshotted" + in caplog.text + ) + + assert await hass.services.async_call( + "scene", + "create", + { + "scene_id": "hallo_3", + "entities": {"light.bed_light": {"state": "on", "brightness": 50}}, + "snapshot_entities": ["light.my_light"], + }, + blocking=True, + ) + await hass.async_block_till_done() + scene = hass.states.get("scene.hallo_3") + assert scene is not None + assert "light.my_light" in scene.attributes.get("entity_id") + assert "light.bed_light" in scene.attributes.get("entity_id") + + +async def test_ensure_no_intersection(hass): + """Test that entities and snapshot_entities do not overlap.""" + assert await async_setup_component(hass, "scene", {"scene": {}}) + + with pytest.raises(vol.MultipleInvalid) as ex: + assert await hass.services.async_call( + "scene", + "create", + { + "scene_id": "hallo", + "entities": {"light.my_light": {"state": "on", "brightness": 50}}, + "snapshot_entities": ["light.my_light"], + }, + blocking=True, + ) + await hass.async_block_till_done() + assert "entities and snapshot_entities must not overlap" in str(ex.value) + assert hass.states.get("scene.hallo") is None diff --git a/tests/components/homekit/conftest.py b/tests/components/homekit/conftest.py index c7e749fbad9..ef534d0e472 100644 --- a/tests/components/homekit/conftest.py +++ b/tests/components/homekit/conftest.py @@ -1,13 +1,12 @@ """HomeKit session fixtures.""" from unittest.mock import patch +from pyhap.accessory_driver import AccessoryDriver import pytest from homeassistant.components.homekit.const import EVENT_HOMEKIT_CHANGED from homeassistant.core import callback as ha_callback -from pyhap.accessory_driver import AccessoryDriver - @pytest.fixture(scope="session") def hk_driver(): diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index fcc0e05b570..f67e0e2478d 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -3,15 +3,15 @@ This includes tests for all mock object types. """ from datetime import datetime, timedelta -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch import pytest from homeassistant.components.homekit.accessories import ( - debounce, HomeAccessory, HomeBridge, HomeDriver, + debounce, ) from homeassistant.components.homekit.const import ( ATTR_DISPLAY_NAME, @@ -30,13 +30,13 @@ from homeassistant.components.homekit.const import ( SERV_ACCESSORY_INFO, ) from homeassistant.const import ( - __version__, ATTR_BATTERY_CHARGING, ATTR_BATTERY_LEVEL, ATTR_ENTITY_ID, - ATTR_SERVICE, ATTR_NOW, + ATTR_SERVICE, EVENT_TIME_CHANGED, + __version__, ) import homeassistant.util.dt as dt_util @@ -97,7 +97,7 @@ async def test_home_accessory(hass, hk_driver): hass.states.async_set(entity_id, "on") await hass.async_block_till_done() with patch( - "homeassistant.components.homekit.accessories." "HomeAccessory.update_state" + "homeassistant.components.homekit.accessories.HomeAccessory.update_state" ) as mock_update_state: await hass.async_add_job(acc.run) await hass.async_block_till_done() @@ -245,6 +245,33 @@ async def test_linked_battery_sensor(hass, hk_driver, caplog): assert acc._char_charging.value == 0 +async def test_missing_linked_battery_sensor(hass, hk_driver, caplog): + """Test battery service with mising linked_battery_sensor.""" + entity_id = "homekit.accessory" + linked_battery = "sensor.battery" + hass.states.async_set(entity_id, "open") + await hass.async_block_till_done() + + acc = HomeAccessory( + hass, + hk_driver, + "Battery Service", + entity_id, + 2, + {CONF_LINKED_BATTERY_SENSOR: linked_battery}, + ) + acc.update_state = lambda x: None + assert not acc.linked_battery_sensor + + await hass.async_add_job(acc.run) + await hass.async_block_till_done() + + assert not acc.linked_battery_sensor + assert not hasattr(acc, "_char_battery") + assert not hasattr(acc, "_char_low_battery") + assert not hasattr(acc, "_char_charging") + + async def test_call_service(hass, hk_driver, events): """Test call_service method.""" entity_id = "homekit.accessory" @@ -316,7 +343,7 @@ def test_home_driver(): # pair with patch("pyhap.accessory_driver.AccessoryDriver.pair") as mock_pair, patch( - "homeassistant.components.homekit.accessories." "dismiss_setup_message" + "homeassistant.components.homekit.accessories.dismiss_setup_message" ) as mock_dissmiss_msg: driver.pair("client_uuid", "client_public") @@ -325,7 +352,7 @@ def test_home_driver(): # unpair with patch("pyhap.accessory_driver.AccessoryDriver.unpair") as mock_unpair, patch( - "homeassistant.components.homekit.accessories." "show_setup_message" + "homeassistant.components.homekit.accessories.show_setup_message" ) as mock_show_msg: driver.unpair("client_uuid") diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index eb15b461461..e6bf185c93b 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -1,13 +1,11 @@ """Package to test the get_accessory method.""" -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch import pytest -from homeassistant.core import State -import homeassistant.components.cover as cover import homeassistant.components.climate as climate -import homeassistant.components.media_player.const as media_player_c -from homeassistant.components.homekit import get_accessory, TYPES +import homeassistant.components.cover as cover +from homeassistant.components.homekit import TYPES, get_accessory from homeassistant.components.homekit.const import ( CONF_FEATURE_LIST, FEATURE_ON_OFF, @@ -18,6 +16,7 @@ from homeassistant.components.homekit.const import ( TYPE_SWITCH, TYPE_VALVE, ) +import homeassistant.components.media_player.const as media_player_c from homeassistant.const import ( ATTR_CODE, ATTR_DEVICE_CLASS, @@ -28,6 +27,7 @@ from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, ) +from homeassistant.core import State def test_not_supported(caplog): diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 97838eaa852..de6aaf0f11e 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -1,35 +1,34 @@ """Tests for the HomeKit component.""" -from unittest.mock import patch, ANY, Mock +from unittest.mock import ANY, Mock, patch import pytest from homeassistant import setup - from homeassistant.components.homekit import ( - generate_aid, - HomeKit, MAX_DEVICES, STATUS_READY, STATUS_RUNNING, STATUS_STOPPED, STATUS_WAIT, + HomeKit, + generate_aid, ) from homeassistant.components.homekit.accessories import HomeBridge from homeassistant.components.homekit.const import ( + BRIDGE_NAME, CONF_AUTO_START, CONF_SAFE_MODE, - BRIDGE_NAME, DEFAULT_PORT, DEFAULT_SAFE_MODE, DOMAIN, HOMEKIT_FILE, - SERVICE_HOMEKIT_START, SERVICE_HOMEKIT_RESET_ACCESSORY, + SERVICE_HOMEKIT_START, ) from homeassistant.const import ( ATTR_ENTITY_ID, - CONF_NAME, CONF_IP_ADDRESS, + CONF_NAME, CONF_PORT, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, @@ -39,7 +38,6 @@ from homeassistant.helpers.entityfilter import generate_filter from tests.components.homekit.common import patch_debounce - IP_ADDRESS = "127.0.0.1" PATH_HOMEKIT = "homeassistant.components.homekit" diff --git a/tests/components/homekit/test_type_fans.py b/tests/components/homekit/test_type_fans.py index 781df6cfb7b..5631791d7a2 100644 --- a/tests/components/homekit/test_type_fans.py +++ b/tests/components/homekit/test_type_fans.py @@ -197,7 +197,10 @@ async def test_fan_speed(hass, hk_driver, cls, events): ) await hass.async_block_till_done() acc = cls.fan(hass, hk_driver, "Fan", entity_id, 2, None) - assert acc.char_speed.value == 0 + + # 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 hass.async_add_job(acc.run) assert ( diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index 784a6c82346..510cfa4f666 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -11,14 +11,14 @@ from homeassistant.components.light import ( ATTR_HS_COLOR, DOMAIN, SUPPORT_BRIGHTNESS, - SUPPORT_COLOR_TEMP, SUPPORT_COLOR, + SUPPORT_COLOR_TEMP, ) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, - STATE_ON, STATE_OFF, + STATE_ON, STATE_UNKNOWN, ) @@ -101,7 +101,9 @@ async def test_light_brightness(hass, hk_driver, cls, events): await hass.async_block_till_done() acc = cls.light(hass, hk_driver, "Light", entity_id, 2, None) - assert acc.char_brightness.value == 0 + # Initial value can be anything but 0. If it is 0, it might cause HomeKit to set the + # brightness to 100 when turning on a light on a freshly booted up server. + assert acc.char_brightness.value != 0 await hass.async_add_job(acc.run) await hass.async_block_till_done() diff --git a/tests/components/homekit/test_type_media_players.py b/tests/components/homekit/test_type_media_players.py index f2682fc86b0..aa007b4d04c 100644 --- a/tests/components/homekit/test_type_media_players.py +++ b/tests/components/homekit/test_type_media_players.py @@ -8,11 +8,11 @@ from homeassistant.components.homekit.const import ( FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE, ) -from homeassistant.components.media_player import DEVICE_CLASS_TV from homeassistant.components.homekit.type_media_players import ( MediaPlayer, TelevisionMediaPlayer, ) +from homeassistant.components.media_player import DEVICE_CLASS_TV from homeassistant.components.media_player.const import ( ATTR_INPUT_SOURCE, ATTR_INPUT_SOURCE_LIST, diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py index c2ed4873286..43533840cc6 100644 --- a/tests/components/homekit/test_type_sensors.py +++ b/tests/components/homekit/test_type_sensors.py @@ -5,14 +5,14 @@ from homeassistant.components.homekit.const import ( THRESHOLD_CO2, ) from homeassistant.components.homekit.type_sensors import ( + BINARY_SENSOR_SERVICE_MAP, AirQualitySensor, BinarySensor, - CarbonMonoxideSensor, CarbonDioxideSensor, + CarbonMonoxideSensor, HumiditySensor, LightSensor, TemperatureSensor, - BINARY_SENSOR_SERVICE_MAP, ) from homeassistant.const import ( ATTR_DEVICE_CLASS, diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index c896ad211e8..174b72f780a 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -20,7 +20,9 @@ from homeassistant.components.climate.const import ( DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, DOMAIN as DOMAIN_CLIMATE, + HVAC_MODE_AUTO, HVAC_MODE_COOL, + HVAC_MODE_FAN_ONLY, HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, @@ -64,7 +66,20 @@ async def test_thermostat(hass, hk_driver, cls, events): """Test if accessory and HA are updated accordingly.""" entity_id = "climate.test" - hass.states.async_set(entity_id, HVAC_MODE_OFF) + hass.states.async_set( + entity_id, + HVAC_MODE_OFF, + { + ATTR_HVAC_MODES: [ + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_COOL, + HVAC_MODE_OFF, + HVAC_MODE_AUTO, + ], + }, + ) await hass.async_block_till_done() acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 2, None) await hass.async_add_job(acc.run) @@ -120,7 +135,7 @@ async def test_thermostat(hass, hk_driver, cls, events): hass.states.async_set( entity_id, - HVAC_MODE_COOL, + HVAC_MODE_FAN_ONLY, { ATTR_TEMPERATURE: 20.0, ATTR_CURRENT_TEMPERATURE: 25.0, @@ -164,9 +179,8 @@ async def test_thermostat(hass, hk_driver, cls, events): hass.states.async_set( entity_id, - HVAC_MODE_HEAT_COOL, + HVAC_MODE_AUTO, { - ATTR_HVAC_MODES: [HVAC_MODE_HEAT, HVAC_MODE_COOL], ATTR_TEMPERATURE: 22.0, ATTR_CURRENT_TEMPERATURE: 18.0, ATTR_HVAC_ACTION: CURRENT_HVAC_HEAT, @@ -183,7 +197,6 @@ async def test_thermostat(hass, hk_driver, cls, events): entity_id, HVAC_MODE_HEAT_COOL, { - ATTR_HVAC_MODES: [HVAC_MODE_HEAT, HVAC_MODE_COOL], ATTR_TEMPERATURE: 22.0, ATTR_CURRENT_TEMPERATURE: 25.0, ATTR_HVAC_ACTION: CURRENT_HVAC_COOL, @@ -198,9 +211,8 @@ async def test_thermostat(hass, hk_driver, cls, events): hass.states.async_set( entity_id, - HVAC_MODE_HEAT_COOL, + HVAC_MODE_AUTO, { - ATTR_HVAC_MODES: [HVAC_MODE_HEAT, HVAC_MODE_COOL], ATTR_TEMPERATURE: 22.0, ATTR_CURRENT_TEMPERATURE: 22.0, ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE, @@ -226,14 +238,23 @@ async def test_thermostat(hass, hk_driver, cls, events): assert len(events) == 1 assert events[-1].data[ATTR_VALUE] == "19.0°C" - await hass.async_add_job(acc.char_target_heat_cool.client_update_value, 1) + await hass.async_add_job(acc.char_target_heat_cool.client_update_value, 2) await hass.async_block_till_done() assert call_set_hvac_mode assert call_set_hvac_mode[0].data[ATTR_ENTITY_ID] == entity_id - assert call_set_hvac_mode[0].data[ATTR_HVAC_MODE] == HVAC_MODE_HEAT - assert acc.char_target_heat_cool.value == 1 + assert call_set_hvac_mode[0].data[ATTR_HVAC_MODE] == HVAC_MODE_COOL + assert acc.char_target_heat_cool.value == 2 assert len(events) == 2 - assert events[-1].data[ATTR_VALUE] == HVAC_MODE_HEAT + assert events[-1].data[ATTR_VALUE] == HVAC_MODE_COOL + + await hass.async_add_job(acc.char_target_heat_cool.client_update_value, 3) + await hass.async_block_till_done() + assert call_set_hvac_mode + assert call_set_hvac_mode[1].data[ATTR_ENTITY_ID] == entity_id + assert call_set_hvac_mode[1].data[ATTR_HVAC_MODE] == HVAC_MODE_AUTO + assert acc.char_target_heat_cool.value == 3 + assert len(events) == 3 + assert events[-1].data[ATTR_VALUE] == HVAC_MODE_AUTO async def test_thermostat_auto(hass, hk_driver, cls, events): @@ -261,7 +282,6 @@ async def test_thermostat_auto(hass, hk_driver, cls, events): entity_id, HVAC_MODE_HEAT_COOL, { - ATTR_HVAC_MODE: HVAC_MODE_HEAT_COOL, ATTR_TARGET_TEMP_HIGH: 22.0, ATTR_TARGET_TEMP_LOW: 20.0, ATTR_CURRENT_TEMPERATURE: 18.0, @@ -278,9 +298,8 @@ async def test_thermostat_auto(hass, hk_driver, cls, events): hass.states.async_set( entity_id, - HVAC_MODE_HEAT_COOL, + HVAC_MODE_COOL, { - ATTR_HVAC_MODE: HVAC_MODE_HEAT_COOL, ATTR_TARGET_TEMP_HIGH: 23.0, ATTR_TARGET_TEMP_LOW: 19.0, ATTR_CURRENT_TEMPERATURE: 24.0, @@ -291,15 +310,14 @@ async def test_thermostat_auto(hass, hk_driver, cls, events): assert acc.char_heating_thresh_temp.value == 19.0 assert acc.char_cooling_thresh_temp.value == 23.0 assert acc.char_current_heat_cool.value == 2 - assert acc.char_target_heat_cool.value == 3 + assert acc.char_target_heat_cool.value == 2 assert acc.char_current_temp.value == 24.0 assert acc.char_display_units.value == 0 hass.states.async_set( entity_id, - HVAC_MODE_HEAT_COOL, + HVAC_MODE_AUTO, { - ATTR_HVAC_MODE: HVAC_MODE_HEAT_COOL, ATTR_TARGET_TEMP_HIGH: 23.0, ATTR_TARGET_TEMP_LOW: 19.0, ATTR_CURRENT_TEMPERATURE: 21.0, @@ -346,7 +364,6 @@ async def test_thermostat_power_state(hass, hk_driver, cls, events): HVAC_MODE_HEAT, { ATTR_SUPPORTED_FEATURES: 4096, - ATTR_HVAC_MODE: HVAC_MODE_HEAT, ATTR_TEMPERATURE: 23.0, ATTR_CURRENT_TEMPERATURE: 18.0, ATTR_HVAC_ACTION: CURRENT_HVAC_HEAT, @@ -364,7 +381,6 @@ async def test_thermostat_power_state(hass, hk_driver, cls, events): entity_id, HVAC_MODE_OFF, { - ATTR_HVAC_MODE: HVAC_MODE_HEAT, ATTR_TEMPERATURE: 23.0, ATTR_CURRENT_TEMPERATURE: 18.0, ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE, @@ -378,7 +394,6 @@ async def test_thermostat_power_state(hass, hk_driver, cls, events): entity_id, HVAC_MODE_OFF, { - ATTR_HVAC_MODE: HVAC_MODE_OFF, ATTR_TEMPERATURE: 23.0, ATTR_CURRENT_TEMPERATURE: 18.0, ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE, @@ -423,7 +438,6 @@ async def test_thermostat_fahrenheit(hass, hk_driver, cls, events): entity_id, HVAC_MODE_HEAT_COOL, { - ATTR_HVAC_MODE: HVAC_MODE_HEAT_COOL, ATTR_TARGET_TEMP_HIGH: 75.2, ATTR_TARGET_TEMP_LOW: 68.1, ATTR_TEMPERATURE: 71.6, @@ -503,6 +517,34 @@ async def test_thermostat_temperature_step_whole(hass, hk_driver, cls): assert acc.char_target_temp.properties[PROP_MIN_STEP] == 1.0 +async def test_thermostat_hvac_modes(hass, hk_driver, cls): + """Test if unsupported HVAC modes are deactivated in HomeKit.""" + entity_id = "climate.test" + + hass.states.async_set( + entity_id, HVAC_MODE_OFF, {ATTR_HVAC_MODES: [HVAC_MODE_HEAT, HVAC_MODE_OFF]} + ) + + await hass.async_block_till_done() + acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 2, None) + await hass.async_add_job(acc.run) + await hass.async_block_till_done() + + with pytest.raises(ValueError): + await hass.async_add_job(acc.char_target_heat_cool.set_value, 3) + await hass.async_block_till_done() + assert acc.char_target_heat_cool.value == 0 + + await hass.async_add_job(acc.char_target_heat_cool.set_value, 1) + await hass.async_block_till_done() + assert acc.char_target_heat_cool.value == 1 + + with pytest.raises(ValueError): + await hass.async_add_job(acc.char_target_heat_cool.set_value, 2) + await hass.async_block_till_done() + assert acc.char_target_heat_cool.value == 1 + + async def test_water_heater(hass, hk_driver, cls, events): """Test if accessory and HA are updated accordingly.""" entity_id = "water_heater.test" @@ -571,7 +613,8 @@ async def test_water_heater(hass, hk_driver, cls, events): await hass.async_block_till_done() assert acc.char_target_heat_cool.value == 1 - await hass.async_add_job(acc.char_target_heat_cool.client_update_value, 3) + with pytest.raises(ValueError): + await hass.async_add_job(acc.char_target_heat_cool.set_value, 3) await hass.async_block_till_done() assert acc.char_target_heat_cool.value == 1 diff --git a/tests/components/homekit_controller/common.py b/tests/components/homekit_controller/common.py index 2e1cfd7a77d..b743f84f73c 100644 --- a/tests/components/homekit_controller/common.py +++ b/tests/components/homekit_controller/common.py @@ -1,28 +1,29 @@ """Code to support homekit_controller tests.""" +from datetime import timedelta import json import os -from datetime import timedelta from unittest import mock -from homekit.model.services import AbstractService, ServicesTypes +from homekit.exceptions import AccessoryNotFoundError +from homekit.model import Accessory, get_id from homekit.model.characteristics import ( AbstractCharacteristic, CharacteristicPermissions, CharacteristicsTypes, ) -from homekit.model import Accessory, get_id -from homekit.exceptions import AccessoryNotFoundError +from homekit.model.services import AbstractService, ServicesTypes from homeassistant import config_entries +from homeassistant.components.homekit_controller import config_flow from homeassistant.components.homekit_controller.const import ( CONTROLLER, DOMAIN, HOMEKIT_ACCESSORY_DISPATCH, ) -from homeassistant.components.homekit_controller import config_flow from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.common import async_fire_time_changed, load_fixture, MockConfigEntry + +from tests.common import MockConfigEntry, async_fire_time_changed, load_fixture class FakePairing: @@ -250,7 +251,7 @@ async def setup_test_accessories(hass, accessories): config_entry.add_to_hass(hass) - pairing_cls_loc = "homekit.controller.ip_implementation.IpPairing" + pairing_cls_loc = "homeassistant.components.homekit_controller.connection.IpPairing" with mock.patch(pairing_cls_loc) as pairing_cls: pairing_cls.return_value = pairing await config_entry.async_setup(hass) 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 6e41a88b299..292c4169688 100644 --- a/tests/components/homekit_controller/specific_devices/test_aqara_gateway.py +++ b/tests/components/homekit_controller/specific_devices/test_aqara_gateway.py @@ -5,10 +5,11 @@ https://github.com/home-assistant/home-assistant/issues/20957 """ from homeassistant.components.light import SUPPORT_BRIGHTNESS, SUPPORT_COLOR + from tests.components.homekit_controller.common import ( + Helper, setup_accessories_from_file, setup_test_accessories, - Helper, ) diff --git a/tests/components/homekit_controller/specific_devices/test_ecobee3.py b/tests/components/homekit_controller/specific_devices/test_ecobee3.py index 8473d235278..bb7695840f0 100644 --- a/tests/components/homekit_controller/specific_devices/test_ecobee3.py +++ b/tests/components/homekit_controller/specific_devices/test_ecobee3.py @@ -8,19 +8,18 @@ from unittest import mock from homekit import AccessoryDisconnectedError -from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY from homeassistant.components.climate.const import ( - SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_HUMIDITY, + SUPPORT_TARGET_TEMPERATURE, ) - +from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY from tests.components.homekit_controller.common import ( FakePairing, + Helper, device_config_changed, setup_accessories_from_file, setup_test_accessories, - Helper, time_changed, ) @@ -154,7 +153,7 @@ async def test_ecobee3_setup_connection_failure(hass): # make sure the IpPairing mock is in place or we'll try to connect to # a real device. Normally this mocking is done by the helper in # setup_test_accessories. - pairing_cls_loc = "homekit.controller.ip_implementation.IpPairing" + pairing_cls_loc = "homeassistant.components.homekit_controller.connection.IpPairing" with mock.patch(pairing_cls_loc) as pairing_cls: pairing_cls.return_value = pairing await time_changed(hass, 5 * 60) 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 f3e4baa4b1f..0b6ebc00eba 100644 --- a/tests/components/homekit_controller/specific_devices/test_hue_bridge.py +++ b/tests/components/homekit_controller/specific_devices/test_hue_bridge.py @@ -1,9 +1,9 @@ """Tests for handling accessories on a Hue bridge via HomeKit.""" from tests.components.homekit_controller.common import ( + Helper, setup_accessories_from_file, setup_test_accessories, - Helper, ) 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 89c5bb4de3c..52339bb6635 100644 --- a/tests/components/homekit_controller/specific_devices/test_koogeek_ls1.py +++ b/tests/components/homekit_controller/specific_devices/test_koogeek_ls1.py @@ -3,17 +3,18 @@ from datetime import timedelta from unittest import mock +from homekit.exceptions import AccessoryDisconnectedError, EncryptionError import pytest -from homekit.exceptions import AccessoryDisconnectedError, EncryptionError -import homeassistant.util.dt as dt_util from homeassistant.components.light import SUPPORT_BRIGHTNESS, SUPPORT_COLOR +import homeassistant.util.dt as dt_util + from tests.common import async_fire_time_changed from tests.components.homekit_controller.common import ( - setup_accessories_from_file, - setup_test_accessories, FakePairing, Helper, + setup_accessories_from_file, + setup_test_accessories, ) LIGHT_ON = ("lightbulb", "on") 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 ef0d35166db..3209139ae1e 100644 --- a/tests/components/homekit_controller/specific_devices/test_lennox_e30.py +++ b/tests/components/homekit_controller/specific_devices/test_lennox_e30.py @@ -5,10 +5,11 @@ https://github.com/home-assistant/home-assistant/issues/20885 """ from homeassistant.components.climate.const import SUPPORT_TARGET_TEMPERATURE + from tests.components.homekit_controller.common import ( + Helper, setup_accessories_from_file, setup_test_accessories, - Helper, ) diff --git a/tests/components/homekit_controller/test_binary_sensor.py b/tests/components/homekit_controller/test_binary_sensor.py index 1b73021e44c..f472ac38d1d 100644 --- a/tests/components/homekit_controller/test_binary_sensor.py +++ b/tests/components/homekit_controller/test_binary_sensor.py @@ -3,6 +3,7 @@ from tests.components.homekit_controller.common import FakeService, setup_test_c MOTION_DETECTED = ("motion", "motion-detected") CONTACT_STATE = ("contact", "contact-state") +SMOKE_DETECTED = ("smoke", "smoke-detected") def create_motion_sensor_service(): @@ -51,3 +52,29 @@ async def test_contact_sensor_read_state(hass, utcnow): helper.characteristics[CONTACT_STATE].value = 1 state = await helper.poll_and_get_state() assert state.state == "on" + + +def create_smoke_sensor_service(): + """Define smoke sensor characteristics.""" + service = FakeService("public.hap.service.sensor.smoke") + + cur_state = service.add_characteristic("smoke-detected") + cur_state.value = 0 + + return service + + +async def test_smoke_sensor_read_state(hass, utcnow): + """Test that we can read the state of a HomeKit contact accessory.""" + sensor = create_smoke_sensor_service() + helper = await setup_test_component(hass, [sensor]) + + helper.characteristics[SMOKE_DETECTED].value = 0 + state = await helper.poll_and_get_state() + assert state.state == "off" + + helper.characteristics[SMOKE_DETECTED].value = 1 + state = await helper.poll_and_get_state() + assert state.state == "on" + + assert state.attributes["device_class"] == "smoke" diff --git a/tests/components/homekit_controller/test_climate.py b/tests/components/homekit_controller/test_climate.py index 0d3544a6f55..e076b2975e2 100644 --- a/tests/components/homekit_controller/test_climate.py +++ b/tests/components/homekit_controller/test_climate.py @@ -1,16 +1,16 @@ """Basic checks for HomeKitclimate.""" from homeassistant.components.climate.const import ( DOMAIN, - SERVICE_SET_HVAC_MODE, - SERVICE_SET_TEMPERATURE, - HVAC_MODE_HEAT_COOL, HVAC_MODE_COOL, HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, SERVICE_SET_HUMIDITY, + SERVICE_SET_HVAC_MODE, + SERVICE_SET_TEMPERATURE, ) -from tests.components.homekit_controller.common import FakeService, setup_test_component +from tests.components.homekit_controller.common import FakeService, setup_test_component HEATING_COOLING_TARGET = ("thermostat", "heating-cooling.target") HEATING_COOLING_CURRENT = ("thermostat", "heating-cooling.current") diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index a8428375ae6..2a7f36ba470 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -7,6 +7,7 @@ import pytest from homeassistant.components.homekit_controller import config_flow from homeassistant.components.homekit_controller.const import KNOWN_DEVICES + from tests.common import MockConfigEntry from tests.components.homekit_controller.common import ( Accessory, @@ -14,7 +15,6 @@ from tests.components.homekit_controller.common import ( setup_platform, ) - PAIRING_START_FORM_ERRORS = [ (homekit.BusyError, "busy_error"), (homekit.MaxTriesError, "max_tries_error"), @@ -27,6 +27,7 @@ PAIRING_START_ABORT_ERRORS = [ ] PAIRING_FINISH_FORM_ERRORS = [ + (homekit.exceptions.MalformedPinError, "authentication_error"), (homekit.MaxPeersError, "max_peers_error"), (homekit.AuthenticationError, "authentication_error"), (homekit.UnknownError, "unknown_error"), @@ -37,6 +38,27 @@ PAIRING_FINISH_ABORT_ERRORS = [ (homekit.AccessoryNotFoundError, "accessory_not_found_error") ] +INVALID_PAIRING_CODES = [ + "aaa-aa-aaa", + "aaa-11-aaa", + "111-aa-aaa", + "aaa-aa-111", + "1111-1-111", + "a111-11-111", + " 111-11-111", + "111-11-111 ", + "111-11-111a", + "1111111", +] + + +VALID_PAIRING_CODES = [ + "111-11-111", + "123-45-678", + "11111111", + "98765432", +] + def _setup_flow_handler(hass): flow = config_flow.HomekitControllerFlowHandler() @@ -56,6 +78,23 @@ async def _setup_flow_zeroconf(hass, discovery_info): return result +@pytest.mark.parametrize("pairing_code", INVALID_PAIRING_CODES) +def test_invalid_pairing_codes(pairing_code): + """Test ensure_pin_format raises for an invalid pin code.""" + with pytest.raises(homekit.exceptions.MalformedPinError): + config_flow.ensure_pin_format(pairing_code) + + +@pytest.mark.parametrize("pairing_code", VALID_PAIRING_CODES) +def test_valid_pairing_codes(pairing_code): + """Test ensure_pin_format corrects format for a valid pin in an alternative format.""" + valid_pin = config_flow.ensure_pin_format(pairing_code).split("-") + assert len(valid_pin) == 3 + assert len(valid_pin[0]) == 3 + assert len(valid_pin[1]) == 2 + assert len(valid_pin[2]) == 3 + + async def test_discovery_works(hass): """Test a device being discovered.""" discovery_info = { @@ -74,6 +113,7 @@ async def test_discovery_works(hass): assert flow.context == { "hkid": "00:00:00:00:00:00", "title_placeholders": {"name": "TestDevice"}, + "unique_id": "00:00:00:00:00:00", } # User initiates pairing - device enters pairing mode and displays code @@ -98,7 +138,7 @@ async def test_discovery_works(hass): # Pairing doesn't error error and pairing results flow.controller.pairings = {"00:00:00:00:00:00": pairing} - result = await flow.async_step_pair({"pairing_code": "111-22-33"}) + result = await flow.async_step_pair({"pairing_code": "111-22-333"}) assert result["type"] == "create_entry" assert result["title"] == "Koogeek-LS1-20833F" assert result["data"] == pairing.pairing_data @@ -122,6 +162,7 @@ async def test_discovery_works_upper_case(hass): assert flow.context == { "hkid": "00:00:00:00:00:00", "title_placeholders": {"name": "TestDevice"}, + "unique_id": "00:00:00:00:00:00", } # User initiates pairing - device enters pairing mode and displays code @@ -145,7 +186,7 @@ async def test_discovery_works_upper_case(hass): ] flow.controller.pairings = {"00:00:00:00:00:00": pairing} - result = await flow.async_step_pair({"pairing_code": "111-22-33"}) + result = await flow.async_step_pair({"pairing_code": "111-22-333"}) assert result["type"] == "create_entry" assert result["title"] == "Koogeek-LS1-20833F" assert result["data"] == pairing.pairing_data @@ -169,6 +210,7 @@ async def test_discovery_works_missing_csharp(hass): assert flow.context == { "hkid": "00:00:00:00:00:00", "title_placeholders": {"name": "TestDevice"}, + "unique_id": "00:00:00:00:00:00", } # User initiates pairing - device enters pairing mode and displays code @@ -193,7 +235,7 @@ async def test_discovery_works_missing_csharp(hass): flow.controller.pairings = {"00:00:00:00:00:00": pairing} - result = await flow.async_step_pair({"pairing_code": "111-22-33"}) + result = await flow.async_step_pair({"pairing_code": "111-22-333"}) assert result["type"] == "create_entry" assert result["title"] == "Koogeek-LS1-20833F" assert result["data"] == pairing.pairing_data @@ -234,6 +276,7 @@ async def test_pair_already_paired_1(hass): assert flow.context == { "hkid": "00:00:00:00:00:00", "title_placeholders": {"name": "TestDevice"}, + "unique_id": "00:00:00:00:00:00", } @@ -259,6 +302,7 @@ async def test_discovery_ignored_model(hass): assert flow.context == { "hkid": "00:00:00:00:00:00", "title_placeholders": {"name": "TestDevice"}, + "unique_id": "00:00:00:00:00:00", } @@ -286,6 +330,7 @@ async def test_discovery_invalid_config_entry(hass): assert flow.context == { "hkid": "00:00:00:00:00:00", "title_placeholders": {"name": "TestDevice"}, + "unique_id": "00:00:00:00:00:00", } # Discovery of a HKID that is in a pairable state but for which there is @@ -315,10 +360,7 @@ async def test_discovery_already_configured(hass): result = await flow.async_step_zeroconf(discovery_info) assert result["type"] == "abort" assert result["reason"] == "already_configured" - assert flow.context == { - "hkid": "00:00:00:00:00:00", - "title_placeholders": {"name": "TestDevice"}, - } + assert flow.context == {} assert conn.async_config_num_changed.call_count == 0 @@ -343,10 +385,7 @@ async def test_discovery_already_configured_config_change(hass): result = await flow.async_step_zeroconf(discovery_info) assert result["type"] == "abort" assert result["reason"] == "already_configured" - assert flow.context == { - "hkid": "00:00:00:00:00:00", - "title_placeholders": {"name": "TestDevice"}, - } + assert flow.context == {} assert conn.async_refresh_entity_map.call_args == mock.call(2) @@ -369,6 +408,7 @@ async def test_pair_unable_to_pair(hass): assert flow.context == { "hkid": "00:00:00:00:00:00", "title_placeholders": {"name": "TestDevice"}, + "unique_id": "00:00:00:00:00:00", } # User initiates pairing - device enters pairing mode and displays code @@ -378,7 +418,7 @@ async def test_pair_unable_to_pair(hass): assert flow.controller.start_pairing.call_count == 1 # Pairing doesn't error but no pairing object is generated - result = await flow.async_step_pair({"pairing_code": "111-22-33"}) + result = await flow.async_step_pair({"pairing_code": "111-22-333"}) assert result["type"] == "form" assert result["errors"]["pairing_code"] == "unable_to_pair" @@ -402,6 +442,7 @@ async def test_pair_abort_errors_on_start(hass, exception, expected): assert flow.context == { "hkid": "00:00:00:00:00:00", "title_placeholders": {"name": "TestDevice"}, + "unique_id": "00:00:00:00:00:00", } # User initiates pairing - device refuses to enter pairing mode @@ -414,6 +455,7 @@ async def test_pair_abort_errors_on_start(hass, exception, expected): assert flow.context == { "hkid": "00:00:00:00:00:00", "title_placeholders": {"name": "TestDevice"}, + "unique_id": "00:00:00:00:00:00", } @@ -436,6 +478,7 @@ async def test_pair_form_errors_on_start(hass, exception, expected): assert flow.context == { "hkid": "00:00:00:00:00:00", "title_placeholders": {"name": "TestDevice"}, + "unique_id": "00:00:00:00:00:00", } # User initiates pairing - device refuses to enter pairing mode @@ -448,6 +491,7 @@ async def test_pair_form_errors_on_start(hass, exception, expected): assert flow.context == { "hkid": "00:00:00:00:00:00", "title_placeholders": {"name": "TestDevice"}, + "unique_id": "00:00:00:00:00:00", } @@ -470,6 +514,7 @@ async def test_pair_abort_errors_on_finish(hass, exception, expected): assert flow.context == { "hkid": "00:00:00:00:00:00", "title_placeholders": {"name": "TestDevice"}, + "unique_id": "00:00:00:00:00:00", } # User initiates pairing - device enters pairing mode and displays code @@ -480,12 +525,13 @@ async def test_pair_abort_errors_on_finish(hass, exception, expected): # User submits code - pairing fails but can be retried flow.finish_pairing.side_effect = exception("error") - result = await flow.async_step_pair({"pairing_code": "111-22-33"}) + result = await flow.async_step_pair({"pairing_code": "111-22-333"}) assert result["type"] == "abort" assert result["reason"] == expected assert flow.context == { "hkid": "00:00:00:00:00:00", "title_placeholders": {"name": "TestDevice"}, + "unique_id": "00:00:00:00:00:00", } @@ -508,6 +554,7 @@ async def test_pair_form_errors_on_finish(hass, exception, expected): assert flow.context == { "hkid": "00:00:00:00:00:00", "title_placeholders": {"name": "TestDevice"}, + "unique_id": "00:00:00:00:00:00", } # User initiates pairing - device enters pairing mode and displays code @@ -518,12 +565,13 @@ async def test_pair_form_errors_on_finish(hass, exception, expected): # User submits code - pairing fails but can be retried flow.finish_pairing.side_effect = exception("error") - result = await flow.async_step_pair({"pairing_code": "111-22-33"}) + result = await flow.async_step_pair({"pairing_code": "111-22-333"}) assert result["type"] == "form" assert result["errors"]["pairing_code"] == expected assert flow.context == { "hkid": "00:00:00:00:00:00", "title_placeholders": {"name": "TestDevice"}, + "unique_id": "00:00:00:00:00:00", } @@ -554,7 +602,9 @@ async def test_import_works(hass): flow = _setup_flow_handler(hass) - pairing_cls_imp = "homekit.controller.ip_implementation.IpPairing" + pairing_cls_imp = ( + "homeassistant.components.homekit_controller.config_flow.IpPairing" + ) with mock.patch(pairing_cls_imp) as pairing_cls: pairing_cls.return_value = pairing @@ -628,7 +678,7 @@ async def test_user_works(hass): assert result["type"] == "form" assert result["step_id"] == "pair" - result = await flow.async_step_pair({"pairing_code": "111-22-33"}) + result = await flow.async_step_pair({"pairing_code": "111-22-333"}) assert result["type"] == "create_entry" assert result["title"] == "Koogeek-LS1-20833F" assert result["data"] == pairing.pairing_data @@ -694,7 +744,9 @@ async def test_parse_new_homekit_json(hass): flow = _setup_flow_handler(hass) - pairing_cls_imp = "homekit.controller.ip_implementation.IpPairing" + pairing_cls_imp = ( + "homeassistant.components.homekit_controller.config_flow.IpPairing" + ) with mock.patch(pairing_cls_imp) as pairing_cls: pairing_cls.return_value = pairing @@ -708,6 +760,7 @@ async def test_parse_new_homekit_json(hass): assert flow.context == { "hkid": "00:00:00:00:00:00", "title_placeholders": {"name": "TestDevice"}, + "unique_id": "00:00:00:00:00:00", } @@ -742,7 +795,9 @@ async def test_parse_old_homekit_json(hass): flow = _setup_flow_handler(hass) - pairing_cls_imp = "homekit.controller.ip_implementation.IpPairing" + pairing_cls_imp = ( + "homeassistant.components.homekit_controller.config_flow.IpPairing" + ) with mock.patch(pairing_cls_imp) as pairing_cls: pairing_cls.return_value = pairing @@ -757,6 +812,7 @@ async def test_parse_old_homekit_json(hass): assert flow.context == { "hkid": "00:00:00:00:00:00", "title_placeholders": {"name": "TestDevice"}, + "unique_id": "00:00:00:00:00:00", } @@ -798,7 +854,9 @@ async def test_parse_overlapping_homekit_json(hass): flow = _setup_flow_handler(hass) - pairing_cls_imp = "homekit.controller.ip_implementation.IpPairing" + pairing_cls_imp = ( + "homeassistant.components.homekit_controller.config_flow.IpPairing" + ) with mock.patch(pairing_cls_imp) as pairing_cls: pairing_cls.return_value = pairing @@ -815,4 +873,87 @@ async def test_parse_overlapping_homekit_json(hass): assert flow.context == { "hkid": "00:00:00:00:00:00", "title_placeholders": {"name": "TestDevice"}, + "unique_id": "00:00:00:00:00:00", + } + + +async def test_unignore_works(hass): + """Test rediscovery triggered disovers work.""" + discovery_info = { + "name": "TestDevice", + "address": "127.0.0.1", + "port": 8080, + "md": "TestDevice", + "pv": "1.0", + "id": "00:00:00:00:00:00", + "c#": 1, + "s#": 1, + "ff": 0, + "ci": 0, + "sf": 1, + } + + pairing = mock.Mock(pairing_data={"AccessoryPairingID": "00:00:00:00:00:00"}) + pairing.list_accessories_and_characteristics.return_value = [ + { + "aid": 1, + "services": [ + { + "characteristics": [{"type": "23", "value": "Koogeek-LS1-20833F"}], + "type": "3e", + } + ], + } + ] + + flow = _setup_flow_handler(hass) + + flow.controller.pairings = {"00:00:00:00:00:00": pairing} + flow.controller.discover.return_value = [discovery_info] + + result = await flow.async_step_unignore({"unique_id": "00:00:00:00:00:00"}) + assert result["type"] == "form" + assert result["step_id"] == "pair" + assert flow.context == { + "hkid": "00:00:00:00:00:00", + "title_placeholders": {"name": "TestDevice"}, + "unique_id": "00:00:00:00:00:00", + } + + # User initiates pairing by clicking on 'configure' - device enters pairing mode and displays code + result = await flow.async_step_pair({}) + assert result["type"] == "form" + assert result["step_id"] == "pair" + assert flow.controller.start_pairing.call_count == 1 + + # Pairing finalized + result = await flow.async_step_pair({"pairing_code": "111-22-333"}) + assert result["type"] == "create_entry" + assert result["title"] == "Koogeek-LS1-20833F" + assert result["data"] == pairing.pairing_data + + +async def test_unignore_ignores_missing_devices(hass): + """Test rediscovery triggered disovers handle devices that have gone away.""" + discovery_info = { + "name": "TestDevice", + "address": "127.0.0.1", + "port": 8080, + "md": "TestDevice", + "pv": "1.0", + "id": "00:00:00:00:00:00", + "c#": 1, + "s#": 1, + "ff": 0, + "ci": 0, + "sf": 1, + } + + flow = _setup_flow_handler(hass) + flow.controller.discover.return_value = [discovery_info] + + result = await flow.async_step_unignore({"unique_id": "00:00:00:00:00:01"}) + assert result["type"] == "abort" + assert flow.context == { + "unique_id": "00:00:00:00:00:01", } diff --git a/tests/components/homekit_controller/test_light.py b/tests/components/homekit_controller/test_light.py index 1608f097ed0..b558160a9f2 100644 --- a/tests/components/homekit_controller/test_light.py +++ b/tests/components/homekit_controller/test_light.py @@ -3,7 +3,6 @@ from homeassistant.components.homekit_controller.const import KNOWN_DEVICES from tests.components.homekit_controller.common import FakeService, setup_test_component - LIGHT_ON = ("lightbulb", "on") LIGHT_BRIGHTNESS = ("lightbulb", "brightness") LIGHT_HUE = ("lightbulb", "hue") diff --git a/tests/components/homekit_controller/test_storage.py b/tests/components/homekit_controller/test_storage.py index 4fcf035ae48..39b0d9d8250 100644 --- a/tests/components/homekit_controller/test_storage.py +++ b/tests/components/homekit_controller/test_storage.py @@ -1,15 +1,15 @@ """Basic checks for entity map storage.""" -from tests.common import flush_store -from tests.components.homekit_controller.common import ( - FakeService, - setup_test_component, - setup_platform, -) - from homeassistant import config_entries from homeassistant.components.homekit_controller import async_remove_entry from homeassistant.components.homekit_controller.const import ENTITY_MAP +from tests.common import flush_store +from tests.components.homekit_controller.common import ( + FakeService, + setup_platform, + setup_test_component, +) + async def test_load_from_storage(hass, hass_storage): """Test that entity map can be correctly loaded from cache.""" diff --git a/tests/components/homematic/test_notify.py b/tests/components/homematic/test_notify.py index 967e217f1a7..411be41eb39 100644 --- a/tests/components/homematic/test_notify.py +++ b/tests/components/homematic/test_notify.py @@ -2,8 +2,9 @@ import unittest -from homeassistant.setup import setup_component import homeassistant.components.notify as notify_comp +from homeassistant.setup import setup_component + from tests.common import assert_setup_component, get_test_home_assistant diff --git a/tests/components/homematicip_cloud/conftest.py b/tests/components/homematicip_cloud/conftest.py index f60f8d659b5..fa19f573c7c 100644 --- a/tests/components/homematicip_cloud/conftest.py +++ b/tests/components/homematicip_cloud/conftest.py @@ -27,7 +27,9 @@ def mock_connection_fixture() -> AsyncConnection: def _rest_call_side_effect(path, body=None): return path, body - connection._restCall.side_effect = _rest_call_side_effect # pylint: disable=W0212 + connection._restCall.side_effect = ( # pylint: disable=protected-access + _rest_call_side_effect + ) connection.api_call.return_value = mock_coro(True) connection.init.side_effect = mock_coro(True) diff --git a/tests/components/homematicip_cloud/helper.py b/tests/components/homematicip_cloud/helper.py index 494ec2dc90b..42ff2061698 100644 --- a/tests/components/homematicip_cloud/helper.py +++ b/tests/components/homematicip_cloud/helper.py @@ -58,7 +58,9 @@ async def async_manipulate_test_data( fire_target = hmip_device if fire_device is None else fire_device if isinstance(fire_target, AsyncHome): - fire_target.fire_update_event(fire_target._rawJSONData) # pylint: disable=W0212 + fire_target.fire_update_event( + fire_target._rawJSONData # pylint: disable=protected-access + ) else: fire_target.fire_update_event() @@ -136,7 +138,9 @@ class HomeTemplate(Home): def _get_mock(instance): """Create a mock and copy instance attributes over mock.""" if isinstance(instance, Mock): - instance.__dict__.update(instance._mock_wraps.__dict__) # pylint: disable=W0212 + instance.__dict__.update( + instance._mock_wraps.__dict__ # pylint: disable=protected-access + ) return instance mock = Mock(spec=instance, wraps=instance) diff --git a/tests/components/homematicip_cloud/test_alarm_control_panel.py b/tests/components/homematicip_cloud/test_alarm_control_panel.py index 78bc0a09ea5..cf85e805143 100644 --- a/tests/components/homematicip_cloud/test_alarm_control_panel.py +++ b/tests/components/homematicip_cloud/test_alarm_control_panel.py @@ -18,7 +18,7 @@ async def _async_manipulate_security_zones( hass, home, internal_active=False, external_active=False, alarm_triggered=False ): """Set new values on hmip security zones.""" - json = home._rawJSONData # pylint: disable=W0212 + json = home._rawJSONData # pylint: disable=protected-access json["functionalHomes"]["SECURITY_AND_ALARM"]["alarmActive"] = alarm_triggered external_zone_id = json["functionalHomes"]["SECURITY_AND_ALARM"]["securityZones"][ "EXTERNAL" diff --git a/tests/components/homematicip_cloud/test_climate.py b/tests/components/homematicip_cloud/test_climate.py index 2b233a6dee2..db052929474 100644 --- a/tests/components/homematicip_cloud/test_climate.py +++ b/tests/components/homematicip_cloud/test_climate.py @@ -7,8 +7,11 @@ from homematicip.functionalHomes import IndoorClimateHome from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN from homeassistant.components.climate.const import ( ATTR_CURRENT_TEMPERATURE, + ATTR_HVAC_ACTION, ATTR_PRESET_MODE, ATTR_PRESET_MODES, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_HEAT, @@ -215,6 +218,17 @@ async def test_hmip_heating_group_heat(hass, default_mock_hap): # Only fire event from last async_manipulate_test_data available. assert hmip_device.mock_calls[-1][0] == "fire_update_event" + await async_manipulate_test_data(hass, hmip_device, "floorHeatingMode", "RADIATOR") + await async_manipulate_test_data(hass, hmip_device, "valvePosition", 0.1) + ha_state = hass.states.get(entity_id) + assert ha_state.state == HVAC_MODE_AUTO + assert ha_state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_HEAT + await async_manipulate_test_data(hass, hmip_device, "floorHeatingMode", "RADIATOR") + await async_manipulate_test_data(hass, hmip_device, "valvePosition", 0.0) + ha_state = hass.states.get(entity_id) + assert ha_state.state == HVAC_MODE_AUTO + assert ha_state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE + async def test_hmip_heating_group_cool(hass, default_mock_hap): """Test HomematicipHeatingGroup.""" @@ -367,7 +381,7 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): ) assert home.mock_calls[-1][0] == "activate_absence_with_duration" assert home.mock_calls[-1][1] == (60,) - assert len(home._connection.mock_calls) == 1 # pylint: disable=W0212 + assert len(home._connection.mock_calls) == 1 # pylint: disable=protected-access await hass.services.async_call( "homematicip_cloud", @@ -377,7 +391,7 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): ) assert home.mock_calls[-1][0] == "activate_absence_with_duration" assert home.mock_calls[-1][1] == (60,) - assert len(home._connection.mock_calls) == 2 # pylint: disable=W0212 + assert len(home._connection.mock_calls) == 2 # pylint: disable=protected-access await hass.services.async_call( "homematicip_cloud", @@ -387,7 +401,7 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): ) assert home.mock_calls[-1][0] == "activate_absence_with_period" assert home.mock_calls[-1][1] == (datetime.datetime(2019, 2, 17, 14, 0),) - assert len(home._connection.mock_calls) == 3 # pylint: disable=W0212 + assert len(home._connection.mock_calls) == 3 # pylint: disable=protected-access await hass.services.async_call( "homematicip_cloud", @@ -397,7 +411,7 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): ) assert home.mock_calls[-1][0] == "activate_absence_with_period" assert home.mock_calls[-1][1] == (datetime.datetime(2019, 2, 17, 14, 0),) - assert len(home._connection.mock_calls) == 4 # pylint: disable=W0212 + assert len(home._connection.mock_calls) == 4 # pylint: disable=protected-access await hass.services.async_call( "homematicip_cloud", @@ -407,7 +421,7 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): ) assert home.mock_calls[-1][0] == "activate_vacation" assert home.mock_calls[-1][1] == (datetime.datetime(2019, 2, 17, 14, 0), 18.5) - assert len(home._connection.mock_calls) == 5 # pylint: disable=W0212 + assert len(home._connection.mock_calls) == 5 # pylint: disable=protected-access await hass.services.async_call( "homematicip_cloud", @@ -417,7 +431,7 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): ) assert home.mock_calls[-1][0] == "activate_vacation" assert home.mock_calls[-1][1] == (datetime.datetime(2019, 2, 17, 14, 0), 18.5) - assert len(home._connection.mock_calls) == 6 # pylint: disable=W0212 + assert len(home._connection.mock_calls) == 6 # pylint: disable=protected-access await hass.services.async_call( "homematicip_cloud", @@ -427,14 +441,14 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): ) assert home.mock_calls[-1][0] == "deactivate_absence" assert home.mock_calls[-1][1] == () - assert len(home._connection.mock_calls) == 7 # pylint: disable=W0212 + assert len(home._connection.mock_calls) == 7 # pylint: disable=protected-access await hass.services.async_call( "homematicip_cloud", "deactivate_eco_mode", blocking=True ) assert home.mock_calls[-1][0] == "deactivate_absence" assert home.mock_calls[-1][1] == () - assert len(home._connection.mock_calls) == 8 # pylint: disable=W0212 + assert len(home._connection.mock_calls) == 8 # pylint: disable=protected-access await hass.services.async_call( "homematicip_cloud", @@ -444,14 +458,14 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): ) assert home.mock_calls[-1][0] == "deactivate_vacation" assert home.mock_calls[-1][1] == () - assert len(home._connection.mock_calls) == 9 # pylint: disable=W0212 + assert len(home._connection.mock_calls) == 9 # pylint: disable=protected-access await hass.services.async_call( "homematicip_cloud", "deactivate_vacation", blocking=True ) assert home.mock_calls[-1][0] == "deactivate_vacation" assert home.mock_calls[-1][1] == () - assert len(home._connection.mock_calls) == 10 # pylint: disable=W0212 + assert len(home._connection.mock_calls) == 10 # pylint: disable=protected-access not_existing_hap_id = "5555F7110000000000000001" await hass.services.async_call( @@ -463,7 +477,7 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): assert home.mock_calls[-1][0] == "deactivate_vacation" assert home.mock_calls[-1][1] == () # There is no further call on connection. - assert len(home._connection.mock_calls) == 10 # pylint: disable=W0212 + assert len(home._connection.mock_calls) == 10 # pylint: disable=protected-access async def test_hmip_heating_group_services(hass, mock_hap_with_service): @@ -485,7 +499,9 @@ async def test_hmip_heating_group_services(hass, mock_hap_with_service): ) assert hmip_device.mock_calls[-1][0] == "set_active_profile" assert hmip_device.mock_calls[-1][1] == (1,) - assert len(hmip_device._connection.mock_calls) == 2 # pylint: disable=W0212 + assert ( + len(hmip_device._connection.mock_calls) == 2 # pylint: disable=protected-access + ) await hass.services.async_call( "homematicip_cloud", @@ -495,4 +511,7 @@ async def test_hmip_heating_group_services(hass, mock_hap_with_service): ) assert hmip_device.mock_calls[-1][0] == "set_active_profile" assert hmip_device.mock_calls[-1][1] == (1,) - assert len(hmip_device._connection.mock_calls) == 12 # pylint: disable=W0212 + assert ( + len(hmip_device._connection.mock_calls) # pylint: disable=protected-access + == 12 + ) diff --git a/tests/components/homematicip_cloud/test_cover.py b/tests/components/homematicip_cloud/test_cover.py index 22922303f9e..728d60d5501 100644 --- a/tests/components/homematicip_cloud/test_cover.py +++ b/tests/components/homematicip_cloud/test_cover.py @@ -1,4 +1,6 @@ """Tests for HomematicIP Cloud cover.""" +from homematicip.base.enums import DoorCommand, DoorState + from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, ATTR_CURRENT_TILT_POSITION, @@ -153,3 +155,47 @@ async def test_hmip_cover_slats(hass, default_mock_hap): await async_manipulate_test_data(hass, hmip_device, "shutterLevel", None) ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_OPEN + + +async def test_hmip_garage_door_tormatic(hass, default_mock_hap): + """Test HomematicipCoverShutte.""" + entity_id = "cover.garage_door_module" + entity_name = "Garage Door Module" + device_model = "HmIP-MOD-TM" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "closed" + assert ha_state.attributes["current_position"] == 0 + service_call_counter = len(hmip_device.mock_calls) + + await hass.services.async_call( + "cover", "open_cover", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 1 + assert hmip_device.mock_calls[-1][0] == "send_door_command" + assert hmip_device.mock_calls[-1][1] == (DoorCommand.OPEN,) + await async_manipulate_test_data(hass, hmip_device, "doorState", DoorState.OPEN) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OPEN + assert ha_state.attributes[ATTR_CURRENT_POSITION] == 100 + + await hass.services.async_call( + "cover", "close_cover", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 3 + assert hmip_device.mock_calls[-1][0] == "send_door_command" + assert hmip_device.mock_calls[-1][1] == (DoorCommand.CLOSE,) + await async_manipulate_test_data(hass, hmip_device, "doorState", DoorState.CLOSED) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_CLOSED + assert ha_state.attributes[ATTR_CURRENT_POSITION] == 0 + + await hass.services.async_call( + "cover", "stop_cover", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 5 + assert hmip_device.mock_calls[-1][0] == "send_door_command" + assert hmip_device.mock_calls[-1][1] == (DoorCommand.STOP,) diff --git a/tests/components/homematicip_cloud/test_device.py b/tests/components/homematicip_cloud/test_device.py index 812f32a3344..77f99655c98 100644 --- a/tests/components/homematicip_cloud/test_device.py +++ b/tests/components/homematicip_cloud/test_device.py @@ -105,7 +105,7 @@ async def test_hap_reconnected(hass, default_mock_hap): ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_UNAVAILABLE - default_mock_hap._accesspoint_connected = False # pylint: disable=W0212 + default_mock_hap._accesspoint_connected = False # pylint: disable=protected-access await async_manipulate_test_data(hass, default_mock_hap.home, "connected", True) await hass.async_block_till_done() ha_state = hass.states.get(entity_id) diff --git a/tests/components/honeywell/test_climate.py b/tests/components/honeywell/test_climate.py index 9c93eb9a7c7..058988203e5 100644 --- a/tests/components/honeywell/test_climate.py +++ b/tests/components/honeywell/test_climate.py @@ -2,25 +2,23 @@ import unittest from unittest import mock -import voluptuous as vol +import pytest import requests.exceptions import somecomfort -import pytest +import voluptuous as vol -from homeassistant.const import ( - CONF_USERNAME, - CONF_PASSWORD, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, -) from homeassistant.components.climate.const import ( ATTR_FAN_MODE, ATTR_FAN_MODES, ATTR_HVAC_MODES, ) - import homeassistant.components.honeywell.climate as honeywell - +from homeassistant.const import ( + CONF_PASSWORD, + CONF_USERNAME, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) pytestmark = pytest.mark.skip("Need to be fixed!") @@ -29,7 +27,7 @@ class TestHoneywell(unittest.TestCase): """A test class for Honeywell themostats.""" @mock.patch("somecomfort.SomeComfort") - @mock.patch("homeassistant.components.honeywell." "climate.HoneywellUSThermostat") + @mock.patch("homeassistant.components.honeywell.climate.HoneywellUSThermostat") def test_setup_us(self, mock_ht, mock_sc): """Test for the US setup.""" config = { @@ -100,7 +98,7 @@ class TestHoneywell(unittest.TestCase): assert not add_entities.called @mock.patch("somecomfort.SomeComfort") - @mock.patch("homeassistant.components.honeywell." "climate.HoneywellUSThermostat") + @mock.patch("homeassistant.components.honeywell.climate.HoneywellUSThermostat") def _test_us_filtered_devices(self, mock_ht, mock_sc, loc=None, dev=None): """Test for US filtered thermostats.""" config = { @@ -159,7 +157,7 @@ class TestHoneywell(unittest.TestCase): assert [mock.sentinel.loc2dev1] == devices @mock.patch("evohomeclient.EvohomeClient") - @mock.patch("homeassistant.components.honeywell.climate." "HoneywellUSThermostat") + @mock.patch("homeassistant.components.honeywell.climate.HoneywellUSThermostat") def test_eu_setup_full_config(self, mock_round, mock_evo): """Test the EU setup with complete configuration.""" config = { @@ -186,7 +184,7 @@ class TestHoneywell(unittest.TestCase): assert 2 == add_entities.call_count @mock.patch("evohomeclient.EvohomeClient") - @mock.patch("homeassistant.components.honeywell.climate." "HoneywellUSThermostat") + @mock.patch("homeassistant.components.honeywell.climate.HoneywellUSThermostat") def test_eu_setup_partial_config(self, mock_round, mock_evo): """Test the EU setup with partial configuration.""" config = { @@ -208,7 +206,7 @@ class TestHoneywell(unittest.TestCase): ) @mock.patch("evohomeclient.EvohomeClient") - @mock.patch("homeassistant.components.honeywell.climate." "HoneywellUSThermostat") + @mock.patch("homeassistant.components.honeywell.climate.HoneywellUSThermostat") def test_eu_setup_bad_temp(self, mock_round, mock_evo): """Test the EU setup with invalid temperature.""" config = { @@ -221,7 +219,7 @@ class TestHoneywell(unittest.TestCase): honeywell.PLATFORM_SCHEMA(config) @mock.patch("evohomeclient.EvohomeClient") - @mock.patch("homeassistant.components.honeywell.climate." "HoneywellUSThermostat") + @mock.patch("homeassistant.components.honeywell.climate.HoneywellUSThermostat") def test_eu_setup_error(self, mock_round, mock_evo): """Test the EU setup with errors.""" config = { diff --git a/tests/components/html5/test_notify.py b/tests/components/html5/test_notify.py index 481d7a010c9..a9fd998f003 100644 --- a/tests/components/html5/test_notify.py +++ b/tests/components/html5/test_notify.py @@ -1,11 +1,12 @@ """Test HTML5 notify platform.""" import json -from unittest.mock import patch, MagicMock, mock_open +from unittest.mock import MagicMock, mock_open, patch + from aiohttp.hdrs import AUTHORIZATION -from homeassistant.setup import async_setup_component -from homeassistant.exceptions import HomeAssistantError import homeassistant.components.html5.notify as html5 +from homeassistant.exceptions import HomeAssistantError +from homeassistant.setup import async_setup_component CONFIG_FILE = "file.conf" diff --git a/tests/components/http/__init__.py b/tests/components/http/__init__.py index db5e1ea5c7a..e96f4a7fcf2 100644 --- a/tests/components/http/__init__.py +++ b/tests/components/http/__init__.py @@ -5,7 +5,6 @@ from aiohttp import web from homeassistant.components.http.const import KEY_REAL_IP - # Relic from the past. Kept here so we can run negative tests. HTTP_HEADER_HA_AUTH = "X-HA-access" diff --git a/tests/components/http/test_auth.py b/tests/components/http/test_auth.py index 499ceab1556..3617690eb3b 100644 --- a/tests/components/http/test_auth.py +++ b/tests/components/http/test_auth.py @@ -3,16 +3,17 @@ from datetime import timedelta from ipaddress import ip_network from unittest.mock import patch -import pytest from aiohttp import BasicAuth, web from aiohttp.web_exceptions import HTTPUnauthorized +import pytest from homeassistant.auth.providers import trusted_networks -from homeassistant.components.http.auth import setup_auth, async_sign_path +from homeassistant.components.http.auth import async_sign_path, setup_auth from homeassistant.components.http.const import KEY_AUTHENTICATED from homeassistant.components.http.real_ip import setup_real_ip from homeassistant.setup import async_setup_component -from . import mock_real_ip, HTTP_HEADER_HA_AUTH + +from . import HTTP_HEADER_HA_AUTH, mock_real_ip API_PASSWORD = "test-password" diff --git a/tests/components/http/test_ban.py b/tests/components/http/test_ban.py index f50afcef8a8..8d9d19b6a12 100644 --- a/tests/components/http/test_ban.py +++ b/tests/components/http/test_ban.py @@ -1,29 +1,28 @@ """The tests for the Home Assistant HTTP component.""" # pylint: disable=protected-access from ipaddress import ip_address -from unittest.mock import patch, mock_open, Mock +from unittest.mock import Mock, mock_open, patch from aiohttp import web from aiohttp.web_exceptions import HTTPUnauthorized from aiohttp.web_middlewares import middleware -from homeassistant.components.http import KEY_AUTHENTICATED -from homeassistant.components.http.view import request_handler_factory -from homeassistant.setup import async_setup_component import homeassistant.components.http as http +from homeassistant.components.http import KEY_AUTHENTICATED from homeassistant.components.http.ban import ( - IpBan, IP_BANS_FILE, - setup_bans, KEY_BANNED_IPS, KEY_FAILED_LOGIN_ATTEMPTS, + IpBan, + setup_bans, ) +from homeassistant.components.http.view import request_handler_factory +from homeassistant.setup import async_setup_component from . import mock_real_ip from tests.common import mock_coro - BANNED_IPS = ["200.201.202.203", "100.64.0.2"] diff --git a/tests/components/http/test_cors.py b/tests/components/http/test_cors.py index 1cea900d971..04447191fd5 100644 --- a/tests/components/http/test_cors.py +++ b/tests/components/http/test_cors.py @@ -4,8 +4,8 @@ from unittest.mock import patch from aiohttp import web from aiohttp.hdrs import ( - ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_ALLOW_HEADERS, + ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_REQUEST_HEADERS, ACCESS_CONTROL_REQUEST_METHOD, AUTHORIZATION, @@ -13,13 +13,12 @@ from aiohttp.hdrs import ( ) import pytest -from homeassistant.setup import async_setup_component from homeassistant.components.http.cors import setup_cors from homeassistant.components.http.view import HomeAssistantView +from homeassistant.setup import async_setup_component from . import HTTP_HEADER_HA_AUTH - TRUSTED_ORIGIN = "https://home-assistant.io" diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index ad8e3ac10fd..212ae7499ab 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -3,10 +3,9 @@ import logging import unittest from unittest.mock import patch -from homeassistant.setup import async_setup_component - import homeassistant.components.http as http -from homeassistant.util.ssl import server_context_modern, server_context_intermediate +from homeassistant.setup import async_setup_component +from homeassistant.util.ssl import server_context_intermediate, server_context_modern class TestView(http.HomeAssistantView): diff --git a/tests/components/http/test_real_ip.py b/tests/components/http/test_real_ip.py index 581624e0c0a..2cb74df3176 100644 --- a/tests/components/http/test_real_ip.py +++ b/tests/components/http/test_real_ip.py @@ -1,10 +1,11 @@ """Test real IP middleware.""" -from aiohttp import web -from aiohttp.hdrs import X_FORWARDED_FOR from ipaddress import ip_network -from homeassistant.components.http.real_ip import setup_real_ip +from aiohttp import web +from aiohttp.hdrs import X_FORWARDED_FOR + from homeassistant.components.http.const import KEY_REAL_IP +from homeassistant.components.http.real_ip import setup_real_ip async def mock_handler(request): diff --git a/tests/components/http/test_view.py b/tests/components/http/test_view.py index 1cbe4c5a030..414ad4e8cb0 100644 --- a/tests/components/http/test_view.py +++ b/tests/components/http/test_view.py @@ -2,8 +2,8 @@ from unittest.mock import Mock from aiohttp.web_exceptions import ( - HTTPInternalServerError, HTTPBadRequest, + HTTPInternalServerError, HTTPUnauthorized, ) import pytest diff --git a/tests/components/huawei_lte/test_config_flow.py b/tests/components/huawei_lte/test_config_flow.py index a9f5034fcfe..29127ed964b 100644 --- a/tests/components/huawei_lte/test_config_flow.py +++ b/tests/components/huawei_lte/test_config_flow.py @@ -2,32 +2,18 @@ from huawei_lte_api.enums.client import ResponseCodeEnum from huawei_lte_api.enums.user import LoginErrorEnum, LoginStateEnum, PasswordTypeEnum -from requests_mock import ANY -from requests.exceptions import ConnectionError import pytest +from requests.exceptions import ConnectionError +from requests_mock import ANY from homeassistant import data_entry_flow -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_URL -from homeassistant.components.huawei_lte.const import DOMAIN +from homeassistant.components import ssdp from homeassistant.components.huawei_lte.config_flow import ConfigFlowHandler -from homeassistant.components.ssdp import ( - ATTR_HOST, - ATTR_MANUFACTURER, - ATTR_MANUFACTURERURL, - ATTR_MODEL_NAME, - ATTR_MODEL_NUMBER, - ATTR_NAME, - ATTR_PORT, - ATTR_PRESENTATIONURL, - ATTR_SERIAL, - ATTR_ST, - ATTR_UDN, - ATTR_UPNP_DEVICE_TYPE, -) +from homeassistant.components.huawei_lte.const import DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME from tests.common import MockConfigEntry - FIXTURE_USER_INPUT = { CONF_URL: "http://192.168.1.1/", CONF_USERNAME: "admin", @@ -157,18 +143,17 @@ async def test_ssdp(flow): url = "http://192.168.100.1/" result = await flow.async_step_ssdp( discovery_info={ - ATTR_ST: "upnp:rootdevice", - ATTR_PORT: 60957, - ATTR_HOST: "192.168.100.1", - ATTR_MANUFACTURER: "Huawei", - ATTR_MANUFACTURERURL: "http://www.huawei.com/", - ATTR_MODEL_NAME: "Huawei router", - ATTR_MODEL_NUMBER: "12345678", - ATTR_NAME: "Mobile Wi-Fi", - ATTR_PRESENTATIONURL: url, - ATTR_SERIAL: "00000000", - ATTR_UDN: "uuid:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", - ATTR_UPNP_DEVICE_TYPE: "urn:schemas-upnp-org:device:InternetGatewayDevice:1", + ssdp.ATTR_SSDP_LOCATION: "http://192.168.100.1:60957/rootDesc.xml", + ssdp.ATTR_SSDP_ST: "upnp:rootdevice", + ssdp.ATTR_UPNP_DEVICE_TYPE: "urn:schemas-upnp-org:device:InternetGatewayDevice:1", + ssdp.ATTR_UPNP_FRIENDLY_NAME: "Mobile Wi-Fi", + ssdp.ATTR_UPNP_MANUFACTURER: "Huawei", + ssdp.ATTR_UPNP_MANUFACTURER_URL: "http://www.huawei.com/", + ssdp.ATTR_UPNP_MODEL_NAME: "Huawei router", + ssdp.ATTR_UPNP_MODEL_NUMBER: "12345678", + ssdp.ATTR_UPNP_PRESENTATION_URL: url, + ssdp.ATTR_UPNP_SERIAL: "00000000", + ssdp.ATTR_UPNP_UDN: "uuid:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", } ) diff --git a/tests/components/hue/test_bridge.py b/tests/components/hue/test_bridge.py index 7265b468714..03966560d8d 100644 --- a/tests/components/hue/test_bridge.py +++ b/tests/components/hue/test_bridge.py @@ -3,113 +3,116 @@ from unittest.mock import Mock, patch import pytest -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.components.hue import bridge, errors +from homeassistant.exceptions import ConfigEntryNotReady from tests.common import mock_coro -async def test_bridge_setup(): +async def test_bridge_setup(hass): """Test a successful setup.""" - hass = Mock() entry = Mock() - api = Mock() + api = Mock(initialize=mock_coro) entry.data = {"host": "1.2.3.4", "username": "mock-username"} hue_bridge = bridge.HueBridge(hass, entry, False, False) - with patch.object(bridge, "get_bridge", return_value=mock_coro(api)): + with patch("aiohue.Bridge", return_value=api), patch.object( + hass.config_entries, "async_forward_entry_setup" + ) as mock_forward: assert await hue_bridge.async_setup() is True assert hue_bridge.api is api - forward_entries = set( - c[1][1] for c in hass.config_entries.async_forward_entry_setup.mock_calls - ) - assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 3 + assert len(mock_forward.mock_calls) == 3 + forward_entries = set(c[1][1] for c in mock_forward.mock_calls) assert forward_entries == set(["light", "binary_sensor", "sensor"]) -async def test_bridge_setup_invalid_username(): +async def test_bridge_setup_invalid_username(hass): """Test we start config flow if username is no longer whitelisted.""" - hass = Mock() - entry = Mock() - entry.data = {"host": "1.2.3.4", "username": "mock-username"} - hue_bridge = bridge.HueBridge(hass, entry, False, False) - - with patch.object(bridge, "get_bridge", side_effect=errors.AuthenticationRequired): - assert await hue_bridge.async_setup() is False - - assert len(hass.async_create_task.mock_calls) == 1 - assert len(hass.config_entries.flow.async_init.mock_calls) == 1 - assert hass.config_entries.flow.async_init.mock_calls[0][2]["data"] == { - "host": "1.2.3.4" - } - - -async def test_bridge_setup_timeout(hass): - """Test we retry to connect if we cannot connect.""" - hass = Mock() entry = Mock() entry.data = {"host": "1.2.3.4", "username": "mock-username"} hue_bridge = bridge.HueBridge(hass, entry, False, False) with patch.object( - bridge, "get_bridge", side_effect=errors.CannotConnect + bridge, "authenticate_bridge", side_effect=errors.AuthenticationRequired + ), patch.object( + hass.config_entries.flow, "async_init", return_value=mock_coro() + ) as mock_init: + assert await hue_bridge.async_setup() is False + + assert len(mock_init.mock_calls) == 1 + assert mock_init.mock_calls[0][2]["data"] == {"host": "1.2.3.4"} + + +async def test_bridge_setup_timeout(hass): + """Test we retry to connect if we cannot connect.""" + entry = Mock() + entry.data = {"host": "1.2.3.4", "username": "mock-username"} + hue_bridge = bridge.HueBridge(hass, entry, False, False) + + with patch.object( + bridge, "authenticate_bridge", side_effect=errors.CannotConnect ), pytest.raises(ConfigEntryNotReady): await hue_bridge.async_setup() -async def test_reset_if_entry_had_wrong_auth(): +async def test_reset_if_entry_had_wrong_auth(hass): """Test calling reset when the entry contained wrong auth.""" - hass = Mock() entry = Mock() entry.data = {"host": "1.2.3.4", "username": "mock-username"} hue_bridge = bridge.HueBridge(hass, entry, False, False) - with patch.object(bridge, "get_bridge", side_effect=errors.AuthenticationRequired): + with patch.object( + bridge, "authenticate_bridge", side_effect=errors.AuthenticationRequired + ), patch.object(bridge, "create_config_flow") as mock_create: assert await hue_bridge.async_setup() is False - assert len(hass.async_create_task.mock_calls) == 1 + assert len(mock_create.mock_calls) == 1 assert await hue_bridge.async_reset() -async def test_reset_unloads_entry_if_setup(): +async def test_reset_unloads_entry_if_setup(hass): """Test calling reset while the entry has been setup.""" - hass = Mock() entry = Mock() entry.data = {"host": "1.2.3.4", "username": "mock-username"} hue_bridge = bridge.HueBridge(hass, entry, False, False) - with patch.object(bridge, "get_bridge", return_value=mock_coro(Mock())): + with patch.object( + bridge, "authenticate_bridge", return_value=mock_coro(Mock()) + ), patch("aiohue.Bridge", return_value=Mock()), patch.object( + hass.config_entries, "async_forward_entry_setup" + ) as mock_forward: assert await hue_bridge.async_setup() is True - assert len(hass.services.async_register.mock_calls) == 1 - assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 3 + assert len(hass.services.async_services()) == 1 + assert len(mock_forward.mock_calls) == 3 - hass.config_entries.async_forward_entry_unload.return_value = mock_coro(True) - assert await hue_bridge.async_reset() + with patch.object( + hass.config_entries, "async_forward_entry_unload", return_value=mock_coro(True) + ) as mock_forward: + assert await hue_bridge.async_reset() - assert len(hass.config_entries.async_forward_entry_unload.mock_calls) == 3 - assert len(hass.services.async_remove.mock_calls) == 1 + assert len(mock_forward.mock_calls) == 3 + assert len(hass.services.async_services()) == 0 -async def test_handle_unauthorized(): +async def test_handle_unauthorized(hass): """Test handling an unauthorized error on update.""" - hass = Mock() entry = Mock() entry.data = {"host": "1.2.3.4", "username": "mock-username"} hue_bridge = bridge.HueBridge(hass, entry, False, False) - with patch.object(bridge, "get_bridge", return_value=mock_coro(Mock())): + with patch.object( + bridge, "authenticate_bridge", return_value=mock_coro(Mock()) + ), patch("aiohue.Bridge", return_value=Mock()): assert await hue_bridge.async_setup() is True assert hue_bridge.authorized is True - await hue_bridge.handle_unauthorized_error() + with patch.object(bridge, "create_config_flow") as mock_create: + await hue_bridge.handle_unauthorized_error() assert hue_bridge.authorized is False - assert len(hass.async_create_task.mock_calls) == 4 - assert len(hass.config_entries.flow.async_init.mock_calls) == 1 - assert hass.config_entries.flow.async_init.mock_calls[0][2]["data"] == { - "host": "1.2.3.4" - } + assert len(mock_create.mock_calls) == 1 + assert mock_create.mock_calls[0][1][1] == "1.2.3.4" diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py index 54082464a7c..2ad6474b9ac 100644 --- a/tests/components/hue/test_config_flow.py +++ b/tests/components/hue/test_config_flow.py @@ -6,49 +6,53 @@ import aiohue import pytest import voluptuous as vol -from homeassistant.components.hue import config_flow, const, errors +from homeassistant import config_entries, data_entry_flow +from homeassistant.components import ssdp +from homeassistant.components.hue import config_flow, const from tests.common import MockConfigEntry, mock_coro -async def test_flow_works(hass, aioclient_mock): +async def test_flow_works(hass): """Test config flow .""" - aioclient_mock.get( - const.API_NUPNP, json=[{"internalipaddress": "1.2.3.4", "id": "bla"}] - ) + mock_bridge = Mock() + mock_bridge.host = "1.2.3.4" + mock_bridge.username = None + mock_bridge.config.name = "Mock Bridge" + mock_bridge.id = "aabbccddeeff" + + async def mock_create_user(username): + mock_bridge.username = username + + mock_bridge.create_user = mock_create_user + mock_bridge.initialize.return_value = mock_coro() flow = config_flow.HueFlowHandler() flow.hass = hass - await flow.async_step_init() + flow.context = {} - with patch("aiohue.Bridge") as mock_bridge: + with patch( + "homeassistant.components.hue.config_flow.discover_nupnp", + return_value=mock_coro([mock_bridge]), + ): + result = await flow.async_step_init() - def mock_constructor(host, websession, username=None): - """Fake the bridge constructor.""" - mock_bridge.host = host - return mock_bridge + assert result["type"] == "form" + assert result["step_id"] == "link" - mock_bridge.side_effect = mock_constructor - mock_bridge.username = "username-abc" - mock_bridge.config.name = "Mock Bridge" - mock_bridge.config.bridgeid = "bridge-id-1234" - mock_bridge.create_user.return_value = mock_coro() - mock_bridge.initialize.return_value = mock_coro() + assert flow.context["unique_id"] == "aabbccddeeff" - result = await flow.async_step_link(user_input={}) - - assert mock_bridge.host == "1.2.3.4" - assert len(mock_bridge.create_user.mock_calls) == 1 - assert len(mock_bridge.initialize.mock_calls) == 1 + result = await flow.async_step_link(user_input={}) assert result["type"] == "create_entry" assert result["title"] == "Mock Bridge" assert result["data"] == { "host": "1.2.3.4", - "bridge_id": "bridge-id-1234", - "username": "username-abc", + "username": "home-assistant#test-home", } + assert len(mock_bridge.initialize.mock_calls) == 1 + async def test_flow_no_discovered_bridges(hass, aioclient_mock): """Test config flow discovers no bridges.""" @@ -65,9 +69,12 @@ async def test_flow_all_discovered_bridges_exist(hass, aioclient_mock): aioclient_mock.get( const.API_NUPNP, json=[{"internalipaddress": "1.2.3.4", "id": "bla"}] ) - MockConfigEntry(domain="hue", data={"host": "1.2.3.4"}).add_to_hass(hass) + MockConfigEntry( + domain="hue", unique_id="bla", data={"host": "1.2.3.4"} + ).add_to_hass(hass) flow = config_flow.HueFlowHandler() flow.hass = hass + flow.context = {} result = await flow.async_step_init() assert result["type"] == "abort" @@ -80,6 +87,7 @@ async def test_flow_one_bridge_discovered(hass, aioclient_mock): ) flow = config_flow.HueFlowHandler() flow.hass = hass + flow.context = {} result = await flow.async_step_init() assert result["type"] == "form" @@ -88,6 +96,11 @@ async def test_flow_one_bridge_discovered(hass, aioclient_mock): async def test_flow_two_bridges_discovered(hass, aioclient_mock): """Test config flow discovers two bridges.""" + # Add ignored config entry. Should still show up as option. + MockConfigEntry( + domain="hue", source=config_entries.SOURCE_IGNORE, unique_id="bla" + ).add_to_hass(hass) + aioclient_mock.get( const.API_NUPNP, json=[ @@ -103,10 +116,10 @@ async def test_flow_two_bridges_discovered(hass, aioclient_mock): assert result["step_id"] == "init" with pytest.raises(vol.Invalid): - assert result["data_schema"]({"host": "0.0.0.0"}) + assert result["data_schema"]({"id": "not-discovered"}) - result["data_schema"]({"host": "1.2.3.4"}) - result["data_schema"]({"host": "5.6.7.8"}) + result["data_schema"]({"id": "bla"}) + result["data_schema"]({"id": "beer"}) async def test_flow_two_bridges_discovered_one_new(hass, aioclient_mock): @@ -118,14 +131,17 @@ async def test_flow_two_bridges_discovered_one_new(hass, aioclient_mock): {"internalipaddress": "5.6.7.8", "id": "beer"}, ], ) - MockConfigEntry(domain="hue", data={"host": "1.2.3.4"}).add_to_hass(hass) + MockConfigEntry( + domain="hue", unique_id="bla", data={"host": "1.2.3.4"} + ).add_to_hass(hass) flow = config_flow.HueFlowHandler() flow.hass = hass + flow.context = {} result = await flow.async_step_init() assert result["type"] == "form" assert result["step_id"] == "link" - assert flow.host == "5.6.7.8" + assert flow.bridge.host == "5.6.7.8" async def test_flow_timeout_discovery(hass): @@ -146,6 +162,7 @@ async def test_flow_link_timeout(hass): """Test config flow .""" flow = config_flow.HueFlowHandler() flow.hass = hass + flow.bridge = Mock() with patch("aiohue.Bridge.create_user", side_effect=asyncio.TimeoutError): result = await flow.async_step_link({}) @@ -159,9 +176,11 @@ async def test_flow_link_button_not_pressed(hass): """Test config flow .""" flow = config_flow.HueFlowHandler() flow.hass = hass + flow.bridge = Mock( + username=None, create_user=Mock(side_effect=aiohue.LinkButtonNotPressed) + ) - with patch("aiohue.Bridge.create_user", side_effect=aiohue.LinkButtonNotPressed): - result = await flow.async_step_link({}) + result = await flow.async_step_link({}) assert result["type"] == "form" assert result["step_id"] == "link" @@ -172,6 +191,7 @@ async def test_flow_link_unknown_host(hass): """Test config flow .""" flow = config_flow.HueFlowHandler() flow.hass = hass + flow.bridge = Mock() with patch("aiohue.Bridge.create_user", side_effect=aiohue.RequestError): result = await flow.async_step_link({}) @@ -187,16 +207,13 @@ async def test_bridge_ssdp(hass): flow.hass = hass flow.context = {} - with patch.object( - config_flow, "get_bridge", side_effect=errors.AuthenticationRequired - ): - result = await flow.async_step_ssdp( - { - "host": "0.0.0.0", - "serial": "1234", - "manufacturerURL": config_flow.HUE_MANUFACTURERURL, - } - ) + result = await flow.async_step_ssdp( + { + ssdp.ATTR_SSDP_LOCATION: "http://0.0.0.0/", + ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL, + ssdp.ATTR_UPNP_SERIAL: "1234", + } + ) assert result["type"] == "form" assert result["step_id"] == "link" @@ -208,7 +225,7 @@ async def test_bridge_ssdp_discover_other_bridge(hass): flow.hass = hass result = await flow.async_step_ssdp( - {"manufacturerURL": "http://www.notphilips.com"} + {ssdp.ATTR_UPNP_MANUFACTURER_URL: "http://www.notphilips.com"} ) assert result["type"] == "abort" @@ -222,59 +239,54 @@ async def test_bridge_ssdp_emulated_hue(hass): result = await flow.async_step_ssdp( { - "name": "HASS Bridge", - "host": "0.0.0.0", - "serial": "1234", - "manufacturerURL": config_flow.HUE_MANUFACTURERURL, + ssdp.ATTR_SSDP_LOCATION: "http://0.0.0.0/", + ssdp.ATTR_UPNP_FRIENDLY_NAME: "HASS Bridge", + ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL, + ssdp.ATTR_UPNP_SERIAL: "1234", } ) assert result["type"] == "abort" + assert result["reason"] == "not_hue_bridge" -async def test_bridge_ssdp_already_configured(hass): - """Test if a discovered bridge has already been configured.""" - MockConfigEntry(domain="hue", data={"host": "0.0.0.0"}).add_to_hass(hass) - +async def test_bridge_ssdp_espalexa(hass): + """Test if discovery info is from an Espalexa based device.""" flow = config_flow.HueFlowHandler() flow.hass = hass flow.context = {} result = await flow.async_step_ssdp( { - "host": "0.0.0.0", - "serial": "1234", - "manufacturerURL": config_flow.HUE_MANUFACTURERURL, + 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_SERIAL: "1234", } ) assert result["type"] == "abort" + assert result["reason"] == "not_hue_bridge" -async def test_import_with_existing_config(hass): - """Test importing a host with an existing config file.""" +async def test_bridge_ssdp_already_configured(hass): + """Test if a discovered bridge has already been configured.""" + MockConfigEntry( + domain="hue", unique_id="1234", data={"host": "0.0.0.0"} + ).add_to_hass(hass) + flow = config_flow.HueFlowHandler() flow.hass = hass flow.context = {} - bridge = Mock() - bridge.username = "username-abc" - bridge.config.bridgeid = "bridge-id-1234" - bridge.config.name = "Mock Bridge" - bridge.host = "0.0.0.0" - - with patch.object( - config_flow, "_find_username_from_config", return_value="mock-user" - ), patch.object(config_flow, "get_bridge", return_value=mock_coro(bridge)): - result = await flow.async_step_import({"host": "0.0.0.0", "path": "bla.conf"}) - - assert result["type"] == "create_entry" - assert result["title"] == "Mock Bridge" - assert result["data"] == { - "host": "0.0.0.0", - "bridge_id": "bridge-id-1234", - "username": "username-abc", - } + with pytest.raises(data_entry_flow.AbortFlow): + await flow.async_step_ssdp( + { + ssdp.ATTR_SSDP_LOCATION: "http://0.0.0.0/", + ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL, + ssdp.ATTR_UPNP_SERIAL: "1234", + } + ) async def test_import_with_no_config(hass): @@ -283,45 +295,12 @@ async def test_import_with_no_config(hass): flow.hass = hass flow.context = {} - with patch.object( - config_flow, "get_bridge", side_effect=errors.AuthenticationRequired - ): - result = await flow.async_step_import({"host": "0.0.0.0"}) + result = await flow.async_step_import({"host": "0.0.0.0"}) assert result["type"] == "form" assert result["step_id"] == "link" -async def test_import_with_existing_but_invalid_config(hass): - """Test importing a host with a config file with invalid username.""" - flow = config_flow.HueFlowHandler() - flow.hass = hass - flow.context = {} - - with patch.object( - config_flow, "_find_username_from_config", return_value="mock-user" - ), patch.object( - config_flow, "get_bridge", side_effect=errors.AuthenticationRequired - ): - result = await flow.async_step_import({"host": "0.0.0.0", "path": "bla.conf"}) - - assert result["type"] == "form" - assert result["step_id"] == "link" - - -async def test_import_cannot_connect(hass): - """Test importing a host that we cannot conncet to.""" - flow = config_flow.HueFlowHandler() - flow.hass = hass - flow.context = {} - - with patch.object(config_flow, "get_bridge", side_effect=errors.CannotConnect): - result = await flow.async_step_import({"host": "0.0.0.0"}) - - assert result["type"] == "abort" - assert result["reason"] == "cannot_connect" - - async def test_creating_entry_removes_entries_for_same_host_or_bridge(hass): """Test that we clean up entries for same host and bridge. @@ -329,39 +308,53 @@ async def test_creating_entry_removes_entries_for_same_host_or_bridge(hass): accessible via a single IP. So when we create a new entry, we'll remove all existing entries that either have same IP or same bridge_id. """ - MockConfigEntry( - domain="hue", data={"host": "0.0.0.0", "bridge_id": "id-1234"} - ).add_to_hass(hass) + orig_entry = MockConfigEntry( + domain="hue", data={"host": "0.0.0.0", "username": "aaaa"}, unique_id="id-1234", + ) + orig_entry.add_to_hass(hass) MockConfigEntry( - domain="hue", data={"host": "1.2.3.4", "bridge_id": "id-1234"} + domain="hue", data={"host": "1.2.3.4", "username": "bbbb"}, unique_id="id-5678", ).add_to_hass(hass) assert len(hass.config_entries.async_entries("hue")) == 2 - flow = config_flow.HueFlowHandler() - flow.hass = hass - flow.context = {} - bridge = Mock() bridge.username = "username-abc" - bridge.config.bridgeid = "id-1234" bridge.config.name = "Mock Bridge" bridge.host = "0.0.0.0" + bridge.id = "id-1234" - with patch.object(config_flow, "get_bridge", return_value=mock_coro(bridge)): - result = await flow.async_step_import({"host": "0.0.0.0"}) + with patch( + "aiohue.Bridge", return_value=bridge, + ): + result = await hass.config_entries.flow.async_init( + "hue", data={"host": "2.2.2.2"}, context={"source": "import"} + ) + + assert result["type"] == "form" + assert result["step_id"] == "link" + + with patch( + "homeassistant.components.hue.config_flow.authenticate_bridge", + return_value=mock_coro(), + ), patch( + "homeassistant.components.hue.async_setup_entry", + side_effect=lambda _, _2: mock_coro(True), + ): + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert result["type"] == "create_entry" assert result["title"] == "Mock Bridge" assert result["data"] == { "host": "0.0.0.0", - "bridge_id": "id-1234", "username": "username-abc", } - # We did not process the result of this entry but already removed the old - # ones. So we should have 0 entries. - assert len(hass.config_entries.async_entries("hue")) == 0 + entries = hass.config_entries.async_entries("hue") + assert len(entries) == 2 + new_entry = entries[-1] + assert orig_entry.entry_id != new_entry.entry_id + assert new_entry.unique_id == "id-1234" async def test_bridge_homekit(hass): @@ -370,16 +363,14 @@ async def test_bridge_homekit(hass): flow.hass = hass flow.context = {} - with patch.object( - config_flow, "get_bridge", side_effect=errors.AuthenticationRequired - ): - result = await flow.async_step_homekit( - { - "host": "0.0.0.0", - "serial": "1234", - "manufacturerURL": config_flow.HUE_MANUFACTURERURL, - } - ) + result = await flow.async_step_homekit( + { + "host": "0.0.0.0", + "serial": "1234", + "manufacturerURL": config_flow.HUE_MANUFACTURERURL, + "properties": {"id": "aa:bb:cc:dd:ee:ff"}, + } + ) assert result["type"] == "form" assert result["step_id"] == "link" @@ -387,12 +378,15 @@ async def test_bridge_homekit(hass): async def test_bridge_homekit_already_configured(hass): """Test if a HomeKit discovered bridge has already been configured.""" - MockConfigEntry(domain="hue", data={"host": "0.0.0.0"}).add_to_hass(hass) + MockConfigEntry( + domain="hue", unique_id="aabbccddeeff", data={"host": "0.0.0.0"} + ).add_to_hass(hass) flow = config_flow.HueFlowHandler() flow.hass = hass flow.context = {} - result = await flow.async_step_homekit({"host": "0.0.0.0"}) - - assert result["type"] == "abort" + with pytest.raises(data_entry_flow.AbortFlow): + await flow.async_step_homekit( + {"host": "0.0.0.0", "properties": {"id": "aa:bb:cc:dd:ee:ff"}} + ) diff --git a/tests/components/hue/test_init.py b/tests/components/hue/test_init.py index f6ff112cc37..b48d66990e8 100644 --- a/tests/components/hue/test_init.py +++ b/tests/components/hue/test_init.py @@ -1,21 +1,18 @@ """Test Hue setup process.""" from unittest.mock import Mock, patch -from homeassistant.setup import async_setup_component from homeassistant.components import hue +from homeassistant.setup import async_setup_component -from tests.common import mock_coro, MockConfigEntry +from tests.common import MockConfigEntry, mock_coro async def test_setup_with_no_config(hass): """Test that we do not discover anything or try to set up a bridge.""" - with patch.object(hass, "config_entries") as mock_config_entries, patch.object( - hue, "configured_hosts", return_value=[] - ): - assert await async_setup_component(hass, hue.DOMAIN, {}) is True + assert await async_setup_component(hass, hue.DOMAIN, {}) is True # No flows started - assert len(mock_config_entries.flow.mock_calls) == 0 + assert len(hass.config_entries.flow.async_progress()) == 0 # No configs stored assert hass.data[hue.DOMAIN] == {} @@ -23,9 +20,9 @@ async def test_setup_with_no_config(hass): async def test_setup_defined_hosts_known_auth(hass): """Test we don't initiate a config entry if config bridge is known.""" - with patch.object(hass, "config_entries") as mock_config_entries, patch.object( - hue, "configured_hosts", return_value=["0.0.0.0"] - ): + MockConfigEntry(domain="hue", data={"host": "0.0.0.0"}).add_to_hass(hass) + + with patch.object(hue, "async_setup_entry", return_value=mock_coro(True)): assert ( await async_setup_component( hass, @@ -34,7 +31,6 @@ async def test_setup_defined_hosts_known_auth(hass): hue.DOMAIN: { hue.CONF_BRIDGES: { hue.CONF_HOST: "0.0.0.0", - hue.CONF_FILENAME: "bla.conf", hue.CONF_ALLOW_HUE_GROUPS: False, hue.CONF_ALLOW_UNREACHABLE: True, } @@ -45,13 +41,12 @@ async def test_setup_defined_hosts_known_auth(hass): ) # Flow started for discovered bridge - assert len(mock_config_entries.flow.mock_calls) == 0 + assert len(hass.config_entries.flow.async_progress()) == 0 # Config stored for domain. assert hass.data[hue.DATA_CONFIGS] == { "0.0.0.0": { hue.CONF_HOST: "0.0.0.0", - hue.CONF_FILENAME: "bla.conf", hue.CONF_ALLOW_HUE_GROUPS: False, hue.CONF_ALLOW_UNREACHABLE: True, } @@ -60,40 +55,30 @@ async def test_setup_defined_hosts_known_auth(hass): async def test_setup_defined_hosts_no_known_auth(hass): """Test we initiate config entry if config bridge is not known.""" - with patch.object(hass, "config_entries") as mock_config_entries, patch.object( - hue, "configured_hosts", return_value=[] - ): - mock_config_entries.flow.async_init.return_value = mock_coro() - assert ( - await async_setup_component( - hass, - hue.DOMAIN, - { - hue.DOMAIN: { - hue.CONF_BRIDGES: { - hue.CONF_HOST: "0.0.0.0", - hue.CONF_FILENAME: "bla.conf", - hue.CONF_ALLOW_HUE_GROUPS: False, - hue.CONF_ALLOW_UNREACHABLE: True, - } + assert ( + await async_setup_component( + hass, + hue.DOMAIN, + { + hue.DOMAIN: { + hue.CONF_BRIDGES: { + hue.CONF_HOST: "0.0.0.0", + hue.CONF_ALLOW_HUE_GROUPS: False, + hue.CONF_ALLOW_UNREACHABLE: True, } - }, - ) - is True + } + }, ) + is True + ) # Flow started for discovered bridge - assert len(mock_config_entries.flow.mock_calls) == 1 - assert mock_config_entries.flow.mock_calls[0][2]["data"] == { - "host": "0.0.0.0", - "path": "bla.conf", - } + assert len(hass.config_entries.flow.async_progress()) == 1 # Config stored for domain. assert hass.data[hue.DATA_CONFIGS] == { "0.0.0.0": { hue.CONF_HOST: "0.0.0.0", - hue.CONF_FILENAME: "bla.conf", hue.CONF_ALLOW_HUE_GROUPS: False, hue.CONF_ALLOW_UNREACHABLE: True, } @@ -126,7 +111,6 @@ async def test_config_passed_to_config_entry(hass): hue.DOMAIN: { hue.CONF_BRIDGES: { hue.CONF_HOST: "0.0.0.0", - hue.CONF_FILENAME: "bla.conf", hue.CONF_ALLOW_HUE_GROUPS: False, hue.CONF_ALLOW_UNREACHABLE: True, } @@ -166,7 +150,7 @@ async def test_unload_entry(hass): return_value=mock_coro(Mock()), ): mock_bridge.return_value.async_setup.return_value = mock_coro(True) - mock_bridge.return_value.api.config = Mock() + mock_bridge.return_value.api.config = Mock(bridgeid="aabbccddeeff") assert await async_setup_component(hass, hue.DOMAIN, {}) is True assert len(mock_bridge.return_value.mock_calls) == 1 @@ -175,3 +159,19 @@ async def test_unload_entry(hass): assert await hue.async_unload_entry(hass, entry) assert len(mock_bridge.return_value.async_reset.mock_calls) == 1 assert hass.data[hue.DOMAIN] == {} + + +async def test_setting_unique_id(hass): + """Test we set unique ID if not set yet.""" + entry = MockConfigEntry(domain=hue.DOMAIN, data={"host": "0.0.0.0"}) + entry.add_to_hass(hass) + + with patch.object(hue, "HueBridge") as mock_bridge, patch( + "homeassistant.helpers.device_registry.async_get_registry", + return_value=mock_coro(Mock()), + ): + mock_bridge.return_value.async_setup.return_value = mock_coro(True) + mock_bridge.return_value.api.config = Mock(bridgeid="mock-id") + assert await async_setup_component(hass, hue.DOMAIN, {}) is True + + assert entry.unique_id == "mock-id" diff --git a/tests/components/hue/test_light.py b/tests/components/hue/test_light.py index 88c527a50ca..c218e729255 100644 --- a/tests/components/hue/test_light.py +++ b/tests/components/hue/test_light.py @@ -5,8 +5,8 @@ import logging from unittest.mock import Mock import aiohue -from aiohue.lights import Lights from aiohue.groups import Groups +from aiohue.lights import Lights import pytest from homeassistant import config_entries @@ -204,6 +204,10 @@ def mock_bridge(hass): return bridge.mock_group_responses.popleft() return None + async def async_request_call(coro): + await coro + + bridge.async_request_call = async_request_call bridge.api.config.apiversion = "9.9.9" bridge.api.lights = Lights({}, mock_request) bridge.api.groups = Groups({}, mock_request) diff --git a/tests/components/hue/test_sensor_base.py b/tests/components/hue/test_sensor_base.py index ba259dccf71..ad927767c30 100644 --- a/tests/components/hue/test_sensor_base.py +++ b/tests/components/hue/test_sensor_base.py @@ -277,6 +277,10 @@ def create_mock_bridge(): return bridge.mock_sensor_responses.popleft() return None + async def async_request_call(coro): + await coro + + bridge.async_request_call = async_request_call bridge.api.config.apiversion = "9.9.9" bridge.api.sensors = Sensors({}, mock_request) return bridge diff --git a/tests/components/iaqualink/test_config_flow.py b/tests/components/iaqualink/test_config_flow.py index 5c4d75ee3c1..d2fa4633d80 100644 --- a/tests/components/iaqualink/test_config_flow.py +++ b/tests/components/iaqualink/test_config_flow.py @@ -5,6 +5,7 @@ import iaqualink import pytest from homeassistant.components.iaqualink import config_flow + from tests.common import MockConfigEntry, mock_coro DATA = {"username": "test@example.com", "password": "pass"} diff --git a/tests/components/icloud/__init__.py b/tests/components/icloud/__init__.py new file mode 100644 index 00000000000..b85f1017e45 --- /dev/null +++ b/tests/components/icloud/__init__.py @@ -0,0 +1 @@ +"""Tests for the iCloud component.""" diff --git a/tests/components/icloud/test_config_flow.py b/tests/components/icloud/test_config_flow.py new file mode 100644 index 00000000000..5555150befc --- /dev/null +++ b/tests/components/icloud/test_config_flow.py @@ -0,0 +1,308 @@ +"""Tests for the iCloud config flow.""" +from unittest.mock import MagicMock, Mock, patch + +from pyicloud.exceptions import PyiCloudFailedLoginException +import pytest + +from homeassistant import data_entry_flow +from homeassistant.components.icloud import config_flow +from homeassistant.components.icloud.config_flow import ( + CONF_TRUSTED_DEVICE, + CONF_VERIFICATION_CODE, +) +from homeassistant.components.icloud.const import ( + CONF_ACCOUNT_NAME, + CONF_GPS_ACCURACY_THRESHOLD, + CONF_MAX_INTERVAL, + DEFAULT_GPS_ACCURACY_THRESHOLD, + DEFAULT_MAX_INTERVAL, + DOMAIN, +) +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.helpers.typing import HomeAssistantType + +from tests.common import MockConfigEntry + +USERNAME = "username@me.com" +PASSWORD = "password" +ACCOUNT_NAME = "Account name 1 2 3" +ACCOUNT_NAME_FROM_USERNAME = None +MAX_INTERVAL = 15 +GPS_ACCURACY_THRESHOLD = 250 + +TRUSTED_DEVICES = [ + {"deviceType": "SMS", "areaCode": "", "phoneNumber": "*******58", "deviceId": "1"} +] + + +@pytest.fixture(name="service") +def mock_controller_service(): + """Mock a successful service.""" + with patch( + "homeassistant.components.icloud.config_flow.PyiCloudService" + ) as service_mock: + service_mock.return_value.requires_2fa = True + yield service_mock + + +@pytest.fixture(name="service_with_cookie") +def mock_controller_service_with_cookie(): + """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.trusted_devices = TRUSTED_DEVICES + service_mock.return_value.send_verification_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.trusted_devices = TRUSTED_DEVICES + service_mock.return_value.send_verification_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.trusted_devices = TRUSTED_DEVICES + service_mock.return_value.send_verification_code = Mock(return_value=True) + service_mock.return_value.validate_verification_code = Mock(return_value=False) + yield service_mock + + +def init_config_flow(hass: HomeAssistantType): + """Init a configuration flow.""" + flow = config_flow.IcloudFlowHandler() + flow.hass = hass + return flow + + +async def test_user(hass: HomeAssistantType, service: MagicMock): + """Test user config.""" + flow = init_config_flow(hass) + + result = await flow.async_step_user() + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + # test with all provided + result = await flow.async_step_user( + {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == CONF_TRUSTED_DEVICE + + +async def test_user_with_cookie( + hass: HomeAssistantType, service_with_cookie: MagicMock +): + """Test user config with presence of a cookie.""" + flow = init_config_flow(hass) + + # test with all provided + result = await flow.async_step_user( + {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 + assert result["data"][CONF_PASSWORD] == PASSWORD + assert result["data"][CONF_ACCOUNT_NAME] == ACCOUNT_NAME_FROM_USERNAME + assert result["data"][CONF_MAX_INTERVAL] == DEFAULT_MAX_INTERVAL + assert result["data"][CONF_GPS_ACCURACY_THRESHOLD] == DEFAULT_GPS_ACCURACY_THRESHOLD + + +async def test_import(hass: HomeAssistantType, service: MagicMock): + """Test import step.""" + flow = init_config_flow(hass) + + # import with username and password + result = await flow.async_step_import( + {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "trusted_device" + + # import with all + result = await flow.async_step_import( + { + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_ACCOUNT_NAME: ACCOUNT_NAME, + CONF_MAX_INTERVAL: MAX_INTERVAL, + CONF_GPS_ACCURACY_THRESHOLD: GPS_ACCURACY_THRESHOLD, + } + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "trusted_device" + + +async def test_import_with_cookie( + hass: HomeAssistantType, service_with_cookie: MagicMock +): + """Test import step with presence of a cookie.""" + flow = init_config_flow(hass) + + # import with username and password + result = await flow.async_step_import( + {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 + assert result["data"][CONF_PASSWORD] == PASSWORD + assert result["data"][CONF_ACCOUNT_NAME] == ACCOUNT_NAME_FROM_USERNAME + assert result["data"][CONF_MAX_INTERVAL] == DEFAULT_MAX_INTERVAL + assert result["data"][CONF_GPS_ACCURACY_THRESHOLD] == DEFAULT_GPS_ACCURACY_THRESHOLD + + # import with all + result = await flow.async_step_import( + { + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_ACCOUNT_NAME: ACCOUNT_NAME, + CONF_MAX_INTERVAL: MAX_INTERVAL, + CONF_GPS_ACCURACY_THRESHOLD: GPS_ACCURACY_THRESHOLD, + } + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == USERNAME + assert result["data"][CONF_USERNAME] == USERNAME + assert result["data"][CONF_PASSWORD] == PASSWORD + assert result["data"][CONF_ACCOUNT_NAME] == ACCOUNT_NAME + assert result["data"][CONF_MAX_INTERVAL] == MAX_INTERVAL + assert result["data"][CONF_GPS_ACCURACY_THRESHOLD] == GPS_ACCURACY_THRESHOLD + + +async def test_abort_if_already_setup(hass: HomeAssistantType): + """Test we abort if the account is already setup.""" + flow = init_config_flow(hass) + MockConfigEntry( + domain=DOMAIN, data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} + ).add_to_hass(hass) + + # Should fail, same USERNAME (import) + result = await flow.async_step_import( + {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "username_exists" + + # Should fail, same ACCOUNT_NAME (import) + result = await flow.async_step_import( + { + CONF_USERNAME: "other_username@icloud.com", + CONF_PASSWORD: PASSWORD, + CONF_ACCOUNT_NAME: ACCOUNT_NAME_FROM_USERNAME, + } + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "username_exists" + + # Should fail, same USERNAME (flow) + result = await flow.async_step_user( + {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {CONF_USERNAME: "username_exists"} + + +async def test_login_failed(hass: HomeAssistantType): + """Test when we have errors during login.""" + flow = init_config_flow(hass) + + with patch( + "pyicloud.base.PyiCloudService.authenticate", + side_effect=PyiCloudFailedLoginException(), + ): + result = await flow.async_step_user( + {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {CONF_USERNAME: "login"} + + +async def test_trusted_device(hass: HomeAssistantType, service: MagicMock): + """Test trusted_device step.""" + flow = init_config_flow(hass) + await flow.async_step_user({CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}) + + result = await flow.async_step_trusted_device() + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == CONF_TRUSTED_DEVICE + + +async def test_trusted_device_success(hass: HomeAssistantType, service: MagicMock): + """Test trusted_device step success.""" + flow = init_config_flow(hass) + await flow.async_step_user({CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}) + + result = await flow.async_step_trusted_device({CONF_TRUSTED_DEVICE: 0}) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == CONF_VERIFICATION_CODE + + +async def test_send_verification_code_failed( + hass: HomeAssistantType, service_send_verification_code_failed: MagicMock +): + """Test when we have errors during send_verification_code.""" + flow = init_config_flow(hass) + await flow.async_step_user({CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}) + + result = await flow.async_step_trusted_device({CONF_TRUSTED_DEVICE: 0}) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == CONF_TRUSTED_DEVICE + assert result["errors"] == {CONF_TRUSTED_DEVICE: "send_verification_code"} + + +async def test_verification_code(hass: HomeAssistantType): + """Test verification_code step.""" + flow = init_config_flow(hass) + await flow.async_step_user({CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}) + + result = await flow.async_step_verification_code() + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == CONF_VERIFICATION_CODE + + +async def test_verification_code_success( + hass: HomeAssistantType, service_with_cookie: MagicMock +): + """Test verification_code step success.""" + flow = init_config_flow(hass) + await flow.async_step_user({CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}) + + result = await flow.async_step_verification_code({CONF_VERIFICATION_CODE: 0}) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == USERNAME + assert result["data"][CONF_USERNAME] == USERNAME + assert result["data"][CONF_PASSWORD] == PASSWORD + assert result["data"][CONF_ACCOUNT_NAME] is None + assert result["data"][CONF_MAX_INTERVAL] == DEFAULT_MAX_INTERVAL + assert result["data"][CONF_GPS_ACCURACY_THRESHOLD] == DEFAULT_GPS_ACCURACY_THRESHOLD + + +async def test_validate_verification_code_failed( + hass: HomeAssistantType, service_validate_verification_code_failed: MagicMock +): + """Test when we have errors during validate_verification_code.""" + flow = init_config_flow(hass) + await flow.async_step_user({CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}) + + result = await flow.async_step_verification_code({CONF_VERIFICATION_CODE: 0}) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == CONF_TRUSTED_DEVICE + assert result["errors"] == {"base": "validate_verification_code"} diff --git a/tests/components/ifttt/test_init.py b/tests/components/ifttt/test_init.py index a71e2921888..74d12ba44f4 100644 --- a/tests/components/ifttt/test_init.py +++ b/tests/components/ifttt/test_init.py @@ -2,8 +2,8 @@ from unittest.mock import patch from homeassistant import data_entry_flow -from homeassistant.core import callback from homeassistant.components import ifttt +from homeassistant.core import callback async def test_config_flow_registers_webhook(hass, aiohttp_client): diff --git a/tests/components/ign_sismologia/test_geo_location.py b/tests/components/ign_sismologia/test_geo_location.py index 4babbb6a425..0f0191f3b82 100644 --- a/tests/components/ign_sismologia/test_geo_location.py +++ b/tests/components/ign_sismologia/test_geo_location.py @@ -1,34 +1,35 @@ """The tests for the IGN Sismologia (Earthquakes) Feed platform.""" import datetime -from unittest.mock import patch, MagicMock, call +from unittest.mock import MagicMock, call, patch from homeassistant.components import geo_location from homeassistant.components.geo_location import ATTR_SOURCE from homeassistant.components.ign_sismologia.geo_location import ( ATTR_EXTERNAL_ID, - SCAN_INTERVAL, - ATTR_REGION, - ATTR_MAGNITUDE, ATTR_IMAGE_URL, + ATTR_MAGNITUDE, ATTR_PUBLICATION_DATE, + ATTR_REGION, ATTR_TITLE, + SCAN_INTERVAL, ) from homeassistant.const import ( - EVENT_HOMEASSISTANT_START, - CONF_RADIUS, + ATTR_ATTRIBUTION, + ATTR_FRIENDLY_NAME, + ATTR_ICON, ATTR_LATITUDE, ATTR_LONGITUDE, - ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, - ATTR_ATTRIBUTION, CONF_LATITUDE, CONF_LONGITUDE, - ATTR_ICON, + CONF_RADIUS, + EVENT_HOMEASSISTANT_START, ) from homeassistant.setup import async_setup_component -from tests.common import assert_setup_component, async_fire_time_changed import homeassistant.util.dt as dt_util +from tests.common import assert_setup_component, async_fire_time_changed + CONFIG = {geo_location.DOMAIN: [{"platform": "ign_sismologia", CONF_RADIUS: 200}]} CONFIG_WITH_CUSTOM_LOCATION = { @@ -93,7 +94,7 @@ async def test_setup(hass): # Patching 'utcnow' to gain more control over the timed update. utcnow = dt_util.utcnow() with patch("homeassistant.util.dt.utcnow", return_value=utcnow), patch( - "georss_ign_sismologia_client." "IgnSismologiaFeed" + "georss_ign_sismologia_client.IgnSismologiaFeed" ) as mock_feed: mock_feed.return_value.update.return_value = ( "OK", @@ -198,7 +199,7 @@ async def test_setup_with_custom_location(hass): # Set up some mock feed entries for this test. mock_entry_1 = _generate_mock_feed_entry("1234", "Title 1", 20.5, (38.1, -3.1)) - with patch("georss_ign_sismologia_client." "IgnSismologiaFeed") as mock_feed: + with patch("georss_ign_sismologia_client.IgnSismologiaFeed") as mock_feed: mock_feed.return_value.update.return_value = "OK", [mock_entry_1] with assert_setup_component(1, geo_location.DOMAIN): diff --git a/tests/components/image_processing/common.py b/tests/components/image_processing/common.py index b767884503d..8522353d3f2 100644 --- a/tests/components/image_processing/common.py +++ b/tests/components/image_processing/common.py @@ -4,20 +4,20 @@ All containing methods are legacy helpers that should not be used by new components. Instead call the service directly. """ from homeassistant.components.image_processing import DOMAIN, SERVICE_SCAN -from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL from homeassistant.core import callback from homeassistant.loader import bind_hass @bind_hass -def scan(hass, entity_id=None): +def scan(hass, entity_id=ENTITY_MATCH_ALL): """Force process of all cameras or given entity.""" hass.add_job(async_scan, hass, entity_id) @callback @bind_hass -def async_scan(hass, entity_id=None): +def async_scan(hass, entity_id=ENTITY_MATCH_ALL): """Force process of all cameras or given entity.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_SCAN, data)) diff --git a/tests/components/image_processing/test_init.py b/tests/components/image_processing/test_init.py index 88c870c78fb..3503fcfb9a2 100644 --- a/tests/components/image_processing/test_init.py +++ b/tests/components/image_processing/test_init.py @@ -1,17 +1,17 @@ """The tests for the image_processing component.""" -from unittest.mock import patch, PropertyMock +from unittest.mock import PropertyMock, patch -from homeassistant.core import callback -from homeassistant.const import ATTR_ENTITY_PICTURE -from homeassistant.setup import setup_component -from homeassistant.exceptions import HomeAssistantError import homeassistant.components.http as http import homeassistant.components.image_processing as ip +from homeassistant.const import ATTR_ENTITY_PICTURE +from homeassistant.core import callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.setup import setup_component from tests.common import ( + assert_setup_component, get_test_home_assistant, get_test_instance_port, - assert_setup_component, ) from tests.components.image_processing import common diff --git a/tests/components/imap_email_content/test_sensor.py b/tests/components/imap_email_content/test_sensor.py index fcb9da6ddf3..ee39bac51ef 100644 --- a/tests/components/imap_email_content/test_sensor.py +++ b/tests/components/imap_email_content/test_sensor.py @@ -1,14 +1,14 @@ """The tests for the IMAP email content sensor platform.""" from collections import deque +import datetime import email from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText -import datetime import unittest -from homeassistant.helpers.template import Template -from homeassistant.helpers.event import track_state_change from homeassistant.components.imap_email_content import sensor as imap_email_content +from homeassistant.helpers.event import track_state_change +from homeassistant.helpers.template import Template from tests.common import get_test_home_assistant diff --git a/tests/components/influxdb/test_init.py b/tests/components/influxdb/test_init.py index 26f3c9bcd0b..1dd2681b7f2 100644 --- a/tests/components/influxdb/test_init.py +++ b/tests/components/influxdb/test_init.py @@ -3,14 +3,14 @@ import datetime import unittest from unittest import mock -from homeassistant.setup import setup_component import homeassistant.components.influxdb as influxdb from homeassistant.const import EVENT_STATE_CHANGED, STATE_OFF, STATE_ON, STATE_STANDBY +from homeassistant.setup import setup_component from tests.common import get_test_home_assistant -@mock.patch("influxdb.InfluxDBClient") +@mock.patch("homeassistant.components.influxdb.InfluxDBClient") @mock.patch( "homeassistant.components.influxdb.InfluxThread.batch_timeout", mock.Mock(return_value=0), diff --git a/tests/components/input_boolean/test_init.py b/tests/components/input_boolean/test_init.py index def8db9b35c..2d504114c78 100644 --- a/tests/components/input_boolean/test_init.py +++ b/tests/components/input_boolean/test_init.py @@ -1,21 +1,22 @@ """The tests for the input_boolean component.""" # pylint: disable=protected-access -import asyncio import logging +from unittest.mock import patch -from homeassistant.core import CoreState, State, Context -from homeassistant.setup import async_setup_component -from homeassistant.components.input_boolean import is_on, CONF_INITIAL, DOMAIN +from homeassistant.components.input_boolean import CONF_INITIAL, DOMAIN, is_on from homeassistant.const import ( - STATE_ON, - STATE_OFF, ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, ATTR_ICON, + SERVICE_RELOAD, SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, ) +from homeassistant.core import Context, CoreState, State +from homeassistant.setup import async_setup_component from tests.common import mock_component, mock_restore_cache @@ -94,8 +95,7 @@ async def test_config_options(hass): assert "mdi:work" == state_2.attributes.get(ATTR_ICON) -@asyncio.coroutine -def test_restore_state(hass): +async def test_restore_state(hass): """Ensure states are restored on startup.""" mock_restore_cache( hass, @@ -109,7 +109,7 @@ def test_restore_state(hass): hass.state = CoreState.starting mock_component(hass, "recorder") - yield from async_setup_component(hass, DOMAIN, {DOMAIN: {"b1": None, "b2": None}}) + await async_setup_component(hass, DOMAIN, {DOMAIN: {"b1": None, "b2": None}}) state = hass.states.get("input_boolean.b1") assert state @@ -120,8 +120,7 @@ def test_restore_state(hass): assert state.state == "off" -@asyncio.coroutine -def test_initial_state_overrules_restore_state(hass): +async def test_initial_state_overrules_restore_state(hass): """Ensure states are restored on startup.""" mock_restore_cache( hass, (State("input_boolean.b1", "on"), State("input_boolean.b2", "off")) @@ -129,7 +128,7 @@ def test_initial_state_overrules_restore_state(hass): hass.state = CoreState.starting - yield from async_setup_component( + await async_setup_component( hass, DOMAIN, {DOMAIN: {"b1": {CONF_INITIAL: False}, "b2": {CONF_INITIAL: True}}}, @@ -165,3 +164,71 @@ async def test_input_boolean_context(hass, hass_admin_user): assert state2 is not None assert state.state != state2.state assert state2.context.user_id == hass_admin_user.id + + +async def test_reload(hass, hass_admin_user): + """Test reload service.""" + count_start = len(hass.states.async_entity_ids()) + + _LOGGER.debug("ENTITIES @ start: %s", hass.states.async_entity_ids()) + + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + "test_1": None, + "test_2": {"name": "Hello World", "icon": "mdi:work", "initial": True}, + } + }, + ) + + _LOGGER.debug("ENTITIES: %s", hass.states.async_entity_ids()) + + assert count_start + 2 == len(hass.states.async_entity_ids()) + + state_1 = hass.states.get("input_boolean.test_1") + state_2 = hass.states.get("input_boolean.test_2") + state_3 = hass.states.get("input_boolean.test_3") + + assert state_1 is not None + assert state_2 is not None + assert state_3 is None + assert STATE_ON == state_2.state + + with patch( + "homeassistant.config.load_yaml_config_file", + autospec=True, + return_value={ + DOMAIN: { + "test_2": { + "name": "Hello World reloaded", + "icon": "mdi:work_reloaded", + "initial": False, + }, + "test_3": None, + } + }, + ): + with patch("homeassistant.config.find_config_file", return_value=""): + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + blocking=True, + context=Context(user_id=hass_admin_user.id), + ) + await hass.async_block_till_done() + + assert count_start + 2 == len(hass.states.async_entity_ids()) + + state_1 = hass.states.get("input_boolean.test_1") + state_2 = hass.states.get("input_boolean.test_2") + state_3 = hass.states.get("input_boolean.test_3") + + assert state_1 is None + assert state_2 is not None + assert state_3 is not None + + assert STATE_OFF == state_2.state + assert "Hello World reloaded" == state_2.attributes.get(ATTR_FRIENDLY_NAME) + assert "mdi:work_reloaded" == state_2.attributes.get(ATTR_ICON) diff --git a/tests/components/input_datetime/test_init.py b/tests/components/input_datetime/test_init.py index 2ddeddbefac..6908c4fc5f1 100644 --- a/tests/components/input_datetime/test_init.py +++ b/tests/components/input_datetime/test_init.py @@ -1,21 +1,23 @@ """Tests for the Input slider component.""" # pylint: disable=protected-access -import asyncio import datetime +from unittest.mock import patch import pytest import voluptuous as vol -from homeassistant.core import CoreState, State, Context -from homeassistant.setup import async_setup_component from homeassistant.components.input_datetime import ( - DOMAIN, ATTR_DATE, ATTR_DATETIME, ATTR_TIME, + DOMAIN, + SERVICE_RELOAD, SERVICE_SET_DATETIME, ) from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import Context, CoreState, State +from homeassistant.exceptions import Unauthorized +from homeassistant.setup import async_setup_component from tests.common import mock_restore_cache @@ -189,10 +191,9 @@ async def test_set_invalid_2(hass): assert state.state == initial -@asyncio.coroutine -def test_set_datetime_date(hass): +async def test_set_datetime_date(hass): """Test set_datetime method with only date.""" - yield from async_setup_component( + await async_setup_component( hass, DOMAIN, {DOMAIN: {"test_date": {"has_time": False, "has_date": True}}} ) @@ -201,7 +202,7 @@ def test_set_datetime_date(hass): dt_obj = datetime.datetime(2017, 9, 7, 19, 46) date_portion = dt_obj.date() - yield from async_set_date_and_time(hass, entity_id, dt_obj) + await async_set_date_and_time(hass, entity_id, dt_obj) state = hass.states.get(entity_id) assert state.state == str(date_portion) @@ -212,8 +213,7 @@ def test_set_datetime_date(hass): assert state.attributes["timestamp"] == date_dt_obj.timestamp() -@asyncio.coroutine -def test_restore_state(hass): +async def test_restore_state(hass): """Ensure states are restored on startup.""" mock_restore_cache( hass, @@ -229,7 +229,7 @@ def test_restore_state(hass): initial = datetime.datetime(2017, 1, 1, 23, 42) - yield from async_setup_component( + await async_setup_component( hass, DOMAIN, { @@ -260,10 +260,9 @@ def test_restore_state(hass): assert state_bogus.state == str(initial) -@asyncio.coroutine -def test_default_value(hass): +async def test_default_value(hass): """Test default value if none has been set via inital or restore state.""" - yield from async_setup_component( + await async_setup_component( hass, DOMAIN, { @@ -310,3 +309,65 @@ async def test_input_datetime_context(hass, hass_admin_user): assert state2 is not None assert state.state != state2.state assert state2.context.user_id == hass_admin_user.id + + +async def test_reload(hass, hass_admin_user, hass_read_only_user): + """Test reload service.""" + count_start = len(hass.states.async_entity_ids()) + + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + "dt1": {"has_time": False, "has_date": True, "initial": "2019-1-1"}, + } + }, + ) + + assert count_start + 1 == len(hass.states.async_entity_ids()) + + state_1 = hass.states.get("input_datetime.dt1") + state_2 = hass.states.get("input_datetime.dt2") + + dt_obj = datetime.datetime(2019, 1, 1, 0, 0) + assert state_1 is not None + assert state_2 is None + assert str(dt_obj.date()) == state_1.state + + with patch( + "homeassistant.config.load_yaml_config_file", + autospec=True, + return_value={ + DOMAIN: { + "dt1": {"has_time": True, "has_date": False, "initial": "23:32"}, + "dt2": {"has_time": True, "has_date": True}, + } + }, + ): + with patch("homeassistant.config.find_config_file", return_value=""): + with pytest.raises(Unauthorized): + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + blocking=True, + context=Context(user_id=hass_read_only_user.id), + ) + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + blocking=True, + context=Context(user_id=hass_admin_user.id), + ) + await hass.async_block_till_done() + + assert count_start + 2 == len(hass.states.async_entity_ids()) + + state_1 = hass.states.get("input_datetime.dt1") + state_2 = hass.states.get("input_datetime.dt2") + + dt_obj = datetime.datetime(2019, 1, 1, 23, 32) + assert state_1 is not None + assert state_2 is not None + assert str(dt_obj.time()) == state_1.state + assert str(datetime.datetime(1970, 1, 1, 0, 0)) == state_2.state diff --git a/tests/components/input_number/test_init.py b/tests/components/input_number/test_init.py index 02d59c367c9..6d032b639cf 100644 --- a/tests/components/input_number/test_init.py +++ b/tests/components/input_number/test_init.py @@ -1,16 +1,20 @@ """The tests for the Input number component.""" # pylint: disable=protected-access -import asyncio +from unittest.mock import patch + +import pytest -from homeassistant.core import CoreState, State, Context from homeassistant.components.input_number import ( ATTR_VALUE, DOMAIN, SERVICE_DECREMENT, SERVICE_INCREMENT, + SERVICE_RELOAD, SERVICE_SET_VALUE, ) from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import Context, CoreState, State +from homeassistant.exceptions import Unauthorized from homeassistant.loader import bind_hass from homeassistant.setup import async_setup_component @@ -166,8 +170,7 @@ async def test_mode(hass): assert "slider" == state.attributes["mode"] -@asyncio.coroutine -def test_restore_state(hass): +async def test_restore_state(hass): """Ensure states are restored on startup.""" mock_restore_cache( hass, (State("input_number.b1", "70"), State("input_number.b2", "200")) @@ -175,7 +178,7 @@ def test_restore_state(hass): hass.state = CoreState.starting - yield from async_setup_component( + await async_setup_component( hass, DOMAIN, {DOMAIN: {"b1": {"min": 0, "max": 100}, "b2": {"min": 10, "max": 100}}}, @@ -190,8 +193,7 @@ def test_restore_state(hass): assert float(state.state) == 10 -@asyncio.coroutine -def test_initial_state_overrules_restore_state(hass): +async def test_initial_state_overrules_restore_state(hass): """Ensure states are restored on startup.""" mock_restore_cache( hass, (State("input_number.b1", "70"), State("input_number.b2", "200")) @@ -199,7 +201,7 @@ def test_initial_state_overrules_restore_state(hass): hass.state = CoreState.starting - yield from async_setup_component( + await async_setup_component( hass, DOMAIN, { @@ -219,14 +221,11 @@ def test_initial_state_overrules_restore_state(hass): assert float(state.state) == 60 -@asyncio.coroutine -def test_no_initial_state_and_no_restore_state(hass): +async def test_no_initial_state_and_no_restore_state(hass): """Ensure that entity is create without initial and restore feature.""" hass.state = CoreState.starting - yield from async_setup_component( - hass, DOMAIN, {DOMAIN: {"b1": {"min": 0, "max": 100}}} - ) + await async_setup_component(hass, DOMAIN, {DOMAIN: {"b1": {"min": 0, "max": 100}}}) state = hass.states.get("input_number.b1") assert state @@ -254,3 +253,57 @@ async def test_input_number_context(hass, hass_admin_user): assert state2 is not None assert state.state != state2.state assert state2.context.user_id == hass_admin_user.id + + +async def test_reload(hass, hass_admin_user, hass_read_only_user): + """Test reload service.""" + count_start = len(hass.states.async_entity_ids()) + + assert await async_setup_component( + hass, DOMAIN, {DOMAIN: {"test_1": {"initial": 50, "min": 0, "max": 51}}} + ) + + assert count_start + 1 == len(hass.states.async_entity_ids()) + + state_1 = hass.states.get("input_number.test_1") + state_2 = hass.states.get("input_number.test_2") + + assert state_1 is not None + assert state_2 is None + assert 50 == float(state_1.state) + + with patch( + "homeassistant.config.load_yaml_config_file", + autospec=True, + return_value={ + DOMAIN: { + "test_1": {"initial": 40, "min": 0, "max": 51}, + "test_2": {"initial": 20, "min": 10, "max": 30}, + } + }, + ): + with patch("homeassistant.config.find_config_file", return_value=""): + with pytest.raises(Unauthorized): + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + blocking=True, + context=Context(user_id=hass_read_only_user.id), + ) + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + blocking=True, + context=Context(user_id=hass_admin_user.id), + ) + await hass.async_block_till_done() + + assert count_start + 2 == len(hass.states.async_entity_ids()) + + state_1 = hass.states.get("input_number.test_1") + state_2 = hass.states.get("input_number.test_2") + + assert state_1 is not None + assert state_2 is not None + assert 40 == float(state_1.state) + assert 20 == float(state_2.state) diff --git a/tests/components/input_select/test_init.py b/tests/components/input_select/test_init.py index cbf9bd5f4ee..8fda80cd3d2 100644 --- a/tests/components/input_select/test_init.py +++ b/tests/components/input_select/test_init.py @@ -1,19 +1,27 @@ """The tests for the Input select component.""" # pylint: disable=protected-access -import asyncio +from unittest.mock import patch + +import pytest -from homeassistant.loader import bind_hass from homeassistant.components.input_select import ( ATTR_OPTION, ATTR_OPTIONS, DOMAIN, - SERVICE_SET_OPTIONS, SERVICE_SELECT_NEXT, SERVICE_SELECT_OPTION, SERVICE_SELECT_PREVIOUS, + SERVICE_SET_OPTIONS, ) -from homeassistant.const import ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, ATTR_ICON -from homeassistant.core import State, Context +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, + ATTR_ICON, + SERVICE_RELOAD, +) +from homeassistant.core import Context, State +from homeassistant.exceptions import Unauthorized +from homeassistant.loader import bind_hass from homeassistant.setup import async_setup_component from tests.common import mock_restore_cache @@ -240,8 +248,7 @@ async def test_set_options_service(hass): assert "test2" == state.state -@asyncio.coroutine -def test_restore_state(hass): +async def test_restore_state(hass): """Ensure states are restored on startup.""" mock_restore_cache( hass, @@ -253,9 +260,7 @@ def test_restore_state(hass): options = {"options": ["first option", "middle option", "last option"]} - yield from async_setup_component( - hass, DOMAIN, {DOMAIN: {"s1": options, "s2": options}} - ) + await async_setup_component(hass, DOMAIN, {DOMAIN: {"s1": options, "s2": options}}) state = hass.states.get("input_select.s1") assert state @@ -266,8 +271,7 @@ def test_restore_state(hass): assert state.state == "first option" -@asyncio.coroutine -def test_initial_state_overrules_restore_state(hass): +async def test_initial_state_overrules_restore_state(hass): """Ensure states are restored on startup.""" mock_restore_cache( hass, @@ -282,9 +286,7 @@ def test_initial_state_overrules_restore_state(hass): "initial": "middle option", } - yield from async_setup_component( - hass, DOMAIN, {DOMAIN: {"s1": options, "s2": options}} - ) + await async_setup_component(hass, DOMAIN, {DOMAIN: {"s1": options, "s2": options}}) state = hass.states.get("input_select.s1") assert state @@ -322,3 +324,81 @@ async def test_input_select_context(hass, hass_admin_user): assert state2 is not None assert state.state != state2.state assert state2.context.user_id == hass_admin_user.id + + +async def test_reload(hass, hass_admin_user, hass_read_only_user): + """Test reload service.""" + count_start = len(hass.states.async_entity_ids()) + + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + "test_1": { + "options": ["first option", "middle option", "last option"], + "initial": "middle option", + }, + "test_2": { + "options": ["an option", "not an option"], + "initial": "an option", + }, + } + }, + ) + + assert count_start + 2 == len(hass.states.async_entity_ids()) + + state_1 = hass.states.get("input_select.test_1") + state_2 = hass.states.get("input_select.test_2") + state_3 = hass.states.get("input_select.test_3") + + 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 + + with patch( + "homeassistant.config.load_yaml_config_file", + autospec=True, + return_value={ + DOMAIN: { + "test_2": { + "options": ["an option", "reloaded option"], + "initial": "reloaded option", + }, + "test_3": { + "options": ["new option", "newer option"], + "initial": "newer option", + }, + } + }, + ): + with patch("homeassistant.config.find_config_file", return_value=""): + with pytest.raises(Unauthorized): + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + blocking=True, + context=Context(user_id=hass_read_only_user.id), + ) + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + blocking=True, + context=Context(user_id=hass_admin_user.id), + ) + await hass.async_block_till_done() + + assert count_start + 2 == len(hass.states.async_entity_ids()) + + state_1 = hass.states.get("input_select.test_1") + state_2 = hass.states.get("input_select.test_2") + state_3 = hass.states.get("input_select.test_3") + + assert state_1 is None + assert state_2 is not None + assert state_3 is not None + assert "reloaded option" == state_2.state + assert "newer option" == state_3.state diff --git a/tests/components/input_text/test_init.py b/tests/components/input_text/test_init.py index 4888994d788..8835128d672 100644 --- a/tests/components/input_text/test_init.py +++ b/tests/components/input_text/test_init.py @@ -1,10 +1,13 @@ """The tests for the Input text component.""" # pylint: disable=protected-access -import asyncio +from unittest.mock import patch + +import pytest from homeassistant.components.input_text import ATTR_VALUE, DOMAIN, SERVICE_SET_VALUE -from homeassistant.const import ATTR_ENTITY_ID -from homeassistant.core import CoreState, State, Context +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_RELOAD +from homeassistant.core import Context, CoreState, State +from homeassistant.exceptions import Unauthorized from homeassistant.loader import bind_hass from homeassistant.setup import async_setup_component @@ -96,8 +99,7 @@ async def test_mode(hass): assert "password" == state.attributes["mode"] -@asyncio.coroutine -def test_restore_state(hass): +async def test_restore_state(hass): """Ensure states are restored on startup.""" mock_restore_cache( hass, @@ -106,10 +108,8 @@ def test_restore_state(hass): hass.state = CoreState.starting - yield from async_setup_component( - hass, - DOMAIN, - {DOMAIN: {"b1": {"min": 0, "max": 10}, "b2": {"min": 0, "max": 10}}}, + assert await async_setup_component( + hass, DOMAIN, {DOMAIN: {"b1": None, "b2": {"min": 0, "max": 10}}}, ) state = hass.states.get("input_text.b1") @@ -121,8 +121,7 @@ def test_restore_state(hass): assert str(state.state) == "unknown" -@asyncio.coroutine -def test_initial_state_overrules_restore_state(hass): +async def test_initial_state_overrules_restore_state(hass): """Ensure states are restored on startup.""" mock_restore_cache( hass, @@ -131,7 +130,7 @@ def test_initial_state_overrules_restore_state(hass): hass.state = CoreState.starting - yield from async_setup_component( + await async_setup_component( hass, DOMAIN, { @@ -151,14 +150,11 @@ def test_initial_state_overrules_restore_state(hass): assert str(state.state) == "test" -@asyncio.coroutine -def test_no_initial_state_and_no_restore_state(hass): +async def test_no_initial_state_and_no_restore_state(hass): """Ensure that entity is create without initial and restore feature.""" hass.state = CoreState.starting - yield from async_setup_component( - hass, DOMAIN, {DOMAIN: {"b1": {"min": 0, "max": 100}}} - ) + await async_setup_component(hass, DOMAIN, {DOMAIN: {"b1": {"min": 0, "max": 100}}}) state = hass.states.get("input_text.b1") assert state @@ -195,3 +191,64 @@ async def test_config_none(hass): state = hass.states.get("input_text.b1") assert state assert str(state.state) == "unknown" + + +async def test_reload(hass, hass_admin_user, hass_read_only_user): + """Test reload service.""" + count_start = len(hass.states.async_entity_ids()) + + assert await async_setup_component( + hass, + DOMAIN, + {DOMAIN: {"test_1": {"initial": "test 1"}, "test_2": {"initial": "test 2"}}}, + ) + + assert count_start + 2 == len(hass.states.async_entity_ids()) + + state_1 = hass.states.get("input_text.test_1") + state_2 = hass.states.get("input_text.test_2") + state_3 = hass.states.get("input_text.test_3") + + assert state_1 is not None + assert state_2 is not None + assert state_3 is None + assert "test 1" == state_1.state + assert "test 2" == state_2.state + + with patch( + "homeassistant.config.load_yaml_config_file", + autospec=True, + return_value={ + DOMAIN: { + "test_2": {"initial": "test reloaded"}, + "test_3": {"initial": "test 3"}, + } + }, + ): + with patch("homeassistant.config.find_config_file", return_value=""): + with pytest.raises(Unauthorized): + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + blocking=True, + context=Context(user_id=hass_read_only_user.id), + ) + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + blocking=True, + context=Context(user_id=hass_admin_user.id), + ) + await hass.async_block_till_done() + + assert count_start + 2 == len(hass.states.async_entity_ids()) + + state_1 = hass.states.get("input_text.test_1") + state_2 = hass.states.get("input_text.test_2") + state_3 = hass.states.get("input_text.test_3") + + assert state_1 is None + assert state_2 is not None + assert state_3 is not None + assert "test reloaded" == state_2.state + assert "test 3" == state_3.state diff --git a/tests/components/intent/__init__.py b/tests/components/intent/__init__.py new file mode 100644 index 00000000000..463f53d921c --- /dev/null +++ b/tests/components/intent/__init__.py @@ -0,0 +1 @@ +"""Tests for the Intent integration.""" diff --git a/tests/components/intent/test_init.py b/tests/components/intent/test_init.py new file mode 100644 index 00000000000..56344b6affe --- /dev/null +++ b/tests/components/intent/test_init.py @@ -0,0 +1,76 @@ +"""Tests for Intent component.""" +import pytest + +from homeassistant.components.cover import SERVICE_OPEN_COVER +from homeassistant.helpers import intent +from homeassistant.setup import async_setup_component + +from tests.common import async_mock_service + + +async def test_http_handle_intent(hass, hass_client, hass_admin_user): + """Test handle intent via HTTP API.""" + + class TestIntentHandler(intent.IntentHandler): + """Test Intent Handler.""" + + intent_type = "OrderBeer" + + async def async_handle(self, intent): + """Handle the intent.""" + assert intent.context.user_id == hass_admin_user.id + response = intent.create_response() + response.async_set_speech( + "I've ordered a {}!".format(intent.slots["type"]["value"]) + ) + response.async_set_card( + "Beer ordered", "You chose a {}.".format(intent.slots["type"]["value"]) + ) + return response + + intent.async_register(hass, TestIntentHandler()) + + result = await async_setup_component(hass, "intent", {}) + assert result + + client = await hass_client() + resp = await client.post( + "/api/intent/handle", json={"name": "OrderBeer", "data": {"type": "Belgian"}} + ) + + assert resp.status == 200 + data = await resp.json() + + assert data == { + "card": { + "simple": {"content": "You chose a Belgian.", "title": "Beer ordered"} + }, + "speech": {"plain": {"extra_data": None, "speech": "I've ordered a Belgian!"}}, + } + + +async def test_cover_intents_loading(hass): + """Test Cover Intents Loading.""" + assert await async_setup_component(hass, "intent", {}) + + with pytest.raises(intent.UnknownIntent): + await intent.async_handle( + hass, "test", "HassOpenCover", {"name": {"value": "garage door"}} + ) + + assert await async_setup_component(hass, "cover", {}) + + hass.states.async_set("cover.garage_door", "closed") + calls = async_mock_service(hass, "cover", SERVICE_OPEN_COVER) + + response = await intent.async_handle( + hass, "test", "HassOpenCover", {"name": {"value": "garage door"}} + ) + await hass.async_block_till_done() + + assert response.speech["plain"]["speech"] == "Opened garage door" + assert len(calls) == 1 + call = calls[0] + assert call.domain == "cover" + assert call.service == "open_cover" + assert call.data == {"entity_id": "cover.garage_door"} diff --git a/tests/components/ios/test_init.py b/tests/components/ios/test_init.py index 9a169987298..31eb43fc611 100644 --- a/tests/components/ios/test_init.py +++ b/tests/components/ios/test_init.py @@ -4,8 +4,8 @@ from unittest.mock import patch import pytest from homeassistant import config_entries, data_entry_flow -from homeassistant.setup import async_setup_component from homeassistant.components import ios +from homeassistant.setup import async_setup_component from tests.common import mock_component, mock_coro diff --git a/tests/components/ipma/test_config_flow.py b/tests/components/ipma/test_config_flow.py index 0850a15d620..fd44f8b2a58 100644 --- a/tests/components/ipma/test_config_flow.py +++ b/tests/components/ipma/test_config_flow.py @@ -1,10 +1,10 @@ """Tests for IPMA config flow.""" from unittest.mock import Mock, patch -from tests.common import mock_coro - -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE from homeassistant.components.ipma import config_flow +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE + +from tests.common import mock_coro async def test_show_config_form(): diff --git a/tests/components/ipma/test_weather.py b/tests/components/ipma/test_weather.py index 5e63f6fa5c7..de13d3c94b2 100644 --- a/tests/components/ipma/test_weather.py +++ b/tests/components/ipma/test_weather.py @@ -1,6 +1,6 @@ """The tests for the IPMA weather component.""" -from unittest.mock import patch from collections import namedtuple +from unittest.mock import patch from homeassistant.components import weather from homeassistant.components.weather import ( @@ -11,9 +11,9 @@ from homeassistant.components.weather import ( ATTR_WEATHER_WIND_SPEED, DOMAIN as WEATHER_DOMAIN, ) +from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, mock_coro -from homeassistant.setup import async_setup_component TEST_CONFIG = {"name": "HomeTown", "latitude": "40.00", "longitude": "-8.00"} diff --git a/tests/components/islamic_prayer_times/test_sensor.py b/tests/components/islamic_prayer_times/test_sensor.py index 734b82076c2..3151b030637 100644 --- a/tests/components/islamic_prayer_times/test_sensor.py +++ b/tests/components/islamic_prayer_times/test_sensor.py @@ -1,10 +1,11 @@ """The tests for the Islamic prayer times sensor platform.""" from datetime import datetime, timedelta from unittest.mock import patch -from homeassistant.setup import async_setup_component + from homeassistant.components.islamic_prayer_times.sensor import IslamicPrayerTimesData -from tests.common import MockDependency +from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util + from tests.common import async_fire_time_changed LATITUDE = 41 @@ -34,8 +35,10 @@ async def test_islamic_prayer_times_min_config(hass): """Test minimum Islamic prayer times configuration.""" min_config_sensors = ["fajr", "dhuhr", "asr", "maghrib", "isha"] - with MockDependency("prayer_times_calculator") as mock_pt_calc: - mock_pt_calc.PrayerTimesCalculator.return_value.fetch_prayer_times.return_value = ( + with patch( + "homeassistant.components.islamic_prayer_times.sensor.PrayerTimesCalculator" + ) as PrayerTimesCalculator: + PrayerTimesCalculator.return_value.fetch_prayer_times.return_value = ( PRAYER_TIMES ) @@ -63,8 +66,10 @@ async def test_islamic_prayer_times_multiple_sensors(hass): "midnight", ] - with MockDependency("prayer_times_calculator") as mock_pt_calc: - mock_pt_calc.PrayerTimesCalculator.return_value.fetch_prayer_times.return_value = ( + with patch( + "homeassistant.components.islamic_prayer_times.sensor.PrayerTimesCalculator" + ) as PrayerTimesCalculator: + PrayerTimesCalculator.return_value.fetch_prayer_times.return_value = ( PRAYER_TIMES ) @@ -87,8 +92,10 @@ async def test_islamic_prayer_times_with_calculation_method(hass): """Test Islamic prayer times configuration with calculation method.""" sensors = ["fajr", "maghrib"] - with MockDependency("prayer_times_calculator") as mock_pt_calc: - mock_pt_calc.PrayerTimesCalculator.return_value.fetch_prayer_times.return_value = ( + with patch( + "homeassistant.components.islamic_prayer_times.sensor.PrayerTimesCalculator" + ) as PrayerTimesCalculator: + PrayerTimesCalculator.return_value.fetch_prayer_times.return_value = ( PRAYER_TIMES ) @@ -113,8 +120,10 @@ async def test_islamic_prayer_times_with_calculation_method(hass): async def test_islamic_prayer_times_data_get_prayer_times(hass): """Test Islamic prayer times data fetcher.""" - with MockDependency("prayer_times_calculator") as mock_pt_calc: - mock_pt_calc.PrayerTimesCalculator.return_value.fetch_prayer_times.return_value = ( + with patch( + "homeassistant.components.islamic_prayer_times.sensor.PrayerTimesCalculator" + ) as PrayerTimesCalculator: + PrayerTimesCalculator.return_value.fetch_prayer_times.return_value = ( PRAYER_TIMES ) @@ -138,8 +147,10 @@ async def test_islamic_prayer_times_sensor_update(hass): "Midnight": "00:45", } - with MockDependency("prayer_times_calculator") as mock_pt_calc: - mock_pt_calc.PrayerTimesCalculator.return_value.fetch_prayer_times.side_effect = [ + with patch( + "homeassistant.components.islamic_prayer_times.sensor.PrayerTimesCalculator" + ) as PrayerTimesCalculator: + PrayerTimesCalculator.return_value.fetch_prayer_times.side_effect = [ PRAYER_TIMES, new_prayer_times, ] @@ -164,7 +175,7 @@ async def test_islamic_prayer_times_sensor_update(hass): future = midnight_dt + timedelta(days=1, minutes=1) with patch( - "homeassistant.components.islamic_prayer_times.sensor" ".dt_util.utcnow", + "homeassistant.components.islamic_prayer_times.sensor.dt_util.utcnow", return_value=future, ): diff --git a/tests/components/izone/test_config_flow.py b/tests/components/izone/test_config_flow.py index faa920271e3..5deafeb08a7 100644 --- a/tests/components/izone/test_config_flow.py +++ b/tests/components/izone/test_config_flow.py @@ -5,7 +5,7 @@ from unittest.mock import Mock, patch import pytest from homeassistant import config_entries, data_entry_flow -from homeassistant.components.izone.const import IZONE, DISPATCH_CONTROLLER_DISCOVERED +from homeassistant.components.izone.const import DISPATCH_CONTROLLER_DISCOVERED, IZONE from tests.common import mock_coro @@ -33,9 +33,9 @@ async def test_not_found(hass, mock_disco): """Test not finding iZone controller.""" with patch( - "homeassistant.components.izone.discovery.async_start_discovery_service" + "homeassistant.components.izone.config_flow.async_start_discovery_service" ) as start_disco, patch( - "homeassistant.components.izone.discovery.async_stop_discovery_service", + "homeassistant.components.izone.config_flow.async_stop_discovery_service", return_value=mock_coro(), ) as stop_disco: start_disco.side_effect = _mock_start_discovery(hass, mock_disco) @@ -62,7 +62,7 @@ async def test_found(hass, mock_disco): "homeassistant.components.izone.climate.async_setup_entry", return_value=mock_coro(True), ) as mock_setup, patch( - "homeassistant.components.izone.discovery.async_start_discovery_service" + "homeassistant.components.izone.config_flow.async_start_discovery_service" ) as start_disco, patch( "homeassistant.components.izone.async_start_discovery_service", return_value=mock_coro(), diff --git a/tests/components/jewish_calendar/__init__.py b/tests/components/jewish_calendar/__init__.py index 54589a640cc..592086461d3 100644 --- a/tests/components/jewish_calendar/__init__.py +++ b/tests/components/jewish_calendar/__init__.py @@ -1,13 +1,12 @@ """Tests for the jewish_calendar component.""" -from datetime import datetime from collections import namedtuple from contextlib import contextmanager +from datetime import datetime from unittest.mock import patch from homeassistant.components import jewish_calendar import homeassistant.util.dt as dt_util - _LatLng = namedtuple("_LatLng", ["lat", "lng"]) NYC_LATLNG = _LatLng(40.7128, -74.0060) diff --git a/tests/components/jewish_calendar/test_binary_sensor.py b/tests/components/jewish_calendar/test_binary_sensor.py index 64745d8929f..a9ea2449c6f 100644 --- a/tests/components/jewish_calendar/test_binary_sensor.py +++ b/tests/components/jewish_calendar/test_binary_sensor.py @@ -1,17 +1,16 @@ """The tests for the Jewish calendar binary sensors.""" -from datetime import timedelta -from datetime import datetime as dt +from datetime import datetime as dt, timedelta import pytest -from homeassistant.const import STATE_ON, STATE_OFF -import homeassistant.util.dt as dt_util -from homeassistant.setup import async_setup_component from homeassistant.components import jewish_calendar +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util + +from . import alter_time, make_jerusalem_test_params, make_nyc_test_params from tests.common import async_fire_time_changed -from . import alter_time, make_nyc_test_params, make_jerusalem_test_params - MELACHA_PARAMS = [ make_nyc_test_params(dt(2018, 9, 1, 16, 0), STATE_ON), diff --git a/tests/components/jewish_calendar/test_sensor.py b/tests/components/jewish_calendar/test_sensor.py index 07e0b7cb192..e630702c6b2 100644 --- a/tests/components/jewish_calendar/test_sensor.py +++ b/tests/components/jewish_calendar/test_sensor.py @@ -1,15 +1,15 @@ """The tests for the Jewish calendar sensors.""" -from datetime import timedelta -from datetime import datetime as dt +from datetime import datetime as dt, timedelta import pytest -import homeassistant.util.dt as dt_util -from homeassistant.setup import async_setup_component from homeassistant.components import jewish_calendar -from tests.common import async_fire_time_changed +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util -from . import alter_time, make_nyc_test_params, make_jerusalem_test_params +from . import alter_time, make_jerusalem_test_params, make_nyc_test_params + +from tests.common import async_fire_time_changed async def test_jewish_calendar_min_config(hass): @@ -180,8 +180,9 @@ async def test_jewish_calendar_sensor( assert sensor_object.state == str(result) if sensor == "holiday": - assert sensor_object.attributes.get("type") == "YOM_TOV" assert sensor_object.attributes.get("id") == "rosh_hashana_i" + assert sensor_object.attributes.get("type") == "YOM_TOV" + assert sensor_object.attributes.get("type_id") == 1 SHABBAT_PARAMS = [ diff --git a/tests/components/kira/test_init.py b/tests/components/kira/test_init.py index 9588c2bc1fa..e5056235127 100644 --- a/tests/components/kira/test_init.py +++ b/tests/components/kira/test_init.py @@ -3,9 +3,8 @@ import os import shutil import tempfile - import unittest -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch import homeassistant.components.kira as kira from homeassistant.setup import setup_component diff --git a/tests/components/light/common.py b/tests/components/light/common.py index 416c02884dc..aa1e62db5bf 100644 --- a/tests/components/light/common.py +++ b/tests/components/light/common.py @@ -21,6 +21,7 @@ from homeassistant.components.light import ( ) from homeassistant.const import ( ATTR_ENTITY_ID, + ENTITY_MATCH_ALL, SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, @@ -31,7 +32,7 @@ from homeassistant.loader import bind_hass @bind_hass def turn_on( hass, - entity_id=None, + entity_id=ENTITY_MATCH_ALL, transition=None, brightness=None, brightness_pct=None, @@ -69,7 +70,7 @@ def turn_on( async def async_turn_on( hass, - entity_id=None, + entity_id=ENTITY_MATCH_ALL, transition=None, brightness=None, brightness_pct=None, @@ -110,12 +111,12 @@ async def async_turn_on( @bind_hass -def turn_off(hass, entity_id=None, transition=None): +def turn_off(hass, entity_id=ENTITY_MATCH_ALL, transition=None): """Turn all or specified light off.""" hass.add_job(async_turn_off, hass, entity_id, transition) -async def async_turn_off(hass, entity_id=None, transition=None): +async def async_turn_off(hass, entity_id=ENTITY_MATCH_ALL, transition=None): """Turn all or specified light off.""" data = { key: value @@ -127,12 +128,12 @@ async def async_turn_off(hass, entity_id=None, transition=None): @bind_hass -def toggle(hass, entity_id=None, transition=None): +def toggle(hass, entity_id=ENTITY_MATCH_ALL, transition=None): """Toggle all or specified light.""" hass.add_job(async_toggle, hass, entity_id, transition) -async def async_toggle(hass, entity_id=None, transition=None): +async def async_toggle(hass, entity_id=ENTITY_MATCH_ALL, transition=None): """Toggle all or specified light.""" data = { key: value diff --git a/tests/components/light/test_device_action.py b/tests/components/light/test_device_action.py index bb50778db52..a3cf57a7dbe 100644 --- a/tests/components/light/test_device_action.py +++ b/tests/components/light/test_device_action.py @@ -1,18 +1,18 @@ """The test for light device automation.""" import pytest -from homeassistant.components.light import DOMAIN -from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM -from homeassistant.setup import async_setup_component import homeassistant.components.automation as automation +from homeassistant.components.light import DOMAIN +from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, - async_get_device_automations, ) diff --git a/tests/components/light/test_device_condition.py b/tests/components/light/test_device_condition.py index a9f4adddfab..7a560dd781d 100644 --- a/tests/components/light/test_device_condition.py +++ b/tests/components/light/test_device_condition.py @@ -1,22 +1,23 @@ """The test for light device automation.""" from datetime import timedelta -import pytest from unittest.mock import patch -from homeassistant.components.light import DOMAIN -from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM -from homeassistant.setup import async_setup_component +import pytest + import homeassistant.components.automation as automation +from homeassistant.components.light import DOMAIN +from homeassistant.const import CONF_PLATFORM, 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, + async_get_device_automation_capabilities, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, - async_get_device_automations, - async_get_device_automation_capabilities, ) diff --git a/tests/components/light/test_device_trigger.py b/tests/components/light/test_device_trigger.py index a6437ef9ee0..dd8320c166e 100644 --- a/tests/components/light/test_device_trigger.py +++ b/tests/components/light/test_device_trigger.py @@ -1,22 +1,23 @@ """The test for light device automation.""" from datetime import timedelta + import pytest -from homeassistant.components.light import DOMAIN -from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM -from homeassistant.setup import async_setup_component import homeassistant.components.automation as automation +from homeassistant.components.light import DOMAIN +from homeassistant.const import CONF_PLATFORM, 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, async_fire_time_changed, + async_get_device_automation_capabilities, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, - async_get_device_automations, - async_get_device_automation_capabilities, ) diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index 8ceda6cbd3e..18cc032bd98 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -1,34 +1,27 @@ """The tests for the Light component.""" # pylint: disable=protected-access +from io import StringIO +import os import unittest import unittest.mock as mock -import os -from io import StringIO import pytest from homeassistant import core -from homeassistant.exceptions import Unauthorized -from homeassistant.setup import setup_component, async_setup_component +from homeassistant.components import light from homeassistant.const import ( ATTR_ENTITY_ID, - STATE_ON, - STATE_OFF, CONF_PLATFORM, - SERVICE_TURN_ON, - SERVICE_TURN_OFF, SERVICE_TOGGLE, - ATTR_SUPPORTED_FEATURES, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, ) -from homeassistant.components import light -from homeassistant.helpers.intent import IntentHandleError +from homeassistant.exceptions import Unauthorized +from homeassistant.setup import async_setup_component, setup_component -from tests.common import ( - async_mock_service, - mock_service, - get_test_home_assistant, - mock_storage, -) +from tests.common import get_test_home_assistant, mock_service, mock_storage from tests.components.light import common @@ -433,89 +426,10 @@ class TestLight(unittest.TestCase): assert {light.ATTR_HS_COLOR: (50.353, 100), light.ATTR_BRIGHTNESS: 100} == data -async def test_intent_set_color(hass): - """Test the set color intent.""" - hass.states.async_set( - "light.hello_2", "off", {ATTR_SUPPORTED_FEATURES: light.SUPPORT_COLOR} - ) - hass.states.async_set("switch.hello", "off") - calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON) - hass.helpers.intent.async_register(light.SetIntentHandler()) - - result = await hass.helpers.intent.async_handle( - "test", - light.INTENT_SET, - {"name": {"value": "Hello"}, "color": {"value": "blue"}}, - ) - await hass.async_block_till_done() - - assert result.speech["plain"]["speech"] == "Changed hello 2 to the color blue" - - assert len(calls) == 1 - call = calls[0] - assert call.domain == light.DOMAIN - assert call.service == SERVICE_TURN_ON - assert call.data.get(ATTR_ENTITY_ID) == "light.hello_2" - assert call.data.get(light.ATTR_RGB_COLOR) == (0, 0, 255) - - -async def test_intent_set_color_tests_feature(hass): - """Test the set color intent.""" - hass.states.async_set("light.hello", "off") - calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON) - hass.helpers.intent.async_register(light.SetIntentHandler()) - - try: - await hass.helpers.intent.async_handle( - "test", - light.INTENT_SET, - {"name": {"value": "Hello"}, "color": {"value": "blue"}}, - ) - assert False, "handling intent should have raised" - except IntentHandleError as err: - assert str(err) == "Entity hello does not support changing colors" - - assert len(calls) == 0 - - -async def test_intent_set_color_and_brightness(hass): - """Test the set color intent.""" - hass.states.async_set( - "light.hello_2", - "off", - {ATTR_SUPPORTED_FEATURES: (light.SUPPORT_COLOR | light.SUPPORT_BRIGHTNESS)}, - ) - hass.states.async_set("switch.hello", "off") - calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON) - hass.helpers.intent.async_register(light.SetIntentHandler()) - - result = await hass.helpers.intent.async_handle( - "test", - light.INTENT_SET, - { - "name": {"value": "Hello"}, - "color": {"value": "blue"}, - "brightness": {"value": "20"}, - }, - ) - await hass.async_block_till_done() - - assert ( - result.speech["plain"]["speech"] - == "Changed hello 2 to the color blue and 20% brightness" - ) - - assert len(calls) == 1 - call = calls[0] - assert call.domain == light.DOMAIN - assert call.service == SERVICE_TURN_ON - assert call.data.get(ATTR_ENTITY_ID) == "light.hello_2" - assert call.data.get(light.ATTR_RGB_COLOR) == (0, 0, 255) - assert call.data.get(light.ATTR_BRIGHTNESS_PCT) == 20 - - async def test_light_context(hass, hass_admin_user): """Test that light context works.""" + platform = getattr(hass.components, "test.light") + platform.init() assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) state = hass.states.get("light.ceiling") @@ -537,6 +451,8 @@ async def test_light_context(hass, hass_admin_user): async def test_light_turn_on_auth(hass, hass_admin_user): """Test that light context works.""" + platform = getattr(hass.components, "test.light") + platform.init() assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) state = hass.states.get("light.ceiling") diff --git a/tests/components/light/test_intent.py b/tests/components/light/test_intent.py new file mode 100644 index 00000000000..4adba921d5e --- /dev/null +++ b/tests/components/light/test_intent.py @@ -0,0 +1,88 @@ +"""Tests for the light intents.""" +from homeassistant.components import light +from homeassistant.components.light import intent +from homeassistant.const import ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_TURN_ON +from homeassistant.helpers.intent import IntentHandleError + +from tests.common import async_mock_service + + +async def test_intent_set_color(hass): + """Test the set color intent.""" + hass.states.async_set( + "light.hello_2", "off", {ATTR_SUPPORTED_FEATURES: light.SUPPORT_COLOR} + ) + hass.states.async_set("switch.hello", "off") + calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON) + await intent.async_setup_intents(hass) + + result = await hass.helpers.intent.async_handle( + "test", + intent.INTENT_SET, + {"name": {"value": "Hello"}, "color": {"value": "blue"}}, + ) + await hass.async_block_till_done() + + assert result.speech["plain"]["speech"] == "Changed hello 2 to the color blue" + + assert len(calls) == 1 + call = calls[0] + assert call.domain == light.DOMAIN + assert call.service == SERVICE_TURN_ON + assert call.data.get(ATTR_ENTITY_ID) == "light.hello_2" + assert call.data.get(light.ATTR_RGB_COLOR) == (0, 0, 255) + + +async def test_intent_set_color_tests_feature(hass): + """Test the set color intent.""" + hass.states.async_set("light.hello", "off") + calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON) + await intent.async_setup_intents(hass) + + try: + await hass.helpers.intent.async_handle( + "test", + intent.INTENT_SET, + {"name": {"value": "Hello"}, "color": {"value": "blue"}}, + ) + assert False, "handling intent should have raised" + except IntentHandleError as err: + assert str(err) == "Entity hello does not support changing colors" + + assert len(calls) == 0 + + +async def test_intent_set_color_and_brightness(hass): + """Test the set color intent.""" + hass.states.async_set( + "light.hello_2", + "off", + {ATTR_SUPPORTED_FEATURES: (light.SUPPORT_COLOR | light.SUPPORT_BRIGHTNESS)}, + ) + hass.states.async_set("switch.hello", "off") + calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON) + await intent.async_setup_intents(hass) + + result = await hass.helpers.intent.async_handle( + "test", + intent.INTENT_SET, + { + "name": {"value": "Hello"}, + "color": {"value": "blue"}, + "brightness": {"value": "20"}, + }, + ) + await hass.async_block_till_done() + + assert ( + result.speech["plain"]["speech"] + == "Changed hello 2 to the color blue and 20% brightness" + ) + + assert len(calls) == 1 + call = calls[0] + assert call.domain == light.DOMAIN + assert call.service == SERVICE_TURN_ON + assert call.data.get(ATTR_ENTITY_ID) == "light.hello_2" + assert call.data.get(light.ATTR_RGB_COLOR) == (0, 0, 255) + assert call.data.get(light.ATTR_BRIGHTNESS_PCT) == 20 diff --git a/tests/components/linky/test_config_flow.py b/tests/components/linky/test_config_flow.py index f18ce72c1c3..2b90c778a8f 100644 --- a/tests/components/linky/test_config_flow.py +++ b/tests/components/linky/test_config_flow.py @@ -1,16 +1,17 @@ """Tests for the Linky config flow.""" -import pytest from unittest.mock import patch + from pylinky.exceptions import ( PyLinkyAccessException, PyLinkyEnedisException, PyLinkyException, PyLinkyWrongLoginException, ) +import pytest from homeassistant import data_entry_flow from homeassistant.components.linky import config_flow -from homeassistant.components.linky.const import DOMAIN, DEFAULT_TIMEOUT +from homeassistant.components.linky.const import DEFAULT_TIMEOUT, DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME from tests.common import MockConfigEntry diff --git a/tests/components/litejet/test_init.py b/tests/components/litejet/test_init.py index 8dd1440a7bc..3861e7a058e 100644 --- a/tests/components/litejet/test_init.py +++ b/tests/components/litejet/test_init.py @@ -3,6 +3,7 @@ import logging import unittest from homeassistant.components import litejet + from tests.common import get_test_home_assistant _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/litejet/test_light.py b/tests/components/litejet/test_light.py index 1fc6f1df94e..e4ca1c2106e 100644 --- a/tests/components/litejet/test_light.py +++ b/tests/components/litejet/test_light.py @@ -21,7 +21,7 @@ ENTITY_OTHER_LIGHT_NUMBER = 2 class TestLiteJetLight(unittest.TestCase): """Test the litejet component.""" - @mock.patch("pylitejet.LiteJet") + @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() diff --git a/tests/components/litejet/test_scene.py b/tests/components/litejet/test_scene.py index d9ab561b6e1..0f42ac40cdf 100644 --- a/tests/components/litejet/test_scene.py +++ b/tests/components/litejet/test_scene.py @@ -20,7 +20,7 @@ ENTITY_OTHER_SCENE_NUMBER = 2 class TestLiteJetScene(unittest.TestCase): """Test the litejet component.""" - @mock.patch("pylitejet.LiteJet") + @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() diff --git a/tests/components/litejet/test_switch.py b/tests/components/litejet/test_switch.py index b7f98087254..a9cf54dc1f6 100644 --- a/tests/components/litejet/test_switch.py +++ b/tests/components/litejet/test_switch.py @@ -21,7 +21,7 @@ ENTITY_OTHER_SWITCH_NUMBER = 2 class TestLiteJetSwitch(unittest.TestCase): """Test the litejet component.""" - @mock.patch("pylitejet.LiteJet") + @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() diff --git a/tests/components/local_file/test_camera.py b/tests/components/local_file/test_camera.py index ae71954bf4a..042b0f76400 100644 --- a/tests/components/local_file/test_camera.py +++ b/tests/components/local_file/test_camera.py @@ -2,8 +2,7 @@ import asyncio from unittest import mock -from homeassistant.components.camera.const import DOMAIN -from homeassistant.components.local_file.camera import SERVICE_UPDATE_FILE_PATH +from homeassistant.components.local_file.const import DOMAIN, SERVICE_UPDATE_FILE_PATH from homeassistant.setup import async_setup_component from tests.common import mock_registry diff --git a/tests/components/local_ip/__init__.py b/tests/components/local_ip/__init__.py new file mode 100644 index 00000000000..47e1f70fcf8 --- /dev/null +++ b/tests/components/local_ip/__init__.py @@ -0,0 +1 @@ +"""Tests for the local_ip integration.""" diff --git a/tests/components/local_ip/test_config_flow.py b/tests/components/local_ip/test_config_flow.py new file mode 100644 index 00000000000..f355e5c75b2 --- /dev/null +++ b/tests/components/local_ip/test_config_flow.py @@ -0,0 +1,19 @@ +"""Tests for the local_ip config_flow.""" +from homeassistant.components.local_ip import DOMAIN + + +async def test_config_flow(hass): + """Test we can finish a config flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"} + ) + assert result["type"] == "form" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"name": "test"} + ) + assert result["type"] == "create_entry" + + await hass.async_block_till_done() + state = hass.states.get("sensor.test") + assert state diff --git a/tests/components/local_ip/test_init.py b/tests/components/local_ip/test_init.py new file mode 100644 index 00000000000..fb43f06eea2 --- /dev/null +++ b/tests/components/local_ip/test_init.py @@ -0,0 +1,22 @@ +"""Tests for the local_ip component.""" +import pytest + +from homeassistant.components.local_ip import DOMAIN +from homeassistant.setup import async_setup_component +from homeassistant.util import get_local_ip + + +@pytest.fixture(name="config") +def config_fixture(): + """Create hass config fixture.""" + return {DOMAIN: {"name": "test"}} + + +async def test_basic_setup(hass, config): + """Test component setup creates entry from config.""" + assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + local_ip = await hass.async_add_executor_job(get_local_ip) + state = hass.states.get("sensor.test") + assert state + assert state.state == local_ip diff --git a/tests/components/locative/test_init.py b/tests/components/locative/test_init.py index 1abbf25d433..009a3d469c5 100644 --- a/tests/components/locative/test_init.py +++ b/tests/components/locative/test_init.py @@ -1,5 +1,5 @@ """The tests the for Locative device tracker platform.""" -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch import pytest diff --git a/tests/components/lock/common.py b/tests/components/lock/common.py index 2ad8f075bce..ec477cba78d 100644 --- a/tests/components/lock/common.py +++ b/tests/components/lock/common.py @@ -7,15 +7,16 @@ from homeassistant.components.lock import DOMAIN from homeassistant.const import ( ATTR_CODE, ATTR_ENTITY_ID, + ENTITY_MATCH_ALL, SERVICE_LOCK, - SERVICE_UNLOCK, SERVICE_OPEN, + SERVICE_UNLOCK, ) from homeassistant.loader import bind_hass @bind_hass -def lock(hass, entity_id=None, code=None): +def lock(hass, entity_id=ENTITY_MATCH_ALL, code=None): """Lock all or specified locks.""" data = {} if code: @@ -26,7 +27,7 @@ def lock(hass, entity_id=None, code=None): hass.services.call(DOMAIN, SERVICE_LOCK, data) -async def async_lock(hass, entity_id=None, code=None): +async def async_lock(hass, entity_id=ENTITY_MATCH_ALL, code=None): """Lock all or specified locks.""" data = {} if code: @@ -38,7 +39,7 @@ async def async_lock(hass, entity_id=None, code=None): @bind_hass -def unlock(hass, entity_id=None, code=None): +def unlock(hass, entity_id=ENTITY_MATCH_ALL, code=None): """Unlock all or specified locks.""" data = {} if code: @@ -49,7 +50,7 @@ def unlock(hass, entity_id=None, code=None): hass.services.call(DOMAIN, SERVICE_UNLOCK, data) -async def async_unlock(hass, entity_id=None, code=None): +async def async_unlock(hass, entity_id=ENTITY_MATCH_ALL, code=None): """Lock all or specified locks.""" data = {} if code: @@ -61,7 +62,7 @@ async def async_unlock(hass, entity_id=None, code=None): @bind_hass -def open_lock(hass, entity_id=None, code=None): +def open_lock(hass, entity_id=ENTITY_MATCH_ALL, code=None): """Open all or specified locks.""" data = {} if code: @@ -72,7 +73,7 @@ def open_lock(hass, entity_id=None, code=None): hass.services.call(DOMAIN, SERVICE_OPEN, data) -async def async_open_lock(hass, entity_id=None, code=None): +async def async_open_lock(hass, entity_id=ENTITY_MATCH_ALL, code=None): """Lock all or specified locks.""" data = {} if code: diff --git a/tests/components/lock/test_device_action.py b/tests/components/lock/test_device_action.py index 2006f9b3ff1..0fc98d9460e 100644 --- a/tests/components/lock/test_device_action.py +++ b/tests/components/lock/test_device_action.py @@ -1,19 +1,19 @@ """The tests for Lock device actions.""" import pytest +import homeassistant.components.automation as automation from homeassistant.components.lock import DOMAIN from homeassistant.const import CONF_PLATFORM -from homeassistant.setup import async_setup_component -import homeassistant.components.automation as automation from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, assert_lists_same, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, - async_get_device_automations, ) diff --git a/tests/components/lock/test_device_condition.py b/tests/components/lock/test_device_condition.py index 675f402e770..638a7edf5d7 100644 --- a/tests/components/lock/test_device_condition.py +++ b/tests/components/lock/test_device_condition.py @@ -1,19 +1,19 @@ """The tests for Lock device conditions.""" import pytest +import homeassistant.components.automation as automation from homeassistant.components.lock import DOMAIN from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED -from homeassistant.setup import async_setup_component -import homeassistant.components.automation as automation from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, assert_lists_same, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, - async_get_device_automations, ) diff --git a/tests/components/lock/test_device_trigger.py b/tests/components/lock/test_device_trigger.py index 572e28c44a6..781ed03307b 100644 --- a/tests/components/lock/test_device_trigger.py +++ b/tests/components/lock/test_device_trigger.py @@ -1,19 +1,19 @@ """The tests for Lock device triggers.""" import pytest +import homeassistant.components.automation as automation from homeassistant.components.lock import DOMAIN from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED -from homeassistant.setup import async_setup_component -import homeassistant.components.automation as automation from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, assert_lists_same, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, - async_get_device_automations, ) diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index 6ff043dff28..1b48f301529 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -1,30 +1,13 @@ """The tests for the logbook component.""" # pylint: disable=protected-access,invalid-name +from datetime import datetime, timedelta import logging -from datetime import timedelta, datetime import unittest import pytest import voluptuous as vol -from homeassistant.components import sun -import homeassistant.core as ha -from homeassistant.const import ( - ATTR_ENTITY_ID, - ATTR_SERVICE, - ATTR_NAME, - EVENT_STATE_CHANGED, - EVENT_HOMEASSISTANT_START, - EVENT_HOMEASSISTANT_STOP, - EVENT_AUTOMATION_TRIGGERED, - EVENT_SCRIPT_STARTED, - ATTR_HIDDEN, - STATE_NOT_HOME, - STATE_ON, - STATE_OFF, -) -import homeassistant.util.dt as dt_util -from homeassistant.components import logbook, recorder +from homeassistant.components import logbook, recorder, sun from homeassistant.components.alexa.smart_home import EVENT_ALEXA_SMART_HOME from homeassistant.components.homekit.const import ( ATTR_DISPLAY_NAME, @@ -32,10 +15,25 @@ from homeassistant.components.homekit.const import ( DOMAIN as DOMAIN_HOMEKIT, EVENT_HOMEKIT_CHANGED, ) -from homeassistant.setup import setup_component, async_setup_component - -from tests.common import init_recorder_component, get_test_home_assistant +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_HIDDEN, + ATTR_NAME, + ATTR_SERVICE, + EVENT_AUTOMATION_TRIGGERED, + EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP, + EVENT_SCRIPT_STARTED, + EVENT_STATE_CHANGED, + STATE_NOT_HOME, + STATE_OFF, + STATE_ON, +) +import homeassistant.core as ha +from homeassistant.setup import async_setup_component, setup_component +import homeassistant.util.dt as dt_util +from tests.common import get_test_home_assistant, init_recorder_component _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/logentries/test_init.py b/tests/components/logentries/test_init.py index 8c69ec5ff87..f850a7dd62b 100644 --- a/tests/components/logentries/test_init.py +++ b/tests/components/logentries/test_init.py @@ -3,9 +3,9 @@ import unittest from unittest import mock -from homeassistant.setup import setup_component import homeassistant.components.logentries as logentries -from homeassistant.const import STATE_ON, STATE_OFF, EVENT_STATE_CHANGED +from homeassistant.const import EVENT_STATE_CHANGED, STATE_OFF, STATE_ON +from homeassistant.setup import setup_component from tests.common import get_test_home_assistant @@ -70,7 +70,7 @@ class TestLogentries(unittest.TestCase): } ] payload = { - "host": "https://webhook.logentries.com/noformat/" "logs/token", + "host": "https://webhook.logentries.com/noformat/logs/token", "event": body, } self.handler_method(event) diff --git a/tests/components/logger/test_init.py b/tests/components/logger/test_init.py index eac6060a5bc..00fa5aa3558 100644 --- a/tests/components/logger/test_init.py +++ b/tests/components/logger/test_init.py @@ -3,8 +3,8 @@ from collections import namedtuple import logging import unittest -from homeassistant.setup import setup_component from homeassistant.components import logger +from homeassistant.setup import setup_component from tests.common import get_test_home_assistant diff --git a/tests/components/logi_circle/test_config_flow.py b/tests/components/logi_circle/test_config_flow.py index 02a9437a225..7ba3816b5e2 100644 --- a/tests/components/logi_circle/test_config_flow.py +++ b/tests/components/logi_circle/test_config_flow.py @@ -8,15 +8,12 @@ from homeassistant import data_entry_flow from homeassistant.components.logi_circle import config_flow from homeassistant.components.logi_circle.config_flow import ( DOMAIN, + AuthorizationFailed, LogiCircleAuthCallbackView, ) from homeassistant.setup import async_setup_component -from tests.common import MockDependency, mock_coro - - -class AuthorizationFailed(Exception): - """Dummy Exception.""" +from tests.common import mock_coro class MockRequest: @@ -40,7 +37,7 @@ def init_config_flow(hass): sensors=None, ) flow = config_flow.LogiCircleFlowHandler() - flow._get_authorization_url = Mock( # pylint: disable=W0212 + flow._get_authorization_url = Mock( # pylint: disable=protected-access return_value="http://example.com" ) flow.hass = hass @@ -50,22 +47,20 @@ def init_config_flow(hass): @pytest.fixture def mock_logi_circle(): """Mock logi_circle.""" - with MockDependency("logi_circle", "exception") as mock_logi_circle_: - mock_logi_circle_.exception.AuthorizationFailed = AuthorizationFailed - mock_logi_circle_.LogiCircle().authorize = Mock( - return_value=mock_coro(return_value=True) - ) - mock_logi_circle_.LogiCircle().close = Mock( - return_value=mock_coro(return_value=True) - ) - mock_logi_circle_.LogiCircle().account = mock_coro( - return_value={"accountId": "testId"} - ) - mock_logi_circle_.LogiCircle().authorize_url = "http://authorize.url" - yield mock_logi_circle_ + with patch( + "homeassistant.components.logi_circle.config_flow.LogiCircle" + ) as logi_circle: + LogiCircle = logi_circle() + LogiCircle.authorize = Mock(return_value=mock_coro(return_value=True)) + LogiCircle.close = Mock(return_value=mock_coro(return_value=True)) + LogiCircle.account = mock_coro(return_value={"accountId": "testId"}) + LogiCircle.authorize_url = "http://authorize.url" + yield LogiCircle -async def test_step_import(hass, mock_logi_circle): # pylint: disable=W0621 +async def test_step_import( + hass, mock_logi_circle # pylint: disable=redefined-outer-name +): """Test that we trigger import when configuring with client.""" flow = init_config_flow(hass) @@ -75,8 +70,8 @@ async def test_step_import(hass, mock_logi_circle): # pylint: disable=W0621 async def test_full_flow_implementation( - hass, mock_logi_circle -): # pylint: disable=W0621 + hass, mock_logi_circle # pylint: disable=redefined-outer-name +): """Test registering an implementation and finishing flow works.""" config_flow.register_flow_implementation( hass, @@ -154,10 +149,10 @@ async def test_abort_if_already_setup(hass): ) async def test_abort_if_authorize_fails( hass, mock_logi_circle, side_effect, error -): # pylint: disable=W0621 +): # pylint: disable=redefined-outer-name """Test we abort if authorizing fails.""" flow = init_config_flow(hass) - mock_logi_circle.LogiCircle().authorize.side_effect = side_effect + mock_logi_circle.authorize.side_effect = side_effect result = await flow.async_step_code("123ABC") assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT @@ -176,7 +171,9 @@ async def test_not_pick_implementation_if_only_one(hass): assert result["step_id"] == "auth" -async def test_gen_auth_url(hass, mock_logi_circle): # pylint: disable=W0621 +async def test_gen_auth_url( + hass, mock_logi_circle +): # pylint: disable=redefined-outer-name """Test generating authorize URL from Logi Circle API.""" config_flow.register_flow_implementation( hass, @@ -192,7 +189,7 @@ async def test_gen_auth_url(hass, mock_logi_circle): # pylint: disable=W0621 flow.flow_impl = "test-auth-url" await async_setup_component(hass, "http", {}) - result = flow._get_authorization_url() # pylint: disable=W0212 + result = flow._get_authorization_url() # pylint: disable=protected-access assert result == "http://authorize.url" @@ -206,7 +203,7 @@ async def test_callback_view_rejects_missing_code(hass): async def test_callback_view_accepts_code( hass, mock_logi_circle -): # pylint: disable=W0621 +): # pylint: disable=redefined-outer-name """Test the auth callback view handles requests with auth code.""" init_config_flow(hass) view = LogiCircleAuthCallbackView() @@ -215,4 +212,4 @@ async def test_callback_view_accepts_code( assert resp.status == 200 await hass.async_block_till_done() - mock_logi_circle.LogiCircle.return_value.authorize.assert_called_with("456") + mock_logi_circle.authorize.assert_called_with("456") diff --git a/tests/components/london_air/test_sensor.py b/tests/components/london_air/test_sensor.py index cd1ee32f223..83405095f2e 100644 --- a/tests/components/london_air/test_sensor.py +++ b/tests/components/london_air/test_sensor.py @@ -1,10 +1,12 @@ """The tests for the tube_state platform.""" import unittest + import requests_mock from homeassistant.components.london_air.sensor import CONF_LOCATIONS, URL from homeassistant.setup import setup_component -from tests.common import load_fixture, get_test_home_assistant + +from tests.common import get_test_home_assistant, load_fixture VALID_CONFIG = {"platform": "london_air", CONF_LOCATIONS: ["Merton"]} diff --git a/tests/components/lovelace/test_init.py b/tests/components/lovelace/test_init.py index 16aab911fb0..e8d041e9dfa 100644 --- a/tests/components/lovelace/test_init.py +++ b/tests/components/lovelace/test_init.py @@ -1,10 +1,10 @@ """Test the Lovelace initialization.""" from unittest.mock import patch -from homeassistant.setup import async_setup_component from homeassistant.components import frontend, lovelace +from homeassistant.setup import async_setup_component -from tests.common import get_system_health_info, async_capture_events +from tests.common import async_capture_events, get_system_health_info async def test_lovelace_from_storage(hass, hass_ws_client, hass_storage): diff --git a/tests/components/mailbox/test_init.py b/tests/components/mailbox/test_init.py index ec398be0d4a..6536e1317fa 100644 --- a/tests/components/mailbox/test_init.py +++ b/tests/components/mailbox/test_init.py @@ -1,5 +1,4 @@ """The tests for the mailbox component.""" -import asyncio from hashlib import sha1 import pytest @@ -16,96 +15,88 @@ def mock_http_client(hass, hass_client): return hass.loop.run_until_complete(hass_client()) -@asyncio.coroutine -def test_get_platforms_from_mailbox(mock_http_client): +async def test_get_platforms_from_mailbox(mock_http_client): """Get platforms from mailbox.""" url = "/api/mailbox/platforms" - req = yield from mock_http_client.get(url) + req = await mock_http_client.get(url) assert req.status == 200 - result = yield from req.json() + result = await req.json() assert len(result) == 1 and "DemoMailbox" == result[0].get("name", None) -@asyncio.coroutine -def test_get_messages_from_mailbox(mock_http_client): +async def test_get_messages_from_mailbox(mock_http_client): """Get messages from mailbox.""" url = "/api/mailbox/messages/DemoMailbox" - req = yield from mock_http_client.get(url) + req = await mock_http_client.get(url) assert req.status == 200 - result = yield from req.json() + result = await req.json() assert len(result) == 10 -@asyncio.coroutine -def test_get_media_from_mailbox(mock_http_client): +async def test_get_media_from_mailbox(mock_http_client): """Get audio from mailbox.""" mp3sha = "3f67c4ea33b37d1710f772a26dd3fb43bb159d50" - msgtxt = "Message 1. " "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " + msgtxt = "Message 1. Lorem ipsum dolor sit amet, consectetur adipiscing elit. " msgsha = sha1(msgtxt.encode("utf-8")).hexdigest() url = "/api/mailbox/media/DemoMailbox/%s" % (msgsha) - req = yield from mock_http_client.get(url) + req = await mock_http_client.get(url) assert req.status == 200 - data = yield from req.read() + data = await req.read() assert sha1(data).hexdigest() == mp3sha -@asyncio.coroutine -def test_delete_from_mailbox(mock_http_client): +async def test_delete_from_mailbox(mock_http_client): """Get audio from mailbox.""" - msgtxt1 = "Message 1. " "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " - msgtxt2 = "Message 3. " "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " + msgtxt1 = "Message 1. Lorem ipsum dolor sit amet, consectetur adipiscing elit. " + msgtxt2 = "Message 3. Lorem ipsum dolor sit amet, consectetur adipiscing elit. " msgsha1 = sha1(msgtxt1.encode("utf-8")).hexdigest() msgsha2 = sha1(msgtxt2.encode("utf-8")).hexdigest() for msg in [msgsha1, msgsha2]: url = "/api/mailbox/delete/DemoMailbox/%s" % (msg) - req = yield from mock_http_client.delete(url) + req = await mock_http_client.delete(url) assert req.status == 200 url = "/api/mailbox/messages/DemoMailbox" - req = yield from mock_http_client.get(url) + req = await mock_http_client.get(url) assert req.status == 200 - result = yield from req.json() + result = await req.json() assert len(result) == 8 -@asyncio.coroutine -def test_get_messages_from_invalid_mailbox(mock_http_client): +async def test_get_messages_from_invalid_mailbox(mock_http_client): """Get messages from mailbox.""" url = "/api/mailbox/messages/mailbox.invalid_mailbox" - req = yield from mock_http_client.get(url) + req = await mock_http_client.get(url) assert req.status == 404 -@asyncio.coroutine -def test_get_media_from_invalid_mailbox(mock_http_client): +async def test_get_media_from_invalid_mailbox(mock_http_client): """Get messages from mailbox.""" msgsha = "0000000000000000000000000000000000000000" url = "/api/mailbox/media/mailbox.invalid_mailbox/%s" % (msgsha) - req = yield from mock_http_client.get(url) + req = await mock_http_client.get(url) assert req.status == 404 -@asyncio.coroutine -def test_get_media_from_invalid_msgid(mock_http_client): +async def test_get_media_from_invalid_msgid(mock_http_client): """Get messages from mailbox.""" msgsha = "0000000000000000000000000000000000000000" url = "/api/mailbox/media/DemoMailbox/%s" % (msgsha) - req = yield from mock_http_client.get(url) + req = await mock_http_client.get(url) assert req.status == 500 -@asyncio.coroutine -def test_delete_from_invalid_mailbox(mock_http_client): +async def test_delete_from_invalid_mailbox(mock_http_client): """Get audio from mailbox.""" msgsha = "0000000000000000000000000000000000000000" url = "/api/mailbox/delete/mailbox.invalid_mailbox/%s" % (msgsha) - req = yield from mock_http_client.delete(url) + req = await mock_http_client.delete(url) assert req.status == 404 diff --git a/tests/components/manual/test_alarm_control_panel.py b/tests/components/manual/test_alarm_control_panel.py index 666d906ff03..f1596277e3c 100644 --- a/tests/components/manual/test_alarm_control_panel.py +++ b/tests/components/manual/test_alarm_control_panel.py @@ -1,22 +1,24 @@ """The tests for the manual Alarm Control Panel component.""" from datetime import timedelta -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch + +from homeassistant.components import alarm_control_panel from homeassistant.components.demo import alarm_control_panel as demo -from homeassistant.setup import async_setup_component from homeassistant.const import ( - STATE_ALARM_DISARMED, - STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY, - STATE_ALARM_ARMED_NIGHT, STATE_ALARM_ARMED_CUSTOM_BYPASS, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, ) -from homeassistant.components import alarm_control_panel +from homeassistant.core import CoreState, State +from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util + from tests.common import async_fire_time_changed, mock_component, mock_restore_cache from tests.components.alarm_control_panel import common -from homeassistant.core import State, CoreState CODE = "HELLO_CODE" @@ -109,7 +111,7 @@ async def test_arm_home_with_pending(hass): future = dt_util.utcnow() + timedelta(seconds=1) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -250,7 +252,7 @@ async def test_arm_away_with_pending(hass): future = dt_util.utcnow() + timedelta(seconds=1) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -365,7 +367,7 @@ async def test_arm_night_with_pending(hass): future = dt_util.utcnow() + timedelta(seconds=1) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -430,7 +432,7 @@ async def test_trigger_no_pending(hass): future = dt_util.utcnow() + timedelta(seconds=60) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -472,7 +474,7 @@ async def test_trigger_with_delay(hass): future = dt_util.utcnow() + timedelta(seconds=1) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -561,7 +563,7 @@ async def test_trigger_with_pending(hass): future = dt_util.utcnow() + timedelta(seconds=2) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -572,7 +574,7 @@ async def test_trigger_with_pending(hass): future = dt_util.utcnow() + timedelta(seconds=5) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -616,7 +618,7 @@ async def test_trigger_with_unused_specific_delay(hass): future = dt_util.utcnow() + timedelta(seconds=5) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -660,7 +662,7 @@ async def test_trigger_with_specific_delay(hass): future = dt_util.utcnow() + timedelta(seconds=1) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -704,7 +706,7 @@ async def test_trigger_with_pending_and_delay(hass): future = dt_util.utcnow() + timedelta(seconds=1) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -716,7 +718,7 @@ async def test_trigger_with_pending_and_delay(hass): future += timedelta(seconds=1) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -761,7 +763,7 @@ async def test_trigger_with_pending_and_specific_delay(hass): future = dt_util.utcnow() + timedelta(seconds=1) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -773,7 +775,7 @@ async def test_trigger_with_pending_and_specific_delay(hass): future += timedelta(seconds=1) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -806,7 +808,7 @@ async def test_armed_home_with_specific_pending(hass): future = dt_util.utcnow() + timedelta(seconds=2) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -838,7 +840,7 @@ async def test_armed_away_with_specific_pending(hass): future = dt_util.utcnow() + timedelta(seconds=2) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -870,7 +872,7 @@ async def test_armed_night_with_specific_pending(hass): future = dt_util.utcnow() + timedelta(seconds=2) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -904,7 +906,7 @@ async def test_trigger_with_specific_pending(hass): future = dt_util.utcnow() + timedelta(seconds=2) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -914,7 +916,7 @@ async def test_trigger_with_specific_pending(hass): future = dt_util.utcnow() + timedelta(seconds=5) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -949,7 +951,7 @@ async def test_trigger_with_disarm_after_trigger(hass): future = dt_util.utcnow() + timedelta(seconds=5) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -1011,7 +1013,7 @@ async def test_trigger_with_unused_zero_specific_trigger_time(hass): future = dt_util.utcnow() + timedelta(seconds=5) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -1046,7 +1048,7 @@ async def test_trigger_with_specific_trigger_time(hass): future = dt_util.utcnow() + timedelta(seconds=5) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -1085,7 +1087,7 @@ async def test_trigger_with_no_disarm_after_trigger(hass): future = dt_util.utcnow() + timedelta(seconds=5) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -1124,7 +1126,7 @@ async def test_back_to_back_trigger_with_no_disarm_after_trigger(hass): future = dt_util.utcnow() + timedelta(seconds=5) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -1138,7 +1140,7 @@ async def test_back_to_back_trigger_with_no_disarm_after_trigger(hass): future = dt_util.utcnow() + timedelta(seconds=5) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -1176,7 +1178,7 @@ async def test_disarm_while_pending_trigger(hass): future = dt_util.utcnow() + timedelta(seconds=5) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -1215,7 +1217,7 @@ async def test_disarm_during_trigger_with_invalid_code(hass): future = dt_util.utcnow() + timedelta(seconds=5) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -1340,7 +1342,7 @@ async def test_arm_custom_bypass_with_pending(hass): future = dt_util.utcnow() + timedelta(seconds=1) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -1398,7 +1400,7 @@ async def test_armed_custom_bypass_with_specific_pending(hass): future = dt_util.utcnow() + timedelta(seconds=2) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -1446,7 +1448,7 @@ async def test_arm_away_after_disabled_disarmed(hass): future = dt_util.utcnow() + timedelta(seconds=1) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -1464,7 +1466,7 @@ async def test_arm_away_after_disabled_disarmed(hass): future += timedelta(seconds=1) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) diff --git a/tests/components/manual_mqtt/test_alarm_control_panel.py b/tests/components/manual_mqtt/test_alarm_control_panel.py index 80a1aa8495d..91e97685588 100644 --- a/tests/components/manual_mqtt/test_alarm_control_panel.py +++ b/tests/components/manual_mqtt/test_alarm_control_panel.py @@ -1,26 +1,26 @@ """The tests for the manual_mqtt Alarm Control Panel component.""" from datetime import timedelta import unittest -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch -from homeassistant.setup import setup_component +from homeassistant.components import alarm_control_panel from homeassistant.const import ( - STATE_ALARM_DISARMED, - STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, ) -from homeassistant.components import alarm_control_panel +from homeassistant.setup import setup_component import homeassistant.util.dt as dt_util from tests.common import ( + assert_setup_component, + fire_mqtt_message, fire_time_changed, get_test_home_assistant, mock_mqtt_component, - fire_mqtt_message, - assert_setup_component, ) from tests.components.alarm_control_panel import common diff --git a/tests/components/marytts/test_tts.py b/tests/components/marytts/test_tts.py index 5692d72d388..65d4ab7e39c 100644 --- a/tests/components/marytts/test_tts.py +++ b/tests/components/marytts/test_tts.py @@ -3,15 +3,14 @@ import asyncio import os import shutil +from homeassistant.components.media_player.const import ( + DOMAIN as DOMAIN_MP, + SERVICE_PLAY_MEDIA, +) import homeassistant.components.tts as tts from homeassistant.setup import setup_component -from homeassistant.components.media_player.const import ( - SERVICE_PLAY_MEDIA, - DOMAIN as DOMAIN_MP, -) - -from tests.common import get_test_home_assistant, assert_setup_component, mock_service +from tests.common import assert_setup_component, get_test_home_assistant, mock_service from tests.components.tts.test_init import mutagen_mock # noqa: F401 diff --git a/tests/components/media_player/common.py b/tests/components/media_player/common.py index 1d1f811c91e..5be9a292829 100644 --- a/tests/components/media_player/common.py +++ b/tests/components/media_player/common.py @@ -18,6 +18,7 @@ from homeassistant.components.media_player.const import ( ) from homeassistant.const import ( ATTR_ENTITY_ID, + ENTITY_MATCH_ALL, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, @@ -37,42 +38,42 @@ from homeassistant.loader import bind_hass @bind_hass -def turn_on(hass, entity_id=None): +def turn_on(hass, entity_id=ENTITY_MATCH_ALL): """Turn on specified media player or all.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} hass.services.call(DOMAIN, SERVICE_TURN_ON, data) @bind_hass -def turn_off(hass, entity_id=None): +def turn_off(hass, entity_id=ENTITY_MATCH_ALL): """Turn off specified media player or all.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} hass.services.call(DOMAIN, SERVICE_TURN_OFF, data) @bind_hass -def toggle(hass, entity_id=None): +def toggle(hass, entity_id=ENTITY_MATCH_ALL): """Toggle specified media player or all.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} hass.services.call(DOMAIN, SERVICE_TOGGLE, data) @bind_hass -def volume_up(hass, entity_id=None): +def volume_up(hass, entity_id=ENTITY_MATCH_ALL): """Send the media player the command for volume up.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} hass.services.call(DOMAIN, SERVICE_VOLUME_UP, data) @bind_hass -def volume_down(hass, entity_id=None): +def volume_down(hass, entity_id=ENTITY_MATCH_ALL): """Send the media player the command for volume down.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} hass.services.call(DOMAIN, SERVICE_VOLUME_DOWN, data) @bind_hass -def mute_volume(hass, mute, entity_id=None): +def mute_volume(hass, mute, entity_id=ENTITY_MATCH_ALL): """Send the media player the command for muting the volume.""" data = {ATTR_MEDIA_VOLUME_MUTED: mute} @@ -83,7 +84,7 @@ def mute_volume(hass, mute, entity_id=None): @bind_hass -def set_volume_level(hass, volume, entity_id=None): +def set_volume_level(hass, volume, entity_id=ENTITY_MATCH_ALL): """Send the media player the command for setting the volume.""" data = {ATTR_MEDIA_VOLUME_LEVEL: volume} @@ -94,49 +95,49 @@ def set_volume_level(hass, volume, entity_id=None): @bind_hass -def media_play_pause(hass, entity_id=None): +def media_play_pause(hass, entity_id=ENTITY_MATCH_ALL): """Send the media player the command for play/pause.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} hass.services.call(DOMAIN, SERVICE_MEDIA_PLAY_PAUSE, data) @bind_hass -def media_play(hass, entity_id=None): +def media_play(hass, entity_id=ENTITY_MATCH_ALL): """Send the media player the command for play/pause.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} hass.services.call(DOMAIN, SERVICE_MEDIA_PLAY, data) @bind_hass -def media_pause(hass, entity_id=None): +def media_pause(hass, entity_id=ENTITY_MATCH_ALL): """Send the media player the command for pause.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} hass.services.call(DOMAIN, SERVICE_MEDIA_PAUSE, data) @bind_hass -def media_stop(hass, entity_id=None): +def media_stop(hass, entity_id=ENTITY_MATCH_ALL): """Send the media player the command for stop.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} hass.services.call(DOMAIN, SERVICE_MEDIA_STOP, data) @bind_hass -def media_next_track(hass, entity_id=None): +def media_next_track(hass, entity_id=ENTITY_MATCH_ALL): """Send the media player the command for next track.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} hass.services.call(DOMAIN, SERVICE_MEDIA_NEXT_TRACK, data) @bind_hass -def media_previous_track(hass, entity_id=None): +def media_previous_track(hass, entity_id=ENTITY_MATCH_ALL): """Send the media player the command for prev track.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} hass.services.call(DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK, data) @bind_hass -def media_seek(hass, position, entity_id=None): +def media_seek(hass, position, entity_id=ENTITY_MATCH_ALL): """Send the media player the command to seek in current playing media.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} data[ATTR_MEDIA_SEEK_POSITION] = position @@ -144,7 +145,7 @@ def media_seek(hass, position, entity_id=None): @bind_hass -def play_media(hass, media_type, media_id, entity_id=None, enqueue=None): +def play_media(hass, media_type, media_id, entity_id=ENTITY_MATCH_ALL, enqueue=None): """Send the media player the command for playing media.""" data = {ATTR_MEDIA_CONTENT_TYPE: media_type, ATTR_MEDIA_CONTENT_ID: media_id} @@ -158,7 +159,7 @@ def play_media(hass, media_type, media_id, entity_id=None, enqueue=None): @bind_hass -def select_source(hass, source, entity_id=None): +def select_source(hass, source, entity_id=ENTITY_MATCH_ALL): """Send the media player the command to select input source.""" data = {ATTR_INPUT_SOURCE: source} @@ -169,7 +170,7 @@ def select_source(hass, source, entity_id=None): @bind_hass -def clear_playlist(hass, entity_id=None): +def clear_playlist(hass, entity_id=ENTITY_MATCH_ALL): """Send the media player the command for clear playlist.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} hass.services.call(DOMAIN, SERVICE_CLEAR_PLAYLIST, data) diff --git a/tests/components/media_player/test_async_helpers.py b/tests/components/media_player/test_async_helpers.py index 4a2e4fed6c5..2e1ded3f084 100644 --- a/tests/components/media_player/test_async_helpers.py +++ b/tests/components/media_player/test_async_helpers.py @@ -1,14 +1,14 @@ """The tests for the Async Media player helper functions.""" -import unittest import asyncio +import unittest import homeassistant.components.media_player as mp from homeassistant.const import ( - STATE_PLAYING, - STATE_PAUSED, - STATE_ON, - STATE_OFF, STATE_IDLE, + STATE_OFF, + STATE_ON, + STATE_PAUSED, + STATE_PLAYING, ) from tests.common import get_test_home_assistant diff --git a/tests/components/media_player/test_device_condition.py b/tests/components/media_player/test_device_condition.py index 6216cc0e2b0..333cc4a2b13 100644 --- a/tests/components/media_player/test_device_condition.py +++ b/tests/components/media_player/test_device_condition.py @@ -1,25 +1,25 @@ """The tests for Media player device conditions.""" import pytest +import homeassistant.components.automation as automation from homeassistant.components.media_player import DOMAIN from homeassistant.const import ( - STATE_ON, - STATE_OFF, STATE_IDLE, + STATE_OFF, + STATE_ON, STATE_PAUSED, STATE_PLAYING, ) -from homeassistant.setup import async_setup_component -import homeassistant.components.automation as automation from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, assert_lists_same, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, - async_get_device_automations, ) diff --git a/tests/components/media_player/test_init.py b/tests/components/media_player/test_init.py index 7e36206a635..3db92cda42d 100644 --- a/tests/components/media_player/test_init.py +++ b/tests/components/media_player/test_init.py @@ -2,8 +2,8 @@ import base64 from unittest.mock import patch -from homeassistant.setup import async_setup_component from homeassistant.components.websocket_api.const import TYPE_RESULT +from homeassistant.setup import async_setup_component from tests.common import mock_coro diff --git a/tests/components/media_player/test_reproduce_state.py b/tests/components/media_player/test_reproduce_state.py index ddc5d6cf0ca..cbc5684f5d5 100644 --- a/tests/components/media_player/test_reproduce_state.py +++ b/tests/components/media_player/test_reproduce_state.py @@ -2,7 +2,6 @@ import pytest -from homeassistant.components.media_player.reproduce_state import async_reproduce_states from homeassistant.components.media_player.const import ( ATTR_INPUT_SOURCE, ATTR_MEDIA_CONTENT_ID, @@ -17,6 +16,7 @@ from homeassistant.components.media_player.const import ( SERVICE_SELECT_SOUND_MODE, SERVICE_SELECT_SOURCE, ) +from homeassistant.components.media_player.reproduce_state import async_reproduce_states from homeassistant.const import ( SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, diff --git a/tests/components/meraki/test_device_tracker.py b/tests/components/meraki/test_device_tracker.py index 9efd9037b67..360eb67a1ff 100644 --- a/tests/components/meraki/test_device_tracker.py +++ b/tests/components/meraki/test_device_tracker.py @@ -4,11 +4,14 @@ import json import pytest -from homeassistant.components.meraki.device_tracker import CONF_VALIDATOR, CONF_SECRET -from homeassistant.setup import async_setup_component import homeassistant.components.device_tracker as device_tracker +from homeassistant.components.meraki.device_tracker import ( + CONF_SECRET, + CONF_VALIDATOR, + URL, +) from homeassistant.const import CONF_PLATFORM -from homeassistant.components.meraki.device_tracker import URL +from homeassistant.setup import async_setup_component @pytest.fixture diff --git a/tests/components/met/test_config_flow.py b/tests/components/met/test_config_flow.py index 32f3be676e0..73c6c819817 100644 --- a/tests/components/met/test_config_flow.py +++ b/tests/components/met/test_config_flow.py @@ -1,10 +1,10 @@ """Tests for Met.no config flow.""" from unittest.mock import Mock, patch -from tests.common import MockConfigEntry, mock_coro - -from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE from homeassistant.components.met import config_flow +from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE + +from tests.common import MockConfigEntry, mock_coro async def test_show_config_form(): diff --git a/tests/components/mfi/test_sensor.py b/tests/components/mfi/test_sensor.py index da472308fc2..05f175fc191 100644 --- a/tests/components/mfi/test_sensor.py +++ b/tests/components/mfi/test_sensor.py @@ -2,12 +2,13 @@ import unittest import unittest.mock as mock +from mficlient.client import FailedToLogin import requests -from homeassistant.setup import setup_component -import homeassistant.components.sensor as sensor import homeassistant.components.mfi.sensor as mfi +import homeassistant.components.sensor as sensor from homeassistant.const import TEMP_CELSIUS +from homeassistant.setup import setup_component from tests.common import get_test_home_assistant @@ -38,28 +39,26 @@ class TestMfiSensorSetup(unittest.TestCase): """Stop everything that was started.""" self.hass.stop() - @mock.patch("mficlient.client.MFiClient") + @mock.patch("homeassistant.components.mfi.sensor.MFiClient") def test_setup_missing_config(self, mock_client): """Test setup with missing configuration.""" config = {"sensor": {"platform": "mfi"}} assert setup_component(self.hass, "sensor", config) assert not mock_client.called - @mock.patch("mficlient.client.MFiClient") + @mock.patch("homeassistant.components.mfi.sensor.MFiClient") def test_setup_failed_login(self, mock_client): """Test setup with login failure.""" - from mficlient.client import FailedToLogin - mock_client.side_effect = FailedToLogin assert not self.PLATFORM.setup_platform(self.hass, dict(self.GOOD_CONFIG), None) - @mock.patch("mficlient.client.MFiClient") + @mock.patch("homeassistant.components.mfi.sensor.MFiClient") def test_setup_failed_connect(self, mock_client): """Test setup with connection failure.""" mock_client.side_effect = requests.exceptions.ConnectionError assert not self.PLATFORM.setup_platform(self.hass, dict(self.GOOD_CONFIG), None) - @mock.patch("mficlient.client.MFiClient") + @mock.patch("homeassistant.components.mfi.sensor.MFiClient") def test_setup_minimum(self, mock_client): """Test setup with minimum configuration.""" config = dict(self.GOOD_CONFIG) @@ -70,7 +69,7 @@ class TestMfiSensorSetup(unittest.TestCase): "foo", "user", "pass", port=6443, use_tls=True, verify=True ) - @mock.patch("mficlient.client.MFiClient") + @mock.patch("homeassistant.components.mfi.sensor.MFiClient") def test_setup_with_port(self, mock_client): """Test setup with port.""" config = dict(self.GOOD_CONFIG) @@ -81,7 +80,7 @@ class TestMfiSensorSetup(unittest.TestCase): "foo", "user", "pass", port=6123, use_tls=True, verify=True ) - @mock.patch("mficlient.client.MFiClient") + @mock.patch("homeassistant.components.mfi.sensor.MFiClient") def test_setup_with_tls_disabled(self, mock_client): """Test setup without TLS.""" config = dict(self.GOOD_CONFIG) @@ -94,7 +93,7 @@ class TestMfiSensorSetup(unittest.TestCase): "foo", "user", "pass", port=6080, use_tls=False, verify=False ) - @mock.patch("mficlient.client.MFiClient") + @mock.patch("homeassistant.components.mfi.sensor.MFiClient") @mock.patch("homeassistant.components.mfi.sensor.MfiSensor") def test_setup_adds_proper_devices(self, mock_sensor, mock_client): """Test if setup adds devices.""" diff --git a/tests/components/mfi/test_switch.py b/tests/components/mfi/test_switch.py index 11a6c402ad6..42469b1b5ac 100644 --- a/tests/components/mfi/test_switch.py +++ b/tests/components/mfi/test_switch.py @@ -2,15 +2,14 @@ import unittest import unittest.mock as mock -from homeassistant.setup import setup_component -import homeassistant.components.switch as switch import homeassistant.components.mfi.switch as mfi -from tests.components.mfi import test_sensor as test_mfi_sensor +import homeassistant.components.switch as switch +from homeassistant.setup import setup_component from tests.common import get_test_home_assistant -class TestMfiSwitchSetup(test_mfi_sensor.TestMfiSensorSetup): +class TestMfiSwitchSetup(unittest.TestCase): """Test the mFi switch.""" PLATFORM = mfi @@ -28,7 +27,15 @@ class TestMfiSwitchSetup(test_mfi_sensor.TestMfiSensorSetup): } } - @mock.patch("mficlient.client.MFiClient") + def setup_method(self, method): + """Set up things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + def teardown_method(self, method): + """Stop everything that was started.""" + self.hass.stop() + + @mock.patch("homeassistant.components.mfi.switch.MFiClient") @mock.patch("homeassistant.components.mfi.switch.MfiSwitch") def test_setup_adds_proper_devices(self, mock_switch, mock_client): """Test if setup adds devices.""" diff --git a/tests/components/mhz19/test_sensor.py b/tests/components/mhz19/test_sensor.py index 06a1f146237..5eab93a30ff 100644 --- a/tests/components/mhz19/test_sensor.py +++ b/tests/components/mhz19/test_sensor.py @@ -1,12 +1,13 @@ """Tests for MH-Z19 sensor.""" import unittest -from unittest.mock import patch, DEFAULT, Mock +from unittest.mock import DEFAULT, Mock, patch -from homeassistant.setup import setup_component -from homeassistant.components.sensor import DOMAIN import homeassistant.components.mhz19.sensor as mhz19 +from homeassistant.components.sensor import DOMAIN from homeassistant.const import TEMP_FAHRENHEIT -from tests.common import get_test_home_assistant, assert_setup_component +from homeassistant.setup import setup_component + +from tests.common import assert_setup_component, get_test_home_assistant class TestMHZ19Sensor(unittest.TestCase): diff --git a/tests/components/microsoft_face/test_init.py b/tests/components/microsoft_face/test_init.py index 26a47778603..3e2cdf0d530 100644 --- a/tests/components/microsoft_face/test_init.py +++ b/tests/components/microsoft_face/test_init.py @@ -19,10 +19,10 @@ from homeassistant.const import ATTR_NAME from homeassistant.setup import setup_component from tests.common import ( - get_test_home_assistant, assert_setup_component, - mock_coro, + get_test_home_assistant, load_fixture, + mock_coro, ) @@ -96,7 +96,7 @@ class TestMicrosoftFaceSetup: self.hass.stop() @patch( - "homeassistant.components.microsoft_face." "MicrosoftFace.update_store", + "homeassistant.components.microsoft_face.MicrosoftFace.update_store", return_value=mock_coro(), ) def test_setup_component(self, mock_update): @@ -105,7 +105,7 @@ class TestMicrosoftFaceSetup: setup_component(self.hass, mf.DOMAIN, self.config) @patch( - "homeassistant.components.microsoft_face." "MicrosoftFace.update_store", + "homeassistant.components.microsoft_face.MicrosoftFace.update_store", return_value=mock_coro(), ) def test_setup_component_wrong_api_key(self, mock_update): @@ -114,7 +114,7 @@ class TestMicrosoftFaceSetup: setup_component(self.hass, mf.DOMAIN, {mf.DOMAIN: {}}) @patch( - "homeassistant.components.microsoft_face." "MicrosoftFace.update_store", + "homeassistant.components.microsoft_face.MicrosoftFace.update_store", return_value=mock_coro(), ) def test_setup_component_test_service(self, mock_update): @@ -170,7 +170,7 @@ class TestMicrosoftFaceSetup: ) @patch( - "homeassistant.components.microsoft_face." "MicrosoftFace.update_store", + "homeassistant.components.microsoft_face.MicrosoftFace.update_store", return_value=mock_coro(), ) def test_service_groups(self, mock_update, aioclient_mock): @@ -257,7 +257,7 @@ class TestMicrosoftFaceSetup: assert "Hans" not in entity_group1.attributes @patch( - "homeassistant.components.microsoft_face." "MicrosoftFace.update_store", + "homeassistant.components.microsoft_face.MicrosoftFace.update_store", return_value=mock_coro(), ) def test_service_train(self, mock_update, aioclient_mock): @@ -317,7 +317,7 @@ class TestMicrosoftFaceSetup: assert aioclient_mock.mock_calls[3][2] == b"Test" @patch( - "homeassistant.components.microsoft_face." "MicrosoftFace.update_store", + "homeassistant.components.microsoft_face.MicrosoftFace.update_store", return_value=mock_coro(), ) def test_service_status_400(self, mock_update, aioclient_mock): @@ -339,7 +339,7 @@ class TestMicrosoftFaceSetup: assert len(aioclient_mock.mock_calls) == 1 @patch( - "homeassistant.components.microsoft_face." "MicrosoftFace.update_store", + "homeassistant.components.microsoft_face.MicrosoftFace.update_store", return_value=mock_coro(), ) def test_service_status_timeout(self, mock_update, aioclient_mock): diff --git a/tests/components/microsoft_face_detect/test_image_processing.py b/tests/components/microsoft_face_detect/test_image_processing.py index b25b3453825..384e0ba130f 100644 --- a/tests/components/microsoft_face_detect/test_image_processing.py +++ b/tests/components/microsoft_face_detect/test_image_processing.py @@ -1,15 +1,15 @@ """The tests for the microsoft face detect platform.""" -from unittest.mock import patch, PropertyMock +from unittest.mock import PropertyMock, patch -from homeassistant.core import callback -from homeassistant.const import ATTR_ENTITY_PICTURE -from homeassistant.setup import setup_component import homeassistant.components.image_processing as ip import homeassistant.components.microsoft_face as mf +from homeassistant.const import ATTR_ENTITY_PICTURE +from homeassistant.core import callback +from homeassistant.setup import setup_component from tests.common import ( - get_test_home_assistant, assert_setup_component, + get_test_home_assistant, load_fixture, mock_coro, ) @@ -28,7 +28,7 @@ class TestMicrosoftFaceDetectSetup: self.hass.stop() @patch( - "homeassistant.components.microsoft_face." "MicrosoftFace.update_store", + "homeassistant.components.microsoft_face.MicrosoftFace.update_store", return_value=mock_coro(), ) def test_setup_platform(self, store_mock): @@ -49,7 +49,7 @@ class TestMicrosoftFaceDetectSetup: assert self.hass.states.get("image_processing.microsoftface_demo_camera") @patch( - "homeassistant.components.microsoft_face." "MicrosoftFace.update_store", + "homeassistant.components.microsoft_face.MicrosoftFace.update_store", return_value=mock_coro(), ) def test_setup_platform_name(self, store_mock): diff --git a/tests/components/microsoft_face_identify/test_image_processing.py b/tests/components/microsoft_face_identify/test_image_processing.py index 95f314dbc21..d1054cf8dc4 100644 --- a/tests/components/microsoft_face_identify/test_image_processing.py +++ b/tests/components/microsoft_face_identify/test_image_processing.py @@ -1,15 +1,15 @@ """The tests for the microsoft face identify platform.""" -from unittest.mock import patch, PropertyMock +from unittest.mock import PropertyMock, patch -from homeassistant.core import callback -from homeassistant.const import ATTR_ENTITY_PICTURE, STATE_UNKNOWN -from homeassistant.setup import setup_component import homeassistant.components.image_processing as ip import homeassistant.components.microsoft_face as mf +from homeassistant.const import ATTR_ENTITY_PICTURE, STATE_UNKNOWN +from homeassistant.core import callback +from homeassistant.setup import setup_component from tests.common import ( - get_test_home_assistant, assert_setup_component, + get_test_home_assistant, load_fixture, mock_coro, ) @@ -28,7 +28,7 @@ class TestMicrosoftFaceIdentifySetup: self.hass.stop() @patch( - "homeassistant.components.microsoft_face." "MicrosoftFace.update_store", + "homeassistant.components.microsoft_face.MicrosoftFace.update_store", return_value=mock_coro(), ) def test_setup_platform(self, store_mock): @@ -49,7 +49,7 @@ class TestMicrosoftFaceIdentifySetup: assert self.hass.states.get("image_processing.microsoftface_demo_camera") @patch( - "homeassistant.components.microsoft_face." "MicrosoftFace.update_store", + "homeassistant.components.microsoft_face.MicrosoftFace.update_store", return_value=mock_coro(), ) def test_setup_platform_name(self, store_mock): diff --git a/tests/components/min_max/test_sensor.py b/tests/components/min_max/test_sensor.py index 7fa0dee5aa3..fcc9a72cf7b 100644 --- a/tests/components/min_max/test_sensor.py +++ b/tests/components/min_max/test_sensor.py @@ -1,14 +1,15 @@ """The test for the min/max sensor platform.""" import unittest -from homeassistant.setup import setup_component from homeassistant.const import ( - STATE_UNKNOWN, - STATE_UNAVAILABLE, ATTR_UNIT_OF_MEASUREMENT, + STATE_UNAVAILABLE, + STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT, ) +from homeassistant.setup import setup_component + from tests.common import get_test_home_assistant diff --git a/tests/components/minio/test_minio.py b/tests/components/minio/test_minio.py index 836b456dc9b..e9b5759097c 100644 --- a/tests/components/minio/test_minio.py +++ b/tests/components/minio/test_minio.py @@ -3,19 +3,19 @@ import asyncio import json from unittest.mock import MagicMock +from asynctest import call, patch import pytest -from asynctest import patch, call from homeassistant.components.minio import ( - QueueListener, - DOMAIN, - CONF_HOST, - CONF_PORT, CONF_ACCESS_KEY, - CONF_SECRET_KEY, - CONF_SECURE, + CONF_HOST, CONF_LISTEN, CONF_LISTEN_BUCKET, + CONF_PORT, + CONF_SECRET_KEY, + CONF_SECURE, + DOMAIN, + QueueListener, ) from homeassistant.core import callback from homeassistant.setup import async_setup_component diff --git a/tests/components/mobile_app/conftest.py b/tests/components/mobile_app/conftest.py index 33af3c2b4a7..cd819a9891c 100644 --- a/tests/components/mobile_app/conftest.py +++ b/tests/components/mobile_app/conftest.py @@ -2,14 +2,13 @@ # pylint: disable=redefined-outer-name,unused-import import pytest -from tests.common import mock_device_registry - +from homeassistant.components.mobile_app.const import DOMAIN from homeassistant.setup import async_setup_component -from homeassistant.components.mobile_app.const import DOMAIN - from .const import REGISTER, REGISTER_CLEARTEXT +from tests.common import mock_device_registry + @pytest.fixture def registry(hass): @@ -18,7 +17,7 @@ def registry(hass): @pytest.fixture -async def create_registrations(authed_api_client): +async def create_registrations(hass, authed_api_client): """Return two new registrations.""" enc_reg = await authed_api_client.post( "/api/mobile_app/registrations", json=REGISTER @@ -34,6 +33,8 @@ async def create_registrations(authed_api_client): assert clear_reg.status == 201 clear_reg_json = await clear_reg.json() + await hass.async_block_till_done() + return (enc_reg_json, clear_reg_json) diff --git a/tests/components/mobile_app/test_entity.py b/tests/components/mobile_app/test_entity.py index 94a4e76ae84..0db9d42048f 100644 --- a/tests/components/mobile_app/test_entity.py +++ b/tests/components/mobile_app/test_entity.py @@ -2,6 +2,8 @@ import logging +from homeassistant.helpers import device_registry + _LOGGER = logging.getLogger(__name__) @@ -64,6 +66,9 @@ async def test_sensor(hass, create_registrations, webhook_client): updated_entity = hass.states.get("sensor.battery_state") assert updated_entity.state == "123" + dev_reg = await device_registry.async_get_registry(hass) + assert len(dev_reg.devices) == len(create_registrations) + 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_notify.py b/tests/components/mobile_app/test_notify.py index 83a2a5e0766..860f3d9f81f 100644 --- a/tests/components/mobile_app/test_notify.py +++ b/tests/components/mobile_app/test_notify.py @@ -2,9 +2,8 @@ # pylint: disable=redefined-outer-name import pytest -from homeassistant.setup import async_setup_component - from homeassistant.components.mobile_app.const import DOMAIN +from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py index 1ea2d50d8d8..6a41b5f054d 100644 --- a/tests/components/mobile_app/test_webhook.py +++ b/tests/components/mobile_app/test_webhook.py @@ -1,6 +1,7 @@ """Webhook tests for mobile_app.""" import logging + import pytest from homeassistant.components.mobile_app.const import CONF_SECRET @@ -9,10 +10,10 @@ from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.core import callback from homeassistant.setup import async_setup_component -from tests.common import async_mock_service - from .const import CALL_SERVICE, FIRE_EVENT, REGISTER_CLEARTEXT, RENDER_TEMPLATE, UPDATE +from tests.common import async_mock_service + _LOGGER = logging.getLogger(__name__) @@ -216,3 +217,23 @@ async def test_webhook_requires_encryption(webhook_client, create_registrations) assert "error" in webhook_json assert webhook_json["success"] is False assert webhook_json["error"]["code"] == "encryption_required" + + +async def test_webhook_update_location(hass, webhook_client, create_registrations): + """Test that encrypted registrations only accept encrypted data.""" + resp = await webhook_client.post( + "/api/webhook/{}".format(create_registrations[1]["webhook_id"]), + json={ + "type": "update_location", + "data": {"gps": [1, 2], "gps_accuracy": 10, "altitude": -10}, + }, + ) + + assert resp.status == 200 + + state = hass.states.get("device_tracker.test_1_2") + assert state is not None + assert state.attributes["latitude"] == 1.0 + assert state.attributes["longitude"] == 2.0 + assert state.attributes["gps_accuracy"] == 10 + assert state.attributes["altitude"] == -10 diff --git a/tests/components/mochad/test_light.py b/tests/components/mochad/test_light.py index 7cb4ecb3cbc..631c5b40734 100644 --- a/tests/components/mochad/test_light.py +++ b/tests/components/mochad/test_light.py @@ -14,8 +14,8 @@ from tests.common import get_test_home_assistant @pytest.fixture(autouse=True) def pymochad_mock(): """Mock pymochad.""" - with mock.patch.dict("sys.modules", {"pymochad": mock.MagicMock()}): - yield + with mock.patch("homeassistant.components.mochad.light.device") as device: + yield device class TestMochadSwitchSetup(unittest.TestCase): diff --git a/tests/components/mochad/test_switch.py b/tests/components/mochad/test_switch.py index 5fc3d4ee415..aa6ce354a32 100644 --- a/tests/components/mochad/test_switch.py +++ b/tests/components/mochad/test_switch.py @@ -4,9 +4,9 @@ import unittest.mock as mock import pytest -from homeassistant.setup import setup_component from homeassistant.components import switch from homeassistant.components.mochad import switch as mochad +from homeassistant.setup import setup_component from tests.common import get_test_home_assistant @@ -14,9 +14,8 @@ from tests.common import get_test_home_assistant @pytest.fixture(autouse=True) def pymochad_mock(): """Mock pymochad.""" - with mock.patch.dict( - "sys.modules", - {"pymochad": mock.MagicMock(), "pymochad.exceptions": mock.MagicMock()}, + with mock.patch("homeassistant.components.mochad.switch.device"), mock.patch( + "homeassistant.components.mochad.switch.MochadException" ): yield diff --git a/tests/components/modbus/test_modbus_sensor.py b/tests/components/modbus/test_modbus_sensor.py index 82d0b4bd5f0..9f13cba8907 100644 --- a/tests/components/modbus/test_modbus_sensor.py +++ b/tests/components/modbus/test_modbus_sensor.py @@ -1,14 +1,9 @@ """The tests for the Modbus sensor component.""" -import pytest from datetime import timedelta from unittest import mock -from homeassistant.const import ( - CONF_NAME, - CONF_OFFSET, - CONF_PLATFORM, - CONF_SCAN_INTERVAL, -) +import pytest + from homeassistant.components.modbus import DEFAULT_HUB, DOMAIN as MODBUS_DOMAIN from homeassistant.components.modbus.sensor import ( CONF_COUNT, @@ -26,9 +21,16 @@ from homeassistant.components.modbus.sensor import ( REGISTER_TYPE_INPUT, ) from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.const import ( + CONF_NAME, + CONF_OFFSET, + CONF_PLATFORM, + CONF_SCAN_INTERVAL, +) from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.common import MockModule, mock_integration, async_fire_time_changed + +from tests.common import MockModule, async_fire_time_changed, mock_integration @pytest.fixture() diff --git a/tests/components/mold_indicator/test_sensor.py b/tests/components/mold_indicator/test_sensor.py index d0a08dd25d4..ce0450d3304 100644 --- a/tests/components/mold_indicator/test_sensor.py +++ b/tests/components/mold_indicator/test_sensor.py @@ -1,13 +1,13 @@ """The tests for the MoldIndicator sensor.""" import unittest -from homeassistant.setup import setup_component -import homeassistant.components.sensor as sensor from homeassistant.components.mold_indicator.sensor import ( - ATTR_DEWPOINT, ATTR_CRITICAL_TEMP, + ATTR_DEWPOINT, ) +import homeassistant.components.sensor as sensor from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, TEMP_CELSIUS +from homeassistant.setup import setup_component from tests.common import get_test_home_assistant diff --git a/tests/components/monoprice/test_media_player.py b/tests/components/monoprice/test_media_player.py index 36110e6d909..e6747dfc4bf 100644 --- a/tests/components/monoprice/test_media_player.py +++ b/tests/components/monoprice/test_media_player.py @@ -1,29 +1,32 @@ """The tests for Monoprice Media player platform.""" +from collections import defaultdict import unittest from unittest import mock + +import pytest import voluptuous as vol -from collections import defaultdict from homeassistant.components.media_player.const import ( - DOMAIN, - SUPPORT_TURN_ON, + SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, - SUPPORT_SELECT_SOURCE, ) -from homeassistant.const import STATE_ON, STATE_OFF - -import tests.common +from homeassistant.components.monoprice.const import ( + DOMAIN, + SERVICE_RESTORE, + SERVICE_SNAPSHOT, +) from homeassistant.components.monoprice.media_player import ( DATA_MONOPRICE, PLATFORM_SCHEMA, - SERVICE_SNAPSHOT, - SERVICE_RESTORE, setup_platform, ) -import pytest +from homeassistant.const import STATE_OFF, STATE_ON + +import tests.common class AttrDict(dict): @@ -172,7 +175,10 @@ class TestMonopriceMediaPlayer(unittest.TestCase): self.hass = tests.common.get_test_home_assistant() self.hass.start() # Note, source dictionary is unsorted! - with mock.patch("pymonoprice.get_monoprice", new=lambda *a: self.monoprice): + with mock.patch( + "homeassistant.components.monoprice.media_player.get_monoprice", + new=lambda *a: self.monoprice, + ): setup_platform( self.hass, { diff --git a/tests/components/moon/test_sensor.py b/tests/components/moon/test_sensor.py index 95b1a1c305f..1e19d0a4d83 100644 --- a/tests/components/moon/test_sensor.py +++ b/tests/components/moon/test_sensor.py @@ -1,10 +1,10 @@ """The test for the moon sensor platform.""" -import unittest from datetime import datetime +import unittest from unittest.mock import patch -import homeassistant.util.dt as dt_util from homeassistant.setup import setup_component +import homeassistant.util.dt as dt_util from tests.common import get_test_home_assistant diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index 28f1a7e9720..3bfe32633b3 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -1,7 +1,6 @@ """The tests for the MQTT binary sensor platform.""" from datetime import datetime, timedelta import json - from unittest.mock import ANY, patch from homeassistant.components import binary_sensor, mqtt @@ -79,7 +78,7 @@ async def expires_helper(hass, mqtt_mock, caplog): """Run the basic expiry code.""" now = datetime(2017, 1, 1, 1, tzinfo=dt_util.UTC) - with patch(("homeassistant.helpers.event." "dt_util.utcnow"), return_value=now): + with patch(("homeassistant.helpers.event.dt_util.utcnow"), return_value=now): async_fire_time_changed(hass, now) async_fire_mqtt_message(hass, "test-topic", "ON") await hass.async_block_till_done() @@ -98,7 +97,7 @@ async def expires_helper(hass, mqtt_mock, caplog): assert state.state == STATE_ON # Next message resets timer - with patch(("homeassistant.helpers.event." "dt_util.utcnow"), return_value=now): + with patch(("homeassistant.helpers.event.dt_util.utcnow"), return_value=now): async_fire_time_changed(hass, now) async_fire_mqtt_message(hass, "test-topic", "OFF") await hass.async_block_till_done() diff --git a/tests/components/mqtt/test_camera.py b/tests/components/mqtt/test_camera.py index 70b5e941fe3..0e7d8ada759 100644 --- a/tests/components/mqtt/test_camera.py +++ b/tests/components/mqtt/test_camera.py @@ -1,6 +1,6 @@ """The tests for mqtt camera component.""" -from unittest.mock import ANY import json +from unittest.mock import ANY from homeassistant.components import camera, mqtt from homeassistant.components.mqtt.discovery import async_start diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index 3f4fc657186..29962287dd7 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -11,18 +11,19 @@ from homeassistant.components import mqtt from homeassistant.components.climate import DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP from homeassistant.components.climate.const import ( DOMAIN as CLIMATE_DOMAIN, - SUPPORT_AUX_HEAT, - SUPPORT_PRESET_MODE, - SUPPORT_FAN_MODE, - SUPPORT_SWING_MODE, - SUPPORT_TARGET_TEMPERATURE, HVAC_MODE_AUTO, HVAC_MODE_COOL, - HVAC_MODE_HEAT, HVAC_MODE_DRY, HVAC_MODE_FAN_ONLY, - SUPPORT_TARGET_TEMPERATURE_RANGE, + HVAC_MODE_HEAT, + PRESET_ECO, PRESET_NONE, + SUPPORT_AUX_HEAT, + SUPPORT_FAN_MODE, + SUPPORT_PRESET_MODE, + SUPPORT_SWING_MODE, + SUPPORT_TARGET_TEMPERATURE, + SUPPORT_TARGET_TEMPERATURE_RANGE, ) from homeassistant.components.mqtt.discovery import async_start from homeassistant.const import STATE_OFF, STATE_UNAVAILABLE @@ -112,7 +113,7 @@ async def test_set_operation_bad_attr_and_state(hass, mqtt_mock, caplog): assert state.state == "off" with pytest.raises(vol.Invalid) as excinfo: await common.async_set_hvac_mode(hass, None, ENTITY_CLIMATE) - assert ("value is not allowed for dictionary value @ " "data['hvac_mode']") in str( + assert ("value is not allowed for dictionary value @ data['hvac_mode']") in str( excinfo.value ) state = hass.states.get(ENTITY_CLIMATE) @@ -446,6 +447,19 @@ async def test_set_away_mode(hass, mqtt_mock): state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("preset_mode") is None + await common.async_set_preset_mode(hass, "hold-on", ENTITY_CLIMATE) + mqtt_mock.async_publish.reset_mock() + + await common.async_set_preset_mode(hass, "away", ENTITY_CLIMATE) + mqtt_mock.async_publish.assert_has_calls( + [ + unittest.mock.call("hold-topic", "off", 0, False), + unittest.mock.call("away-mode-topic", "AN", 0, False), + ] + ) + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("preset_mode") == "away" + async def test_set_hvac_action(hass, mqtt_mock): """Test setting of the HVAC action.""" @@ -495,6 +509,12 @@ async def test_set_hold(hass, mqtt_mock): state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("preset_mode") == "hold-on" + await common.async_set_preset_mode(hass, PRESET_ECO, ENTITY_CLIMATE) + mqtt_mock.async_publish.assert_called_once_with("hold-topic", "eco", 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("preset_mode") == PRESET_ECO + await common.async_set_preset_mode(hass, PRESET_NONE, ENTITY_CLIMATE) mqtt_mock.async_publish.assert_called_once_with("hold-topic", "off", 0, False) state = hass.states.get(ENTITY_CLIMATE) diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index bb734d2c03d..b15518961a4 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -16,11 +16,11 @@ from homeassistant.const import ( SERVICE_SET_COVER_POSITION, SERVICE_SET_COVER_TILT_POSITION, SERVICE_STOP_COVER, + SERVICE_TOGGLE, + SERVICE_TOGGLE_COVER_TILT, STATE_CLOSED, STATE_OPEN, STATE_UNAVAILABLE, - SERVICE_TOGGLE, - SERVICE_TOGGLE_COVER_TILT, STATE_UNKNOWN, ) from homeassistant.setup import async_setup_component diff --git a/tests/components/mqtt/test_device_tracker.py b/tests/components/mqtt/test_device_tracker.py index 71348fcf5cb..f4324bd8634 100644 --- a/tests/components/mqtt/test_device_tracker.py +++ b/tests/components/mqtt/test_device_tracker.py @@ -27,7 +27,7 @@ async def test_ensure_device_tracker_platform_validation(hass): assert "qos" in config with patch( - "homeassistant.components.mqtt.device_tracker." "async_setup_scanner", + "homeassistant.components.mqtt.device_tracker.async_setup_scanner", autospec=True, side_effect=mock_setup_scanner, ) as mock_sp: diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 860ef52a98a..6320be3b772 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -1,7 +1,6 @@ """The tests for the MQTT discovery.""" from pathlib import Path import re - from unittest.mock import patch from homeassistant.components import mqtt @@ -180,9 +179,7 @@ async def test_discovery_incl_nodeid(hass, mqtt_mock, caplog): await async_start(hass, "homeassistant", {}, entry) async_fire_mqtt_message( - hass, - "homeassistant/binary_sensor/my_node_id/bla" "/config", - '{ "name": "Beer" }', + hass, "homeassistant/binary_sensor/my_node_id/bla/config", '{ "name": "Beer" }', ) await hass.async_block_till_done() @@ -213,7 +210,7 @@ async def test_non_duplicate_discovery(hass, mqtt_mock, caplog): assert state is not None assert state.name == "Beer" assert state_duplicate is None - assert "Component has already been discovered: " "binary_sensor bla" in caplog.text + assert "Component has already been discovered: binary_sensor bla" in caplog.text async def test_discovery_expansion(hass, mqtt_mock, caplog): @@ -425,7 +422,7 @@ async def test_complex_discovery_topic_prefix(hass, mqtt_mock, caplog): async_fire_mqtt_message( hass, - ("my_home/homeassistant/register" "/binary_sensor/node1/object1/config"), + ("my_home/homeassistant/register/binary_sensor/node1/object1/config"), '{ "name": "Beer" }', ) await hass.async_block_till_done() diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 6f2a87d1caa..3d8e261fdb2 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -15,8 +15,8 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import callback -from homeassistant.setup import async_setup_component from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index b03cd4b8d73..43ccaf6dea5 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -555,7 +555,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): }, ) with patch( - "homeassistant.helpers.restore_state.RestoreEntity" ".async_get_last_state", + "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", return_value=mock_coro(fake_state), ): with assert_setup_component(1, light.DOMAIN): diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index f424b36ded6..355451f6469 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -117,7 +117,7 @@ class JsonValidator(object): """Helper to compare JSON.""" def __init__(self, jsondata): - """Constructor.""" + """Initialize JSON validator.""" self.jsondata = jsondata def __eq__(self, other): @@ -299,7 +299,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): ) with patch( - "homeassistant.helpers.restore_state.RestoreEntity" ".async_get_last_state", + "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", return_value=mock_coro(fake_state), ): assert await async_setup_component( diff --git a/tests/components/mqtt/test_light_template.py b/tests/components/mqtt/test_light_template.py index 5148f45e6e7..1d109af5930 100644 --- a/tests/components/mqtt/test_light_template.py +++ b/tests/components/mqtt/test_light_template.py @@ -261,7 +261,7 @@ async def test_optimistic(hass, mqtt_mock): ) with patch( - "homeassistant.helpers.restore_state.RestoreEntity" ".async_get_last_state", + "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", return_value=mock_coro(fake_state), ): with assert_setup_component(1, light.DOMAIN): diff --git a/tests/components/mqtt/test_lock.py b/tests/components/mqtt/test_lock.py index fbaedd3f945..9b89fa7159d 100644 --- a/tests/components/mqtt/test_lock.py +++ b/tests/components/mqtt/test_lock.py @@ -34,6 +34,8 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): "command_topic": "command-topic", "payload_lock": "LOCK", "payload_unlock": "UNLOCK", + "state_locked": "LOCKED", + "state_unlocked": "UNLOCKED", } }, ) @@ -42,12 +44,46 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): assert state.state is STATE_UNLOCKED assert not state.attributes.get(ATTR_ASSUMED_STATE) - async_fire_mqtt_message(hass, "state-topic", "LOCK") + async_fire_mqtt_message(hass, "state-topic", "LOCKED") state = hass.states.get("lock.test") assert state.state is STATE_LOCKED - async_fire_mqtt_message(hass, "state-topic", "UNLOCK") + async_fire_mqtt_message(hass, "state-topic", "UNLOCKED") + + state = hass.states.get("lock.test") + assert state.state is STATE_UNLOCKED + + +async def test_controlling_non_default_state_via_topic(hass, mqtt_mock): + """Test the controlling state via topic.""" + assert await async_setup_component( + hass, + lock.DOMAIN, + { + lock.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "payload_lock": "LOCK", + "payload_unlock": "UNLOCK", + "state_locked": "closed", + "state_unlocked": "open", + } + }, + ) + + state = hass.states.get("lock.test") + assert state.state is STATE_UNLOCKED + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "state-topic", "closed") + + state = hass.states.get("lock.test") + assert state.state is STATE_LOCKED + + async_fire_mqtt_message(hass, "state-topic", "open") state = hass.states.get("lock.test") assert state.state is STATE_UNLOCKED @@ -66,6 +102,8 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock): "command_topic": "command-topic", "payload_lock": "LOCK", "payload_unlock": "UNLOCK", + "state_locked": "LOCKED", + "state_unlocked": "UNLOCKED", "value_template": "{{ value_json.val }}", } }, @@ -74,12 +112,48 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock): state = hass.states.get("lock.test") assert state.state is STATE_UNLOCKED - async_fire_mqtt_message(hass, "state-topic", '{"val":"LOCK"}') + async_fire_mqtt_message(hass, "state-topic", '{"val":"LOCKED"}') state = hass.states.get("lock.test") assert state.state is STATE_LOCKED - async_fire_mqtt_message(hass, "state-topic", '{"val":"UNLOCK"}') + async_fire_mqtt_message(hass, "state-topic", '{"val":"UNLOCKED"}') + + state = hass.states.get("lock.test") + assert state.state is STATE_UNLOCKED + + +async def test_controlling_non_default_state_via_topic_and_json_message( + hass, mqtt_mock +): + """Test the controlling state via topic and JSON message.""" + assert await async_setup_component( + hass, + lock.DOMAIN, + { + lock.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "payload_lock": "LOCK", + "payload_unlock": "UNLOCK", + "state_locked": "closed", + "state_unlocked": "open", + "value_template": "{{ value_json.val }}", + } + }, + ) + + state = hass.states.get("lock.test") + assert state.state is STATE_UNLOCKED + + async_fire_mqtt_message(hass, "state-topic", '{"val":"closed"}') + + state = hass.states.get("lock.test") + assert state.state is STATE_LOCKED + + async_fire_mqtt_message(hass, "state-topic", '{"val":"open"}') state = hass.states.get("lock.test") assert state.state is STATE_UNLOCKED @@ -97,6 +171,8 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): "command_topic": "command-topic", "payload_lock": "LOCK", "payload_unlock": "UNLOCK", + "state_locked": "LOCKED", + "state_unlocked": "UNLOCKED", } }, ) @@ -135,6 +211,8 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock): "command_topic": "command-topic", "payload_lock": "LOCK", "payload_unlock": "UNLOCK", + "state_locked": "LOCKED", + "state_unlocked": "UNLOCKED", "optimistic": True, } }, @@ -206,6 +284,8 @@ async def test_custom_availability_payload(hass, mqtt_mock): "command_topic": "command-topic", "payload_lock": "LOCK", "payload_unlock": "UNLOCK", + "state_locked": "LOCKED", + "state_unlocked": "UNLOCKED", "availability_topic": "availability-topic", "payload_available": "good", "payload_not_available": "nogood", diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index cd55a08482d..66f8996bc2e 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -14,8 +14,8 @@ import homeassistant.util.dt as dt_util from tests.common import ( MockConfigEntry, async_fire_mqtt_message, - async_mock_mqtt_component, async_fire_time_changed, + async_mock_mqtt_component, mock_registry, ) @@ -63,7 +63,7 @@ async def test_setting_sensor_value_expires(hass, mqtt_mock, caplog): assert state.state == "unknown" now = datetime(2017, 1, 1, 1, tzinfo=dt_util.UTC) - with patch(("homeassistant.helpers.event." "dt_util.utcnow"), return_value=now): + with patch(("homeassistant.helpers.event.dt_util.utcnow"), return_value=now): async_fire_time_changed(hass, now) async_fire_mqtt_message(hass, "test-topic", "100") await hass.async_block_till_done() @@ -82,7 +82,7 @@ async def test_setting_sensor_value_expires(hass, mqtt_mock, caplog): assert state.state == "100" # Next message resets timer - with patch(("homeassistant.helpers.event." "dt_util.utcnow"), return_value=now): + with patch(("homeassistant.helpers.event.dt_util.utcnow"), return_value=now): async_fire_time_changed(hass, now) async_fire_mqtt_message(hass, "test-topic", "101") await hass.async_block_till_done() diff --git a/tests/components/mqtt/test_server.py b/tests/components/mqtt/test_server.py index 71dff7ef3ac..3627c95040e 100644 --- a/tests/components/mqtt/test_server.py +++ b/tests/components/mqtt/test_server.py @@ -19,13 +19,9 @@ class TestMQTT: """Stop everything that was started.""" self.hass.stop() - @patch( - "homeassistant.components.mqtt.server.custom_app_context", Mock(return_value="") - ) + @patch("passlib.apps.custom_app_context", Mock(return_value="")) @patch("tempfile.NamedTemporaryFile", Mock(return_value=MagicMock())) - @patch( - "homeassistant.components.mqtt.server.Broker", Mock(return_value=MagicMock()) - ) + @patch("hbmqtt.broker.Broker", Mock(return_value=MagicMock())) @patch("hbmqtt.broker.Broker.start", Mock(return_value=mock_coro())) @patch("homeassistant.components.mqtt.MQTT") def test_creating_config_with_pass_and_no_http_pass(self, mock_mqtt): @@ -45,13 +41,9 @@ class TestMQTT: assert mock_mqtt.mock_calls[1][2]["username"] == "homeassistant" assert mock_mqtt.mock_calls[1][2]["password"] == password - @patch( - "homeassistant.components.mqtt.server.custom_app_context", Mock(return_value="") - ) + @patch("passlib.apps.custom_app_context", Mock(return_value="")) @patch("tempfile.NamedTemporaryFile", Mock(return_value=MagicMock())) - @patch( - "homeassistant.components.mqtt.server.Broker", Mock(return_value=MagicMock()) - ) + @patch("hbmqtt.broker.Broker", Mock(return_value=MagicMock())) @patch("hbmqtt.broker.Broker.start", Mock(return_value=mock_coro())) @patch("homeassistant.components.mqtt.MQTT") def test_creating_config_with_pass_and_http_pass(self, mock_mqtt): diff --git a/tests/components/mqtt/test_state_vacuum.py b/tests/components/mqtt/test_state_vacuum.py index 572c3b05752..eb3071eb120 100644 --- a/tests/components/mqtt/test_state_vacuum.py +++ b/tests/components/mqtt/test_state_vacuum.py @@ -26,6 +26,7 @@ from homeassistant.components.vacuum import ( from homeassistant.const import ( CONF_NAME, CONF_PLATFORM, + ENTITY_MATCH_ALL, STATE_UNAVAILABLE, STATE_UNKNOWN, ) @@ -75,29 +76,41 @@ async def test_all_commands(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) - await hass.services.async_call(DOMAIN, SERVICE_START, blocking=True) + await hass.services.async_call( + DOMAIN, SERVICE_START, {"entity_id": ENTITY_MATCH_ALL}, blocking=True + ) mqtt_mock.async_publish.assert_called_once_with(COMMAND_TOPIC, "start", 0, False) mqtt_mock.async_publish.reset_mock() - await hass.services.async_call(DOMAIN, SERVICE_STOP, blocking=True) + await hass.services.async_call( + DOMAIN, SERVICE_STOP, {"entity_id": ENTITY_MATCH_ALL}, blocking=True + ) mqtt_mock.async_publish.assert_called_once_with(COMMAND_TOPIC, "stop", 0, False) mqtt_mock.async_publish.reset_mock() - await hass.services.async_call(DOMAIN, SERVICE_PAUSE, blocking=True) + await hass.services.async_call( + DOMAIN, SERVICE_PAUSE, {"entity_id": ENTITY_MATCH_ALL}, blocking=True + ) mqtt_mock.async_publish.assert_called_once_with(COMMAND_TOPIC, "pause", 0, False) mqtt_mock.async_publish.reset_mock() - await hass.services.async_call(DOMAIN, SERVICE_LOCATE, blocking=True) + await hass.services.async_call( + DOMAIN, SERVICE_LOCATE, {"entity_id": ENTITY_MATCH_ALL}, blocking=True + ) mqtt_mock.async_publish.assert_called_once_with(COMMAND_TOPIC, "locate", 0, False) mqtt_mock.async_publish.reset_mock() - await hass.services.async_call(DOMAIN, SERVICE_CLEAN_SPOT, blocking=True) + await hass.services.async_call( + DOMAIN, SERVICE_CLEAN_SPOT, {"entity_id": ENTITY_MATCH_ALL}, blocking=True + ) mqtt_mock.async_publish.assert_called_once_with( COMMAND_TOPIC, "clean_spot", 0, False ) mqtt_mock.async_publish.reset_mock() - await hass.services.async_call(DOMAIN, SERVICE_RETURN_TO_BASE, blocking=True) + await hass.services.async_call( + DOMAIN, SERVICE_RETURN_TO_BASE, {"entity_id": ENTITY_MATCH_ALL}, blocking=True + ) mqtt_mock.async_publish.assert_called_once_with( COMMAND_TOPIC, "return_to_base", 0, False ) @@ -134,27 +147,39 @@ async def test_commands_without_supported_features(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) - await hass.services.async_call(DOMAIN, SERVICE_START, blocking=True) + await hass.services.async_call( + DOMAIN, SERVICE_START, {"entity_id": ENTITY_MATCH_ALL}, blocking=True + ) mqtt_mock.async_publish.assert_not_called() mqtt_mock.async_publish.reset_mock() - await hass.services.async_call(DOMAIN, SERVICE_PAUSE, blocking=True) + await hass.services.async_call( + DOMAIN, SERVICE_PAUSE, {"entity_id": ENTITY_MATCH_ALL}, blocking=True + ) mqtt_mock.async_publish.assert_not_called() mqtt_mock.async_publish.reset_mock() - await hass.services.async_call(DOMAIN, SERVICE_STOP, blocking=True) + await hass.services.async_call( + DOMAIN, SERVICE_STOP, {"entity_id": ENTITY_MATCH_ALL}, blocking=True + ) mqtt_mock.async_publish.assert_not_called() mqtt_mock.async_publish.reset_mock() - await hass.services.async_call(DOMAIN, SERVICE_RETURN_TO_BASE, blocking=True) + await hass.services.async_call( + DOMAIN, SERVICE_RETURN_TO_BASE, {"entity_id": ENTITY_MATCH_ALL}, blocking=True + ) mqtt_mock.async_publish.assert_not_called() mqtt_mock.async_publish.reset_mock() - await hass.services.async_call(DOMAIN, SERVICE_LOCATE, blocking=True) + await hass.services.async_call( + DOMAIN, SERVICE_LOCATE, {"entity_id": ENTITY_MATCH_ALL}, blocking=True + ) mqtt_mock.async_publish.assert_not_called() mqtt_mock.async_publish.reset_mock() - await hass.services.async_call(DOMAIN, SERVICE_CLEAN_SPOT, blocking=True) + await hass.services.async_call( + DOMAIN, SERVICE_CLEAN_SPOT, {"entity_id": ENTITY_MATCH_ALL}, blocking=True + ) mqtt_mock.async_publish.assert_not_called() mqtt_mock.async_publish.reset_mock() diff --git a/tests/components/mqtt/test_switch.py b/tests/components/mqtt/test_switch.py index 35cbea5a82b..25fc3212f05 100644 --- a/tests/components/mqtt/test_switch.py +++ b/tests/components/mqtt/test_switch.py @@ -69,7 +69,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mock_publish): fake_state = ha.State("switch.test", "on") with patch( - "homeassistant.helpers.restore_state.RestoreEntity" ".async_get_last_state", + "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", return_value=mock_coro(fake_state), ): assert await async_setup_component( diff --git a/tests/components/mqtt_eventstream/test_init.py b/tests/components/mqtt_eventstream/test_init.py index b5ebe3c1ae1..f4062458d91 100644 --- a/tests/components/mqtt_eventstream/test_init.py +++ b/tests/components/mqtt_eventstream/test_init.py @@ -2,19 +2,19 @@ import json from unittest.mock import ANY, patch -from homeassistant.setup import setup_component import homeassistant.components.mqtt_eventstream as eventstream from homeassistant.const import EVENT_STATE_CHANGED from homeassistant.core import State, callback from homeassistant.helpers.json import JSONEncoder +from homeassistant.setup import setup_component import homeassistant.util.dt as dt_util from tests.common import ( + fire_mqtt_message, + fire_time_changed, get_test_home_assistant, mock_mqtt_component, - fire_mqtt_message, mock_state_change_event, - fire_time_changed, ) diff --git a/tests/components/mqtt_json/test_device_tracker.py b/tests/components/mqtt_json/test_device_tracker.py index 00be001840b..5af196c5bf2 100644 --- a/tests/components/mqtt_json/test_device_tracker.py +++ b/tests/components/mqtt_json/test_device_tracker.py @@ -2,18 +2,19 @@ import json import logging import os + from asynctest import patch import pytest -from homeassistant.setup import async_setup_component from homeassistant.components.device_tracker.legacy import ( - YAML_DEVICES, - ENTITY_ID_FORMAT, DOMAIN as DT_DOMAIN, + ENTITY_ID_FORMAT, + YAML_DEVICES, ) from homeassistant.const import CONF_PLATFORM +from homeassistant.setup import async_setup_component -from tests.common import async_mock_mqtt_component, async_fire_mqtt_message +from tests.common import async_fire_mqtt_message, async_mock_mqtt_component _LOGGER = logging.getLogger(__name__) @@ -45,7 +46,7 @@ async def test_ensure_device_tracker_platform_validation(hass): assert "qos" in config with patch( - "homeassistant.components.mqtt_json.device_tracker." "async_setup_scanner", + "homeassistant.components.mqtt_json.device_tracker.async_setup_scanner", autospec=True, side_effect=mock_setup_scanner, ) as mock_sp: diff --git a/tests/components/mqtt_room/test_sensor.py b/tests/components/mqtt_room/test_sensor.py index 07b8f89ef3f..e8a9e62403f 100644 --- a/tests/components/mqtt_room/test_sensor.py +++ b/tests/components/mqtt_room/test_sensor.py @@ -1,12 +1,12 @@ """The tests for the MQTT room presence sensor.""" -import json import datetime +import json from unittest.mock import patch -from homeassistant.setup import async_setup_component +from homeassistant.components.mqtt import CONF_QOS, CONF_STATE_TOPIC, DEFAULT_QOS import homeassistant.components.sensor as sensor -from homeassistant.components.mqtt import CONF_STATE_TOPIC, CONF_QOS, DEFAULT_QOS from homeassistant.const import CONF_NAME, CONF_PLATFORM +from homeassistant.setup import async_setup_component from homeassistant.util import dt from tests.common import async_fire_mqtt_message, async_mock_mqtt_component diff --git a/tests/components/mqtt_statestream/test_init.py b/tests/components/mqtt_statestream/test_init.py index 280b61a490c..ffab0e0846f 100644 --- a/tests/components/mqtt_statestream/test_init.py +++ b/tests/components/mqtt_statestream/test_init.py @@ -1,9 +1,9 @@ """The tests for the MQTT statestream component.""" from unittest.mock import ANY, call, patch -from homeassistant.setup import setup_component import homeassistant.components.mqtt_statestream as statestream from homeassistant.core import State +from homeassistant.setup import setup_component from tests.common import ( get_test_home_assistant, diff --git a/tests/components/mythicbeastsdns/test_init.py b/tests/components/mythicbeastsdns/test_init.py index c62b7241510..ee037a029ed 100644 --- a/tests/components/mythicbeastsdns/test_init.py +++ b/tests/components/mythicbeastsdns/test_init.py @@ -1,9 +1,10 @@ """Test the Mythic Beasts DNS component.""" import logging + import asynctest -from homeassistant.setup import async_setup_component from homeassistant.components import mythicbeastsdns +from homeassistant.setup import async_setup_component _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/namecheapdns/test_init.py b/tests/components/namecheapdns/test_init.py index e3ccb48b6da..1e771b11ea8 100644 --- a/tests/components/namecheapdns/test_init.py +++ b/tests/components/namecheapdns/test_init.py @@ -4,8 +4,8 @@ from datetime import timedelta import pytest -from homeassistant.setup import async_setup_component from homeassistant.components import namecheapdns +from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow from tests.common import async_fire_time_changed diff --git a/tests/components/neato/test_config_flow.py b/tests/components/neato/test_config_flow.py index 3f4bd90d0c1..59db79c1052 100644 --- a/tests/components/neato/test_config_flow.py +++ b/tests/components/neato/test_config_flow.py @@ -1,8 +1,8 @@ """Tests for the Neato config flow.""" -import pytest from unittest.mock import patch from pybotvac.exceptions import NeatoLoginException, NeatoRobotException +import pytest from homeassistant import data_entry_flow from homeassistant.components.neato import config_flow diff --git a/tests/components/neato/test_init.py b/tests/components/neato/test_init.py index 444cbe8cc5d..8fa6ad05945 100644 --- a/tests/components/neato/test_init.py +++ b/tests/components/neato/test_init.py @@ -1,8 +1,8 @@ """Tests for the Neato init file.""" -import pytest from unittest.mock import patch from pybotvac.exceptions import NeatoLoginException +import pytest from homeassistant.components.neato.const import CONF_VENDOR, NEATO_DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME diff --git a/tests/components/ness_alarm/test_init.py b/tests/components/ness_alarm/test_init.py index 3e5ccdd5b66..9da361852e9 100644 --- a/tests/components/ness_alarm/test_init.py +++ b/tests/components/ness_alarm/test_init.py @@ -1,37 +1,36 @@ """Tests for the ness_alarm component.""" from enum import Enum +from asynctest import MagicMock, patch import pytest -from asynctest import patch, MagicMock from homeassistant.components import alarm_control_panel from homeassistant.components.ness_alarm import ( - DOMAIN, - CONF_DEVICE_PORT, - CONF_ZONE_NAME, - CONF_ZONES, - CONF_ZONE_ID, - SERVICE_AUX, - SERVICE_PANIC, ATTR_CODE, ATTR_OUTPUT_ID, + CONF_DEVICE_PORT, + CONF_ZONE_ID, + CONF_ZONE_NAME, + CONF_ZONES, + DOMAIN, + SERVICE_AUX, + SERVICE_PANIC, ) from homeassistant.const import ( - STATE_ALARM_ARMING, - SERVICE_ALARM_DISARM, ATTR_ENTITY_ID, + CONF_HOST, SERVICE_ALARM_ARM_AWAY, SERVICE_ALARM_ARM_HOME, + SERVICE_ALARM_DISARM, SERVICE_ALARM_TRIGGER, - STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMING, + STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_UNKNOWN, - CONF_HOST, ) from homeassistant.setup import async_setup_component -from tests.common import MockDependency VALID_CONFIG = { DOMAIN: { @@ -261,7 +260,12 @@ def mock_nessclient(): _mock_factory = MagicMock() _mock_factory.return_value = _mock_instance - with MockDependency("nessclient"), patch( - "nessclient.Client", new=_mock_factory, create=True - ), patch("nessclient.ArmingState", new=MockArmingState): + with patch( + "homeassistant.components.ness_alarm.Client", new=_mock_factory, create=True + ), patch( + "homeassistant.components.ness_alarm.ArmingState", new=MockArmingState + ), patch( + "homeassistant.components.ness_alarm.alarm_control_panel.ArmingState", + new=MockArmingState, + ): yield _mock_instance diff --git a/tests/components/nest/test_config_flow.py b/tests/components/nest/test_config_flow.py index 4018a94b666..ec6218fb0d7 100644 --- a/tests/components/nest/test_config_flow.py +++ b/tests/components/nest/test_config_flow.py @@ -3,8 +3,8 @@ import asyncio from unittest.mock import Mock, patch from homeassistant import data_entry_flow +from homeassistant.components.nest import DOMAIN, config_flow from homeassistant.setup import async_setup_component -from homeassistant.components.nest import config_flow, DOMAIN from tests.common import mock_coro diff --git a/tests/components/nest/test_local_auth.py b/tests/components/nest/test_local_auth.py index bf3c3e4230a..491b9bd9e07 100644 --- a/tests/components/nest/test_local_auth.py +++ b/tests/components/nest/test_local_auth.py @@ -1,11 +1,11 @@ """Test Nest local auth.""" -from homeassistant.components.nest import const, config_flow, local_auth from urllib.parse import parse_qsl import pytest - import requests_mock as rmock +from homeassistant.components.nest import config_flow, const, local_auth + @pytest.fixture def registered_flow(hass): diff --git a/tests/components/nextbus/test_sensor.py b/tests/components/nextbus/test_sensor.py index d7c1919dff0..bee9db445e2 100644 --- a/tests/components/nextbus/test_sensor.py +++ b/tests/components/nextbus/test_sensor.py @@ -1,13 +1,13 @@ """The tests for the nexbus sensor component.""" from copy import deepcopy +from unittest.mock import patch import pytest -import homeassistant.components.sensor as sensor import homeassistant.components.nextbus.sensor as nextbus +import homeassistant.components.sensor as sensor -from tests.common import assert_setup_component, async_setup_component, MockDependency - +from tests.common import assert_setup_component, async_setup_component VALID_AGENCY = "sf-muni" VALID_ROUTE = "F" @@ -54,14 +54,16 @@ async def assert_setup_sensor(hass, config, count=1): @pytest.fixture def mock_nextbus(): """Create a mock py_nextbus module.""" - with MockDependency("py_nextbus") as py_nextbus: - yield py_nextbus + with patch( + "homeassistant.components.nextbus.sensor.NextBusClient" + ) as NextBusClient: + yield NextBusClient @pytest.fixture def mock_nextbus_predictions(mock_nextbus): """Create a mock of NextBusClient predictions.""" - instance = mock_nextbus.NextBusClient.return_value + instance = mock_nextbus.return_value instance.get_predictions_for_multi_stops.return_value = BASIC_RESULTS yield instance.get_predictions_for_multi_stops @@ -70,7 +72,7 @@ def mock_nextbus_predictions(mock_nextbus): @pytest.fixture def mock_nextbus_lists(mock_nextbus): """Mock all list functions in nextbus to test validate logic.""" - instance = mock_nextbus.NextBusClient.return_value + instance = mock_nextbus.return_value instance.get_agency_list.return_value = { "agency": [{"tag": "sf-muni", "title": "San Francisco Muni"}] } @@ -94,17 +96,18 @@ async def test_invalid_config(hass, mock_nextbus, mock_nextbus_lists): async def test_validate_tags(hass, mock_nextbus, mock_nextbus_lists): """Test that additional validation against the API is successful.""" - client = mock_nextbus.NextBusClient() # with self.subTest('Valid everything'): - assert nextbus.validate_tags(client, VALID_AGENCY, VALID_ROUTE, VALID_STOP) + assert nextbus.validate_tags(mock_nextbus(), VALID_AGENCY, VALID_ROUTE, VALID_STOP) # with self.subTest('Invalid agency'): - assert not nextbus.validate_tags(client, "not-valid", VALID_ROUTE, VALID_STOP) + assert not nextbus.validate_tags( + mock_nextbus(), "not-valid", VALID_ROUTE, VALID_STOP + ) # with self.subTest('Invalid route'): - assert not nextbus.validate_tags(client, VALID_AGENCY, "0", VALID_STOP) + assert not nextbus.validate_tags(mock_nextbus(), VALID_AGENCY, "0", VALID_STOP) # with self.subTest('Invalid stop'): - assert not nextbus.validate_tags(client, VALID_AGENCY, VALID_ROUTE, 0) + assert not nextbus.validate_tags(mock_nextbus(), VALID_AGENCY, VALID_ROUTE, 0) async def test_verify_valid_state( @@ -203,7 +206,7 @@ async def test_direction_list( }, { "title": "Outbound 2", - "prediction": {"minutes": "4", "epochTime": "1553807374000"}, + "prediction": {"minutes": "0", "epochTime": "1553807374000"}, }, ], } @@ -218,7 +221,7 @@ async def test_direction_list( assert state.attributes["route"] == VALID_ROUTE_TITLE assert state.attributes["stop"] == VALID_STOP_TITLE assert state.attributes["direction"] == "Outbound, Outbound 2" - assert state.attributes["upcoming"] == "1, 2, 3, 4" + assert state.attributes["upcoming"] == "0, 1, 2, 3" async def test_custom_name( diff --git a/tests/components/no_ip/test_init.py b/tests/components/no_ip/test_init.py index 07b3eabae10..c9b8d05906e 100644 --- a/tests/components/no_ip/test_init.py +++ b/tests/components/no_ip/test_init.py @@ -1,11 +1,10 @@ """Test the NO-IP component.""" -import asyncio from datetime import timedelta import pytest -from homeassistant.setup import async_setup_component from homeassistant.components import no_ip +from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow from tests.common import async_fire_time_changed @@ -39,12 +38,11 @@ def setup_no_ip(hass, aioclient_mock): ) -@asyncio.coroutine -def test_setup(hass, aioclient_mock): +async def test_setup(hass, aioclient_mock): """Test setup works if update passes.""" aioclient_mock.get(UPDATE_URL, params={"hostname": DOMAIN}, text="nochg 0.0.0.0") - result = yield from async_setup_component( + result = await async_setup_component( hass, no_ip.DOMAIN, {no_ip.DOMAIN: {"domain": DOMAIN, "username": USERNAME, "password": PASSWORD}}, @@ -53,16 +51,15 @@ def test_setup(hass, aioclient_mock): assert aioclient_mock.call_count == 1 async_fire_time_changed(hass, utcnow() + timedelta(minutes=5)) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert aioclient_mock.call_count == 2 -@asyncio.coroutine -def test_setup_fails_if_update_fails(hass, aioclient_mock): +async def test_setup_fails_if_update_fails(hass, aioclient_mock): """Test setup fails if first update fails.""" aioclient_mock.get(UPDATE_URL, params={"hostname": DOMAIN}, text="nohost") - result = yield from async_setup_component( + result = await async_setup_component( hass, no_ip.DOMAIN, {no_ip.DOMAIN: {"domain": DOMAIN, "username": USERNAME, "password": PASSWORD}}, @@ -71,12 +68,11 @@ def test_setup_fails_if_update_fails(hass, aioclient_mock): assert aioclient_mock.call_count == 1 -@asyncio.coroutine -def test_setup_fails_if_wrong_auth(hass, aioclient_mock): +async def test_setup_fails_if_wrong_auth(hass, aioclient_mock): """Test setup fails if first update fails through wrong authentication.""" aioclient_mock.get(UPDATE_URL, params={"hostname": DOMAIN}, text="badauth") - result = yield from async_setup_component( + result = await async_setup_component( hass, no_ip.DOMAIN, {no_ip.DOMAIN: {"domain": DOMAIN, "username": USERNAME, "password": PASSWORD}}, diff --git a/tests/components/notion/test_config_flow.py b/tests/components/notion/test_config_flow.py index 42b28d2c0e1..f7651a570cf 100644 --- a/tests/components/notion/test_config_flow.py +++ b/tests/components/notion/test_config_flow.py @@ -1,4 +1,6 @@ """Define tests for the Notion config flow.""" +from unittest.mock import patch + import aionotion import pytest @@ -6,7 +8,7 @@ from homeassistant import data_entry_flow from homeassistant.components.notion import DOMAIN, config_flow from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from tests.common import MockConfigEntry, MockDependency, mock_coro +from tests.common import MockConfigEntry, mock_coro @pytest.fixture @@ -18,8 +20,8 @@ def mock_client_coro(): @pytest.fixture def mock_aionotion(mock_client_coro): """Mock the aionotion library.""" - with MockDependency("aionotion") as mock_: - mock_.async_get_client.return_value = mock_client_coro + with patch("homeassistant.components.notion.config_flow.async_get_client") as mock_: + mock_.return_value = mock_client_coro yield mock_ diff --git a/tests/components/nsw_fuel_station/test_sensor.py b/tests/components/nsw_fuel_station/test_sensor.py index c65c1fe5091..babdd0cf1c3 100644 --- a/tests/components/nsw_fuel_station/test_sensor.py +++ b/tests/components/nsw_fuel_station/test_sensor.py @@ -4,7 +4,8 @@ from unittest.mock import patch from homeassistant.components import sensor from homeassistant.setup import setup_component -from tests.common import get_test_home_assistant, assert_setup_component, MockDependency + +from tests.common import assert_setup_component, get_test_home_assistant VALID_CONFIG = { "platform": "nsw_fuel_station", @@ -83,9 +84,11 @@ class TestNSWFuelStation(unittest.TestCase): """Stop everything that was started.""" self.hass.stop() - @MockDependency("nsw_fuel") - @patch("nsw_fuel.FuelCheckClient", new=FuelCheckClientMock) - def test_setup(self, mock_nsw_fuel): + @patch( + "homeassistant.components.nsw_fuel_station.sensor.FuelCheckClient", + new=FuelCheckClientMock, + ) + def test_setup(self): """Test the setup with custom settings.""" with assert_setup_component(1, sensor.DOMAIN): assert setup_component(self.hass, sensor.DOMAIN, {"sensor": VALID_CONFIG}) @@ -96,9 +99,11 @@ class TestNSWFuelStation(unittest.TestCase): state = self.hass.states.get("sensor.{}".format(entity_id)) assert state is not None - @MockDependency("nsw_fuel") - @patch("nsw_fuel.FuelCheckClient", new=FuelCheckClientMock) - def test_sensor_values(self, mock_nsw_fuel): + @patch( + "homeassistant.components.nsw_fuel_station.sensor.FuelCheckClient", + new=FuelCheckClientMock, + ) + def test_sensor_values(self): """Test retrieval of sensor values.""" assert setup_component(self.hass, sensor.DOMAIN, {"sensor": VALID_CONFIG}) diff --git a/tests/components/nsw_rural_fire_service_feed/test_geo_location.py b/tests/components/nsw_rural_fire_service_feed/test_geo_location.py index f5f88087010..1915706ca1a 100644 --- a/tests/components/nsw_rural_fire_service_feed/test_geo_location.py +++ b/tests/components/nsw_rural_fire_service_feed/test_geo_location.py @@ -1,25 +1,29 @@ -"""The tests for the geojson platform.""" +"""The tests for the NSW Rural Fire Service Feeds platform.""" import datetime -from asynctest.mock import patch, MagicMock, call +from unittest.mock import ANY + +from aio_geojson_nsw_rfs_incidents import NswRuralFireServiceIncidentsFeed +from asynctest.mock import MagicMock, call, patch from homeassistant.components import geo_location from homeassistant.components.geo_location import ATTR_SOURCE from homeassistant.components.nsw_rural_fire_service_feed.geo_location import ( - ATTR_EXTERNAL_ID, - SCAN_INTERVAL, ATTR_CATEGORY, + ATTR_COUNCIL_AREA, + ATTR_EXTERNAL_ID, ATTR_FIRE, ATTR_LOCATION, - ATTR_COUNCIL_AREA, + ATTR_PUBLICATION_DATE, + ATTR_RESPONSIBLE_AGENCY, + ATTR_SIZE, ATTR_STATUS, ATTR_TYPE, - ATTR_SIZE, - ATTR_RESPONSIBLE_AGENCY, - ATTR_PUBLICATION_DATE, + SCAN_INTERVAL, ) from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_FRIENDLY_NAME, + ATTR_ICON, ATTR_LATITUDE, ATTR_LONGITUDE, ATTR_UNIT_OF_MEASUREMENT, @@ -27,12 +31,13 @@ from homeassistant.const import ( CONF_LONGITUDE, CONF_RADIUS, EVENT_HOMEASSISTANT_START, - ATTR_ICON, + EVENT_HOMEASSISTANT_STOP, ) from homeassistant.setup import async_setup_component -from tests.common import assert_setup_component, async_fire_time_changed import homeassistant.util.dt as dt_util +from tests.common import assert_setup_component, async_fire_time_changed + CONFIG = { geo_location.DOMAIN: [{"platform": "nsw_rural_fire_service_feed", CONF_RADIUS: 200}] } @@ -110,12 +115,12 @@ async def test_setup(hass): mock_entry_3 = _generate_mock_feed_entry("3456", "Title 3", 25.5, (-31.2, 150.2)) mock_entry_4 = _generate_mock_feed_entry("4567", "Title 4", 12.5, (-31.3, 150.3)) - utcnow = dt_util.utcnow() # Patching 'utcnow' to gain more control over the timed update. + utcnow = dt_util.utcnow() with patch("homeassistant.util.dt.utcnow", return_value=utcnow), patch( - "geojson_client.nsw_rural_fire_service_feed." "NswRuralFireServiceFeed" - ) as mock_feed: - mock_feed.return_value.update.return_value = ( + "aio_geojson_client.feed.GeoJsonFeed.update" + ) as mock_feed_update: + mock_feed_update.return_value = ( "OK", [mock_entry_1, mock_entry_2, mock_entry_3], ) @@ -187,7 +192,7 @@ async def test_setup(hass): # Simulate an update - one existing, one new entry, # one outdated entry - mock_feed.return_value.update.return_value = ( + mock_feed_update.return_value = ( "OK", [mock_entry_1, mock_entry_4, mock_entry_3], ) @@ -199,7 +204,7 @@ async def test_setup(hass): # Simulate an update - empty data, but successful update, # so no changes to entities. - mock_feed.return_value.update.return_value = "OK_NO_DATA", None + mock_feed_update.return_value = "OK_NO_DATA", None async_fire_time_changed(hass, utcnow + 2 * SCAN_INTERVAL) await hass.async_block_till_done() @@ -207,13 +212,18 @@ async def test_setup(hass): assert len(all_states) == 3 # Simulate an update - empty data, removes all entities - mock_feed.return_value.update.return_value = "ERROR", None + mock_feed_update.return_value = "ERROR", None 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) == 0 + # Artificially trigger update. + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + # Collect events. + await hass.async_block_till_done() + async def test_setup_with_custom_location(hass): """Test the setup with a custom location.""" @@ -221,9 +231,12 @@ async def test_setup_with_custom_location(hass): mock_entry_1 = _generate_mock_feed_entry("1234", "Title 1", 20.5, (-31.1, 150.1)) with patch( - "geojson_client.nsw_rural_fire_service_feed." "NswRuralFireServiceFeed" - ) as mock_feed: - mock_feed.return_value.update.return_value = "OK", [mock_entry_1] + "aio_geojson_nsw_rfs_incidents.feed_manager.NswRuralFireServiceIncidentsFeed", + wraps=NswRuralFireServiceIncidentsFeed, + ) as mock_feed_manager, patch( + "aio_geojson_client.feed.GeoJsonFeed.update" + ) as mock_feed_update: + mock_feed_update.return_value = "OK", [mock_entry_1] with assert_setup_component(1, geo_location.DOMAIN): assert await async_setup_component( @@ -238,6 +251,6 @@ async def test_setup_with_custom_location(hass): all_states = hass.states.async_all() assert len(all_states) == 1 - assert mock_feed.call_args == call( - (15.1, 25.2), filter_categories=[], filter_radius=200.0 + assert mock_feed_manager.call_args == call( + ANY, (15.1, 25.2), filter_categories=[], filter_radius=200.0 ) diff --git a/tests/components/nuheat/test_climate.py b/tests/components/nuheat/test_climate.py index 1ab791dfd37..c35497968ac 100644 --- a/tests/components/nuheat/test_climate.py +++ b/tests/components/nuheat/test_climate.py @@ -67,7 +67,7 @@ class TestNuHeat(unittest.TestCase): thermostat = mocked_thermostat(self.api, "12345", "F") thermostats = [thermostat] - self.hass.data[nuheat.NUHEAT_DOMAIN] = (self.api, ["12345"]) + self.hass.data[nuheat.DOMAIN] = (self.api, ["12345"]) config = {} add_entities = Mock() @@ -85,12 +85,12 @@ class TestNuHeat(unittest.TestCase): thermostat.schedule_update_ha_state = Mock() thermostat.entity_id = "climate.master_bathroom" - self.hass.data[nuheat.NUHEAT_DOMAIN] = (self.api, ["12345"]) + self.hass.data[nuheat.DOMAIN] = (self.api, ["12345"]) nuheat.setup_platform(self.hass, {}, Mock(), {}) # Explicit entity self.hass.services.call( - nuheat.NUHEAT_DOMAIN, + nuheat.DOMAIN, nuheat.SERVICE_RESUME_PROGRAM, {"entity_id": "climate.master_bathroom"}, True, @@ -103,9 +103,7 @@ class TestNuHeat(unittest.TestCase): thermostat.schedule_update_ha_state.reset_mock() # All entities - self.hass.services.call( - nuheat.NUHEAT_DOMAIN, nuheat.SERVICE_RESUME_PROGRAM, {}, True - ) + self.hass.services.call(nuheat.DOMAIN, nuheat.SERVICE_RESUME_PROGRAM, {}, True) thermostat.resume_program.assert_called_with() thermostat.schedule_update_ha_state.assert_called_with(True) diff --git a/tests/components/nws/test_weather.py b/tests/components/nws/test_weather.py index 0e450f06238..adda88a789d 100644 --- a/tests/components/nws/test_weather.py +++ b/tests/components/nws/test_weather.py @@ -1,13 +1,5 @@ """Tests for the NWS weather component.""" from homeassistant.components.nws.weather import ATTR_FORECAST_PRECIP_PROB -from homeassistant.components.weather import ( - ATTR_WEATHER_HUMIDITY, - ATTR_WEATHER_PRESSURE, - ATTR_WEATHER_TEMPERATURE, - ATTR_WEATHER_VISIBILITY, - ATTR_WEATHER_WIND_BEARING, - ATTR_WEATHER_WIND_SPEED, -) from homeassistant.components.weather import ( ATTR_FORECAST, ATTR_FORECAST_CONDITION, @@ -15,25 +7,30 @@ from homeassistant.components.weather import ( ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, ATTR_FORECAST_WIND_SPEED, + ATTR_WEATHER_HUMIDITY, + ATTR_WEATHER_PRESSURE, + ATTR_WEATHER_TEMPERATURE, + ATTR_WEATHER_VISIBILITY, + ATTR_WEATHER_WIND_BEARING, + ATTR_WEATHER_WIND_SPEED, ) - from homeassistant.const import ( LENGTH_KILOMETERS, LENGTH_METERS, LENGTH_MILES, + PRESSURE_HPA, PRESSURE_INHG, PRESSURE_PA, - PRESSURE_HPA, TEMP_CELSIUS, TEMP_FAHRENHEIT, ) -from homeassistant.util.pressure import convert as convert_pressure -from homeassistant.util.distance import convert as convert_distance -from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM -from homeassistant.util.temperature import convert as convert_temperature from homeassistant.setup import async_setup_component +from homeassistant.util.distance import convert as convert_distance +from homeassistant.util.pressure import convert as convert_pressure +from homeassistant.util.temperature import convert as convert_temperature +from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM -from tests.common import load_fixture, assert_setup_component +from tests.common import assert_setup_component, load_fixture EXP_OBS_IMP = { ATTR_WEATHER_TEMPERATURE: round( diff --git a/tests/components/nx584/test_binary_sensor.py b/tests/components/nx584/test_binary_sensor.py index 3736d97bbcd..24823e9046a 100644 --- a/tests/components/nx584/test_binary_sensor.py +++ b/tests/components/nx584/test_binary_sensor.py @@ -1,15 +1,15 @@ """The tests for the nx584 sensor platform.""" -import requests import unittest from unittest import mock from nx584 import client as nx584_client +import pytest +import requests from homeassistant.components.nx584 import binary_sensor as nx584 from homeassistant.setup import setup_component from tests.common import get_test_home_assistant -import pytest class StopMe(Exception): diff --git a/tests/components/onboarding/test_init.py b/tests/components/onboarding/test_init.py index a7fc1ad43f4..e347a07e73f 100644 --- a/tests/components/onboarding/test_init.py +++ b/tests/components/onboarding/test_init.py @@ -1,13 +1,13 @@ """Tests for the init.""" -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch -from homeassistant.setup import async_setup_component from homeassistant.components import onboarding - -from tests.common import mock_coro, MockUser +from homeassistant.setup import async_setup_component from . import mock_storage +from tests.common import MockUser, mock_coro + # Temporarily: if auth not active, always set onboarded=True diff --git a/tests/components/onboarding/test_views.py b/tests/components/onboarding/test_views.py index 7881b75ee99..6d2c6e4c08f 100644 --- a/tests/components/onboarding/test_views.py +++ b/tests/components/onboarding/test_views.py @@ -4,15 +4,15 @@ from unittest.mock import patch import pytest -from homeassistant.setup import async_setup_component from homeassistant.components import onboarding from homeassistant.components.onboarding import const, views +from homeassistant.setup import async_setup_component + +from . import mock_storage from tests.common import CLIENT_ID, register_auth_provider from tests.components.met.conftest import mock_weather # noqa: F401 -from . import mock_storage - @pytest.fixture(autouse=True) def always_mock_weather(mock_weather): # noqa: F811 diff --git a/tests/components/openalpr_cloud/test_image_processing.py b/tests/components/openalpr_cloud/test_image_processing.py index e559b6adc45..4aec9e68709 100644 --- a/tests/components/openalpr_cloud/test_image_processing.py +++ b/tests/components/openalpr_cloud/test_image_processing.py @@ -1,15 +1,15 @@ """The tests for the openalpr cloud platform.""" import asyncio -from unittest.mock import patch, PropertyMock +from unittest.mock import PropertyMock, patch -from homeassistant.core import callback -from homeassistant.setup import setup_component from homeassistant.components import camera, image_processing as ip from homeassistant.components.openalpr_cloud.image_processing import OPENALPR_API_URL +from homeassistant.core import callback +from homeassistant.setup import setup_component from tests.common import ( - get_test_home_assistant, assert_setup_component, + get_test_home_assistant, load_fixture, mock_coro, ) diff --git a/tests/components/openalpr_local/test_image_processing.py b/tests/components/openalpr_local/test_image_processing.py index bc29c227b0c..4c34abca1d4 100644 --- a/tests/components/openalpr_local/test_image_processing.py +++ b/tests/components/openalpr_local/test_image_processing.py @@ -1,13 +1,13 @@ """The tests for the openalpr local platform.""" import asyncio -from unittest.mock import patch, PropertyMock, MagicMock +from unittest.mock import MagicMock, PropertyMock, patch -from homeassistant.core import callback -from homeassistant.const import ATTR_ENTITY_PICTURE -from homeassistant.setup import setup_component import homeassistant.components.image_processing as ip +from homeassistant.const import ATTR_ENTITY_PICTURE +from homeassistant.core import callback +from homeassistant.setup import setup_component -from tests.common import get_test_home_assistant, assert_setup_component, load_fixture +from tests.common import assert_setup_component, get_test_home_assistant, load_fixture from tests.components.image_processing import common diff --git a/tests/components/openhardwaremonitor/test_sensor.py b/tests/components/openhardwaremonitor/test_sensor.py index 909f9ab2732..3fb93cb1375 100644 --- a/tests/components/openhardwaremonitor/test_sensor.py +++ b/tests/components/openhardwaremonitor/test_sensor.py @@ -1,8 +1,11 @@ """The tests for the Open Hardware Monitor platform.""" import unittest + import requests_mock + from homeassistant.setup import setup_component -from tests.common import load_fixture, get_test_home_assistant + +from tests.common import get_test_home_assistant, load_fixture class TestOpenHardwareMonitorSetup(unittest.TestCase): diff --git a/tests/components/opentherm_gw/test_config_flow.py b/tests/components/opentherm_gw/test_config_flow.py index 89f2783cf71..26048543a22 100644 --- a/tests/components/opentherm_gw/test_config_flow.py +++ b/tests/components/opentherm_gw/test_config_flow.py @@ -1,18 +1,19 @@ """Test the Opentherm Gateway config flow.""" import asyncio -from serial import SerialException from unittest.mock import patch +from pyotgw import OTGW_ABOUT +from serial import SerialException + from homeassistant import config_entries, data_entry_flow, setup -from homeassistant.const import CONF_DEVICE, CONF_ID, CONF_NAME, PRECISION_HALVES from homeassistant.components.opentherm_gw.const import ( - DOMAIN, CONF_FLOOR_TEMP, CONF_PRECISION, + DOMAIN, ) +from homeassistant.const import CONF_DEVICE, CONF_ID, CONF_NAME, PRECISION_HALVES -from pyotgw import OTGW_ABOUT -from tests.common import mock_coro, MockConfigEntry +from tests.common import MockConfigEntry, mock_coro async def test_form_user(hass): diff --git a/tests/components/openuv/test_config_flow.py b/tests/components/openuv/test_config_flow.py index c57e22e44e2..3aa67abdc4f 100644 --- a/tests/components/openuv/test_config_flow.py +++ b/tests/components/openuv/test_config_flow.py @@ -1,6 +1,8 @@ """Define tests for the OpenUV config flow.""" -import pytest +from unittest.mock import patch + from pyopenuv.errors import OpenUvError +import pytest from homeassistant import data_entry_flow from homeassistant.components.openuv import DOMAIN, config_flow @@ -11,7 +13,7 @@ from homeassistant.const import ( CONF_LONGITUDE, ) -from tests.common import MockConfigEntry, MockDependency, mock_coro +from tests.common import MockConfigEntry, mock_coro @pytest.fixture @@ -23,9 +25,9 @@ def uv_index_response(): @pytest.fixture def mock_pyopenuv(uv_index_response): """Mock the pyopenuv library.""" - with MockDependency("pyopenuv") as mock_pyopenuv_: - mock_pyopenuv_.Client().uv_index.return_value = uv_index_response - yield mock_pyopenuv_ + with patch("homeassistant.components.openuv.config_flow.Client") as MockClient: + MockClient().uv_index.return_value = uv_index_response + yield MockClient async def test_duplicate_error(hass): diff --git a/tests/components/owntracks/test_config_flow.py b/tests/components/owntracks/test_config_flow.py index 54e33a1ab61..21bb5bcf993 100644 --- a/tests/components/owntracks/test_config_flow.py +++ b/tests/components/owntracks/test_config_flow.py @@ -1,16 +1,16 @@ """Tests for OwnTracks config flow.""" from unittest.mock import Mock, patch + import pytest from homeassistant import data_entry_flow -from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.components.owntracks import config_flow from homeassistant.components.owntracks.config_flow import CONF_CLOUDHOOK, CONF_SECRET from homeassistant.components.owntracks.const import DOMAIN +from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.setup import async_setup_component -from tests.common import mock_coro, MockConfigEntry - +from tests.common import MockConfigEntry, mock_coro CONF_WEBHOOK_URL = "webhook_url" @@ -126,7 +126,7 @@ async def test_user_not_supports_encryption(hass, not_supports_encryption): async def test_unload(hass): """Test unloading a config flow.""" with patch( - "homeassistant.config_entries.ConfigEntries" ".async_forward_entry_setup" + "homeassistant.config_entries.ConfigEntries.async_forward_entry_setup" ) as mock_forward: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "import"}, data={} @@ -140,7 +140,7 @@ async def test_unload(hass): assert entry.data["webhook_id"] in hass.data["webhook"] with patch( - "homeassistant.config_entries.ConfigEntries" ".async_forward_entry_unload", + "homeassistant.config_entries.ConfigEntries.async_forward_entry_unload", return_value=mock_coro(), ) as mock_unload: assert await hass.config_entries.async_unload(entry.entry_id) diff --git a/tests/components/owntracks/test_device_tracker.py b/tests/components/owntracks/test_device_tracker.py index 5ba68474513..730da4bc7b2 100644 --- a/tests/components/owntracks/test_device_tracker.py +++ b/tests/components/owntracks/test_device_tracker.py @@ -1300,7 +1300,7 @@ async def test_single_waypoint_import(hass, context): async def test_not_implemented_message(hass, context): """Handle not implemented message type.""" patch_handler = patch( - "homeassistant.components.owntracks." "messages.async_handle_not_impl_msg", + "homeassistant.components.owntracks.messages.async_handle_not_impl_msg", return_value=mock_coro(False), ) patch_handler.start() @@ -1311,7 +1311,7 @@ async def test_not_implemented_message(hass, context): async def test_unsupported_message(hass, context): """Handle not implemented message type.""" patch_handler = patch( - "homeassistant.components.owntracks." "messages.async_handle_unsupported_msg", + "homeassistant.components.owntracks.messages.async_handle_unsupported_msg", return_value=mock_coro(False), ) patch_handler.start() @@ -1396,7 +1396,7 @@ def config_context(hass, setup_comp): patch_load.start() patch_save = patch( - "homeassistant.components.device_tracker." "DeviceTracker.async_update_config" + "homeassistant.components.device_tracker.DeviceTracker.async_update_config" ) patch_save.start() @@ -1406,6 +1406,25 @@ def config_context(hass, setup_comp): patch_save.stop() +@pytest.fixture(name="not_supports_encryption") +def mock_not_supports_encryption(): + """Mock non successful nacl import.""" + with patch( + "homeassistant.components.owntracks.messages.supports_encryption", + return_value=False, + ): + yield + + +@pytest.fixture(name="get_cipher_error") +def mock_get_cipher_error(): + """Mock non successful cipher.""" + with patch( + "homeassistant.components.owntracks.messages.get_cipher", side_effect=OSError() + ): + yield + + @patch("homeassistant.components.owntracks.messages.get_cipher", mock_cipher) async def test_encrypted_payload(hass, setup_comp): """Test encrypted payload.""" @@ -1422,6 +1441,22 @@ async def test_encrypted_payload_topic_key(hass, setup_comp): assert_location_latitude(hass, LOCATION_MESSAGE["lat"]) +async def test_encrypted_payload_not_supports_encryption( + hass, setup_comp, not_supports_encryption +): + """Test encrypted payload with no supported encryption.""" + await setup_owntracks(hass, {CONF_SECRET: TEST_SECRET_KEY}) + await send_message(hass, LOCATION_TOPIC, MOCK_ENCRYPTED_LOCATION_MESSAGE) + assert hass.states.get(DEVICE_TRACKER_STATE) is None + + +async def test_encrypted_payload_get_cipher_error(hass, setup_comp, get_cipher_error): + """Test encrypted payload with no supported encryption.""" + await setup_owntracks(hass, {CONF_SECRET: TEST_SECRET_KEY}) + await send_message(hass, LOCATION_TOPIC, MOCK_ENCRYPTED_LOCATION_MESSAGE) + assert hass.states.get(DEVICE_TRACKER_STATE) is None + + @patch("homeassistant.components.owntracks.messages.get_cipher", mock_cipher) async def test_encrypted_payload_no_key(hass, setup_comp): """Test encrypted payload with no key, .""" @@ -1460,8 +1495,7 @@ async def test_encrypted_payload_no_topic_key(hass, setup_comp): async def test_encrypted_payload_libsodium(hass, setup_comp): """Test sending encrypted message payload.""" try: - # pylint: disable=unused-import - import nacl # noqa: F401 + import nacl # noqa: F401 pylint: disable=unused-import except (ImportError, OSError): pytest.skip("PyNaCl/libsodium is not installed") return diff --git a/tests/components/owntracks/test_helper.py b/tests/components/owntracks/test_helper.py new file mode 100644 index 00000000000..2c06ac0c4e7 --- /dev/null +++ b/tests/components/owntracks/test_helper.py @@ -0,0 +1,30 @@ +"""Test the owntracks_http platform.""" +from unittest.mock import patch + +import pytest + +from homeassistant.components.owntracks import helper + + +@pytest.fixture(name="nacl_imported") +def mock_nacl_imported(): + """Mock a successful import.""" + with patch("homeassistant.components.owntracks.helper.nacl"): + yield + + +@pytest.fixture(name="nacl_not_imported") +def mock_nacl_not_imported(): + """Mock non successful import.""" + with patch("homeassistant.components.owntracks.helper.nacl", new=None): + yield + + +def test_supports_encryption(nacl_imported): + """Test if env supports encryption.""" + assert helper.supports_encryption() + + +def test_supports_encryption_failed(nacl_not_imported): + """Test if env does not support encryption.""" + assert not helper.supports_encryption() diff --git a/tests/components/owntracks/test_init.py b/tests/components/owntracks/test_init.py index aab772d64e3..e60efb42ee2 100644 --- a/tests/components/owntracks/test_init.py +++ b/tests/components/owntracks/test_init.py @@ -1,11 +1,10 @@ """Test the owntracks_http platform.""" -import asyncio - import pytest -from homeassistant.setup import async_setup_component from homeassistant.components import owntracks -from tests.common import mock_component, MockConfigEntry +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry, mock_component MINIMAL_LOCATION_MESSAGE = { "_type": "location", @@ -54,10 +53,9 @@ def mock_client(hass, aiohttp_client): return hass.loop.run_until_complete(aiohttp_client(hass.http.app)) -@asyncio.coroutine -def test_handle_valid_message(mock_client): +async def test_handle_valid_message(mock_client): """Test that we forward messages correctly to OwnTracks.""" - resp = yield from mock_client.post( + resp = await mock_client.post( "/api/webhook/owntracks_test", json=LOCATION_MESSAGE, headers={"X-Limit-u": "Paulus", "X-Limit-d": "Pixel"}, @@ -65,14 +63,13 @@ def test_handle_valid_message(mock_client): assert resp.status == 200 - json = yield from resp.json() + json = await resp.json() assert json == [] -@asyncio.coroutine -def test_handle_valid_minimal_message(mock_client): +async def test_handle_valid_minimal_message(mock_client): """Test that we forward messages correctly to OwnTracks.""" - resp = yield from mock_client.post( + resp = await mock_client.post( "/api/webhook/owntracks_test", json=MINIMAL_LOCATION_MESSAGE, headers={"X-Limit-u": "Paulus", "X-Limit-d": "Pixel"}, @@ -80,14 +77,13 @@ def test_handle_valid_minimal_message(mock_client): assert resp.status == 200 - json = yield from resp.json() + json = await resp.json() assert json == [] -@asyncio.coroutine -def test_handle_value_error(mock_client): +async def test_handle_value_error(mock_client): """Test we don't disclose that this is a valid webhook.""" - resp = yield from mock_client.post( + resp = await mock_client.post( "/api/webhook/owntracks_test", json="", headers={"X-Limit-u": "Paulus", "X-Limit-d": "Pixel"}, @@ -95,14 +91,13 @@ def test_handle_value_error(mock_client): assert resp.status == 200 - json = yield from resp.text() + json = await resp.text() assert json == "" -@asyncio.coroutine -def test_returns_error_missing_username(mock_client, caplog): +async def test_returns_error_missing_username(mock_client, caplog): """Test that an error is returned when username is missing.""" - resp = yield from mock_client.post( + resp = await mock_client.post( "/api/webhook/owntracks_test", json=LOCATION_MESSAGE, headers={"X-Limit-d": "Pixel"}, @@ -110,29 +105,27 @@ def test_returns_error_missing_username(mock_client, caplog): # Needs to be 200 or OwnTracks keeps retrying bad packet. assert resp.status == 200 - json = yield from resp.json() + json = await resp.json() assert json == [] assert "No topic or user found" in caplog.text -@asyncio.coroutine -def test_returns_error_incorrect_json(mock_client, caplog): +async def test_returns_error_incorrect_json(mock_client, caplog): """Test that an error is returned when username is missing.""" - resp = yield from mock_client.post( + resp = await mock_client.post( "/api/webhook/owntracks_test", data="not json", headers={"X-Limit-d": "Pixel"} ) # Needs to be 200 or OwnTracks keeps retrying bad packet. assert resp.status == 200 - json = yield from resp.json() + json = await resp.json() assert json == [] assert "invalid JSON" in caplog.text -@asyncio.coroutine -def test_returns_error_missing_device(mock_client): +async def test_returns_error_missing_device(mock_client): """Test that an error is returned when device name is missing.""" - resp = yield from mock_client.post( + resp = await mock_client.post( "/api/webhook/owntracks_test", json=LOCATION_MESSAGE, headers={"X-Limit-u": "Paulus"}, @@ -140,7 +133,7 @@ def test_returns_error_missing_device(mock_client): assert resp.status == 200 - json = yield from resp.json() + json = await resp.json() assert json == [] diff --git a/tests/components/persistent_notification/test_init.py b/tests/components/persistent_notification/test_init.py index 286d8a701ed..daf671bd52b 100644 --- a/tests/components/persistent_notification/test_init.py +++ b/tests/components/persistent_notification/test_init.py @@ -1,7 +1,7 @@ """The tests for the persistent notification component.""" -from homeassistant.components.websocket_api.const import TYPE_RESULT -from homeassistant.setup import setup_component, async_setup_component import homeassistant.components.persistent_notification as pn +from homeassistant.components.websocket_api.const import TYPE_RESULT +from homeassistant.setup import async_setup_component, setup_component from tests.common import get_test_home_assistant diff --git a/tests/components/pi_hole/test_init.py b/tests/components/pi_hole/test_init.py index f30422bfea9..c2d9ec77f03 100644 --- a/tests/components/pi_hole/test_init.py +++ b/tests/components/pi_hole/test_init.py @@ -1,39 +1,36 @@ """Test pi_hole component.""" -from asynctest import CoroutineMock -from hole import Hole - -from homeassistant.components import pi_hole -from tests.common import async_setup_component from unittest.mock import patch +from asynctest import CoroutineMock -def mock_pihole_data_call(Hole): - """Need to override so as to allow mocked data.""" - Hole.__init__ = ( - lambda self, host, loop, session, location, tls, verify_tls=True, api_token=None: None - ) - Hole.data = { - "ads_blocked_today": 0, - "ads_percentage_today": 0, - "clients_ever_seen": 0, - "dns_queries_today": 0, - "domains_being_blocked": 0, - "queries_cached": 0, - "queries_forwarded": 0, - "status": 0, - "unique_clients": 0, - "unique_domains": 0, - } - pass +from homeassistant.components import pi_hole + +from tests.common import async_setup_component + +ZERO_DATA = { + "ads_blocked_today": 0, + "ads_percentage_today": 0, + "clients_ever_seen": 0, + "dns_queries_today": 0, + "domains_being_blocked": 0, + "queries_cached": 0, + "queries_forwarded": 0, + "status": 0, + "unique_clients": 0, + "unique_domains": 0, +} -async def test_setup_no_config(hass): - """Tests component setup with no config.""" - with patch.object( - Hole, "get_data", new=CoroutineMock(side_effect=mock_pihole_data_call(Hole)) - ): - assert await async_setup_component(hass, pi_hole.DOMAIN, {pi_hole.DOMAIN: {}}) +async def test_setup_minimal_config(hass): + """Tests component setup with minimal config.""" + with patch("homeassistant.components.pi_hole.Hole") as _hole: + _hole.return_value.get_data = CoroutineMock(return_value=None) + _hole.return_value.data = ZERO_DATA + + assert await async_setup_component( + hass, pi_hole.DOMAIN, {pi_hole.DOMAIN: [{"host": "pi.hole"}]} + ) await hass.async_block_till_done() @@ -82,13 +79,16 @@ async def test_setup_no_config(hass): assert hass.states.get("sensor.pi_hole_seen_clients").state == "0" -async def test_setup_custom_config(hass): - """Tests component setup with custom config.""" - with patch.object( - Hole, "get_data", new=CoroutineMock(side_effect=mock_pihole_data_call(Hole)) - ): +async def test_setup_name_config(hass): + """Tests component setup with a custom name.""" + with patch("homeassistant.components.pi_hole.Hole") as _hole: + _hole.return_value.get_data = CoroutineMock(return_value=None) + _hole.return_value.data = ZERO_DATA + assert await async_setup_component( - hass, pi_hole.DOMAIN, {pi_hole.DOMAIN: {"name": "Custom"}} + hass, + pi_hole.DOMAIN, + {pi_hole.DOMAIN: [{"host": "pi.hole", "name": "Custom"}]}, ) await hass.async_block_till_done() @@ -97,3 +97,66 @@ async def test_setup_custom_config(hass): hass.states.get("sensor.custom_ads_blocked_today").name == "Custom Ads Blocked Today" ) + + +async def test_disable_service_call(hass): + """Test disable service call with no Pi-hole named.""" + with patch("homeassistant.components.pi_hole.Hole") as _hole: + mock_disable = CoroutineMock(return_value=None) + _hole.return_value.disable = mock_disable + _hole.return_value.get_data = CoroutineMock(return_value=None) + _hole.return_value.data = ZERO_DATA + + assert await async_setup_component( + hass, + pi_hole.DOMAIN, + { + pi_hole.DOMAIN: [ + {"host": "pi.hole", "api_key": "1"}, + {"host": "pi.hole", "name": "Custom", "api_key": "2"}, + ] + }, + ) + + await hass.async_block_till_done() + + await hass.services.async_call( + pi_hole.DOMAIN, + pi_hole.SERVICE_DISABLE, + {pi_hole.SERVICE_DISABLE_ATTR_DURATION: "00:00:01"}, + blocking=True, + ) + + await hass.async_block_till_done() + + assert mock_disable.call_count == 2 + + +async def test_enable_service_call(hass): + """Test enable service call with no Pi-hole named.""" + with patch("homeassistant.components.pi_hole.Hole") as _hole: + mock_enable = CoroutineMock(return_value=None) + _hole.return_value.enable = mock_enable + _hole.return_value.get_data = CoroutineMock(return_value=None) + _hole.return_value.data = ZERO_DATA + + assert await async_setup_component( + hass, + pi_hole.DOMAIN, + { + pi_hole.DOMAIN: [ + {"host": "pi.hole", "api_key": "1"}, + {"host": "pi.hole", "name": "Custom", "api_key": "2"}, + ] + }, + ) + + await hass.async_block_till_done() + + await hass.services.async_call( + pi_hole.DOMAIN, pi_hole.SERVICE_ENABLE, {}, blocking=True + ) + + await hass.async_block_till_done() + + assert mock_enable.call_count == 2 diff --git a/tests/components/pilight/test_init.py b/tests/components/pilight/test_init.py index e45c7ddc02f..2a2342a90dc 100644 --- a/tests/components/pilight/test_init.py +++ b/tests/components/pilight/test_init.py @@ -1,18 +1,18 @@ """The tests for the pilight component.""" +from datetime import timedelta import logging +import socket import unittest from unittest.mock import patch -import socket -from datetime import timedelta import pytest from homeassistant import core as ha -from homeassistant.setup import setup_component from homeassistant.components import pilight +from homeassistant.setup import setup_component from homeassistant.util import dt as dt_util -from tests.common import get_test_home_assistant, assert_setup_component +from tests.common import assert_setup_component, get_test_home_assistant _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/pilight/test_sensor.py b/tests/components/pilight/test_sensor.py index 721b04386fe..4bc1e80b07a 100644 --- a/tests/components/pilight/test_sensor.py +++ b/tests/components/pilight/test_sensor.py @@ -1,11 +1,11 @@ """The tests for the Pilight sensor platform.""" import logging -from homeassistant.setup import setup_component -import homeassistant.components.sensor as sensor from homeassistant.components import pilight +import homeassistant.components.sensor as sensor +from homeassistant.setup import setup_component -from tests.common import get_test_home_assistant, assert_setup_component, mock_component +from tests.common import assert_setup_component, get_test_home_assistant, mock_component HASS = None diff --git a/tests/components/plant/test_init.py b/tests/components/plant/test_init.py index fdbf43618ae..fb919d56607 100644 --- a/tests/components/plant/test_init.py +++ b/tests/components/plant/test_init.py @@ -1,23 +1,23 @@ """Unit tests for platform/plant.py.""" import asyncio -import unittest -import pytest from datetime import datetime, timedelta +import unittest + +import pytest -from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, - STATE_UNAVAILABLE, - STATE_UNKNOWN, - STATE_PROBLEM, - STATE_OK, -) from homeassistant.components import recorder import homeassistant.components.plant as plant +from homeassistant.const import ( + ATTR_UNIT_OF_MEASUREMENT, + STATE_OK, + STATE_PROBLEM, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) from homeassistant.setup import setup_component from tests.common import get_test_home_assistant, init_recorder_component - GOOD_DATA = { "moisture": 50, "battery": 90, diff --git a/tests/components/plex/mock_classes.py b/tests/components/plex/mock_classes.py index 69e6a84df63..de6ffa51170 100644 --- a/tests/components/plex/mock_classes.py +++ b/tests/components/plex/mock_classes.py @@ -1,6 +1,6 @@ """Mock classes used in tests.""" -from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.components.plex.const import CONF_SERVER, CONF_SERVER_IDENTIFIER +from homeassistant.const import CONF_HOST, CONF_PORT MOCK_SERVERS = [ { @@ -30,7 +30,7 @@ class MockResource: self.provides = ["server"] self._mock_plex_server = MockPlexServer(index) - def connect(self): + def connect(self, timeout): """Mock the resource connect method.""" return self._mock_plex_server diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index c0d14f1efdc..0fb1f850809 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -9,10 +9,10 @@ from homeassistant.components.plex import config_flow from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SSL, CONF_TOKEN, CONF_URL from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry - from .mock_classes import MOCK_SERVERS, MockPlexAccount, MockPlexServer +from tests.common import MockConfigEntry + MOCK_TOKEN = "secret_token" MOCK_FILE_CONTENTS = { f"{MOCK_SERVERS[0][CONF_HOST]}:{MOCK_SERVERS[0][CONF_PORT]}": { @@ -178,9 +178,8 @@ async def test_import_bad_hostname(hass): CONF_URL: f"http://{MOCK_SERVERS[0][CONF_HOST]}:{MOCK_SERVERS[0][CONF_PORT]}", }, ) - assert result["type"] == "form" - assert result["step_id"] == "start_website_auth" - assert result["errors"]["base"] == "not_found" + assert result["type"] == "abort" + assert result["reason"] == "non-interactive" async def test_unknown_exception(hass): @@ -384,12 +383,14 @@ async def test_already_configured(hass): mock_plex_server = MockPlexServer() flow = init_config_flow(hass) + flow.context = {"source": "import"} MockConfigEntry( domain=config_flow.DOMAIN, data={ + config_flow.CONF_SERVER: MOCK_SERVERS[0][config_flow.CONF_SERVER], config_flow.CONF_SERVER_IDENTIFIER: MOCK_SERVERS[0][ config_flow.CONF_SERVER_IDENTIFIER - ] + ], }, ).add_to_hass(hass) @@ -530,3 +531,20 @@ async def test_callback_view(hass, aiohttp_client): resp = await client.get(forward_url) assert resp.status == 200 + + +async def test_multiple_servers_with_import(hass): + """Test importing a config with multiple servers available.""" + + with patch( + "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(servers=2) + ), asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( + "plexauth.PlexAuth.token", return_value=MOCK_TOKEN + ): + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": "import"}, + data={CONF_TOKEN: MOCK_TOKEN}, + ) + assert result["type"] == "abort" + assert result["reason"] == "non-interactive" diff --git a/tests/components/point/test_config_flow.py b/tests/components/point/test_config_flow.py index ec808b5fd80..c1c705e752d 100644 --- a/tests/components/point/test_config_flow.py +++ b/tests/components/point/test_config_flow.py @@ -7,14 +7,14 @@ import pytest from homeassistant import data_entry_flow from homeassistant.components.point import DOMAIN, config_flow -from tests.common import MockDependency, mock_coro +from tests.common import mock_coro def init_config_flow(hass, side_effect=None): """Init a configuration flow.""" config_flow.register_flow_implementation(hass, DOMAIN, "id", "secret") flow = config_flow.PointFlowHandler() - flow._get_authorization_url = Mock( # pylint: disable=W0212 + flow._get_authorization_url = Mock( # pylint: disable=protected-access return_value=mock_coro("https://example.com"), side_effect=side_effect ) flow.hass = hass @@ -28,17 +28,17 @@ def is_authorized(): @pytest.fixture -def mock_pypoint(is_authorized): # pylint: disable=W0621 +def mock_pypoint(is_authorized): # pylint: disable=redefined-outer-name """Mock pypoint.""" - with MockDependency("pypoint") as mock_pypoint_: - mock_pypoint_.PointSession().get_access_token.return_value = { + with patch( + "homeassistant.components.point.config_flow.PointSession" + ) as PointSession: + PointSession.return_value.get_access_token.return_value = { "access_token": "boo" } - mock_pypoint_.PointSession().is_authorized = is_authorized - mock_pypoint_.PointSession().user.return_value = { - "email": "john.doe@example.com" - } - yield mock_pypoint_ + PointSession.return_value.is_authorized = is_authorized + PointSession.return_value.user.return_value = {"email": "john.doe@example.com"} + yield PointSession async def test_abort_if_no_implementation_registered(hass): @@ -66,7 +66,9 @@ async def test_abort_if_already_setup(hass): assert result["reason"] == "already_setup" -async def test_full_flow_implementation(hass, mock_pypoint): # pylint: disable=W0621 +async def test_full_flow_implementation( + hass, mock_pypoint # pylint: disable=redefined-outer-name +): """Test registering an implementation and finishing flow works.""" config_flow.register_flow_implementation(hass, "test-other", None, None) flow = init_config_flow(hass) @@ -92,7 +94,7 @@ async def test_full_flow_implementation(hass, mock_pypoint): # pylint: disable= assert result["data"]["token"] == {"access_token": "boo"} -async def test_step_import(hass, mock_pypoint): # pylint: disable=W0621 +async def test_step_import(hass, mock_pypoint): # pylint: disable=redefined-outer-name """Test that we trigger import when configuring with client.""" flow = init_config_flow(hass) @@ -104,7 +106,7 @@ async def test_step_import(hass, mock_pypoint): # pylint: disable=W0621 @pytest.mark.parametrize("is_authorized", [False]) async def test_wrong_code_flow_implementation( hass, mock_pypoint -): # pylint: disable=W0621 +): # pylint: disable=redefined-outer-name """Test wrong code.""" flow = init_config_flow(hass) diff --git a/tests/components/prometheus/test_init.py b/tests/components/prometheus/test_init.py index cf1ff7489f6..dd5b673e844 100644 --- a/tests/components/prometheus/test_init.py +++ b/tests/components/prometheus/test_init.py @@ -1,14 +1,14 @@ """The tests for the Prometheus exporter.""" import asyncio -import pytest -from homeassistant.const import ENERGY_KILO_WATT_HOUR, DEVICE_CLASS_POWER +import pytest from homeassistant import setup from homeassistant.components import climate, sensor from homeassistant.components.demo.sensor import DemoSensor -from homeassistant.setup import async_setup_component import homeassistant.components.prometheus as prometheus +from homeassistant.const import DEVICE_CLASS_POWER, ENERGY_KILO_WATT_HOUR +from homeassistant.setup import async_setup_component @pytest.fixture diff --git a/tests/components/proximity/test_init.py b/tests/components/proximity/test_init.py index 41e9ea98ebd..a01d625d8c4 100644 --- a/tests/components/proximity/test_init.py +++ b/tests/components/proximity/test_init.py @@ -3,8 +3,8 @@ import unittest from homeassistant.components import proximity from homeassistant.components.proximity import DOMAIN - from homeassistant.setup import setup_component + from tests.common import get_test_home_assistant diff --git a/tests/components/ps4/test_init.py b/tests/components/ps4/test_init.py index 5b6d6f87cd5..028c1643ff0 100644 --- a/tests/components/ps4/test_init.py +++ b/tests/components/ps4/test_init.py @@ -26,8 +26,9 @@ from homeassistant.const import ( CONF_TOKEN, ) from homeassistant.exceptions import HomeAssistantError -from homeassistant.util import location from homeassistant.setup import async_setup_component +from homeassistant.util import location + from tests.common import MockConfigEntry, mock_coro, mock_registry MOCK_HOST = "192.168.0.1" diff --git a/tests/components/ps4/test_media_player.py b/tests/components/ps4/test_media_player.py index e2b9e382dc4..56a659aa152 100644 --- a/tests/components/ps4/test_media_player.py +++ b/tests/components/ps4/test_media_player.py @@ -29,13 +29,13 @@ from homeassistant.const import ( CONF_REGION, CONF_TOKEN, STATE_IDLE, - STATE_STANDBY, STATE_PLAYING, + STATE_STANDBY, STATE_UNKNOWN, ) from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry, mock_device_registry, mock_registry, mock_coro +from tests.common import MockConfigEntry, mock_coro, mock_device_registry, mock_registry MOCK_CREDS = "123412341234abcd12341234abcd12341234abcd12341234abcd12341234abcd" MOCK_NAME = "ha_ps4_name" diff --git a/tests/components/ptvsd/test_ptvsd.py b/tests/components/ptvsd/test_ptvsd.py index e7d9c97be22..d4a2aa1ab94 100644 --- a/tests/components/ptvsd/test_ptvsd.py +++ b/tests/components/ptvsd/test_ptvsd.py @@ -1,12 +1,13 @@ """Tests for PTVSD Debugger.""" from unittest.mock import patch + from asynctest import CoroutineMock from pytest import mark +from homeassistant.bootstrap import _async_set_up_integrations import homeassistant.components.ptvsd as ptvsd_component from homeassistant.setup import async_setup_component -from homeassistant.bootstrap import _async_set_up_integrations @mark.skip("causes code cover to fail") diff --git a/tests/components/push/test_camera.py b/tests/components/push/test_camera.py index c48f0c4322a..b5803b96889 100644 --- a/tests/components/push/test_camera.py +++ b/tests/components/push/test_camera.py @@ -1,7 +1,6 @@ """The tests for generic camera component.""" -import io - from datetime import timedelta +import io from homeassistant import core as ha from homeassistant.setup import async_setup_component diff --git a/tests/components/pushbullet/test_notify.py b/tests/components/pushbullet/test_notify.py index d95a942d25b..4c731c1f704 100644 --- a/tests/components/pushbullet/test_notify.py +++ b/tests/components/pushbullet/test_notify.py @@ -6,8 +6,9 @@ from unittest.mock import patch from pushbullet import PushBullet import requests_mock -from homeassistant.setup import setup_component import homeassistant.components.notify as notify +from homeassistant.setup import setup_component + from tests.common import assert_setup_component, get_test_home_assistant, load_fixture diff --git a/tests/components/python_script/test_init.py b/tests/components/python_script/test_init.py index d7732c00f94..f61f0004723 100644 --- a/tests/components/python_script/test_init.py +++ b/tests/components/python_script/test_init.py @@ -1,17 +1,15 @@ """Test the python_script component.""" -import asyncio import logging -from unittest.mock import patch, mock_open +from unittest.mock import mock_open, patch +from homeassistant.components.python_script import DOMAIN, FOLDER, execute from homeassistant.helpers.service import async_get_all_descriptions from homeassistant.setup import async_setup_component -from homeassistant.components.python_script import DOMAIN, execute, FOLDER from tests.common import patch_yaml_files -@asyncio.coroutine -def test_setup(hass): +async def test_setup(hass): """Test we can discover scripts.""" scripts = [ "/some/config/dir/python_scripts/hello.py", @@ -20,7 +18,7 @@ def test_setup(hass): with patch( "homeassistant.components.python_script.os.path.isdir", return_value=True ), patch("homeassistant.components.python_script.glob.iglob", return_value=scripts): - res = yield from async_setup_component(hass, "python_script", {}) + res = await async_setup_component(hass, "python_script", {}) assert res assert hass.services.has_service("python_script", "hello") @@ -31,7 +29,7 @@ def test_setup(hass): mock_open(read_data="fake source"), create=True, ), patch("homeassistant.components.python_script.execute") as mock_ex: - yield from hass.services.async_call( + await hass.services.async_call( "python_script", "hello", {"some": "data"}, blocking=True ) @@ -44,20 +42,18 @@ def test_setup(hass): assert data == {"some": "data"} -@asyncio.coroutine -def test_setup_fails_on_no_dir(hass, caplog): +async def test_setup_fails_on_no_dir(hass, caplog): """Test we fail setup when no dir found.""" with patch( "homeassistant.components.python_script.os.path.isdir", return_value=False ): - res = yield from async_setup_component(hass, "python_script", {}) + res = await async_setup_component(hass, "python_script", {}) assert not res assert "Folder python_scripts not found in configuration folder" in caplog.text -@asyncio.coroutine -def test_execute_with_data(hass, caplog): +async def test_execute_with_data(hass, caplog): """Test executing a script.""" caplog.set_level(logging.WARNING) source = """ @@ -65,7 +61,7 @@ hass.states.set('test.entity', data.get('name', 'not set')) """ hass.async_add_job(execute, hass, "test.py", source, {"name": "paulus"}) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert hass.states.is_state("test.entity", "paulus") @@ -73,8 +69,7 @@ hass.states.set('test.entity', data.get('name', 'not set')) assert caplog.text == "" -@asyncio.coroutine -def test_execute_warns_print(hass, caplog): +async def test_execute_warns_print(hass, caplog): """Test print triggers warning.""" caplog.set_level(logging.WARNING) source = """ @@ -82,13 +77,12 @@ print("This triggers warning.") """ hass.async_add_job(execute, hass, "test.py", source, {}) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert "Don't use print() inside scripts." in caplog.text -@asyncio.coroutine -def test_execute_logging(hass, caplog): +async def test_execute_logging(hass, caplog): """Test logging works.""" caplog.set_level(logging.INFO) source = """ @@ -96,13 +90,12 @@ logger.info('Logging from inside script') """ hass.async_add_job(execute, hass, "test.py", source, {}) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert "Logging from inside script" in caplog.text -@asyncio.coroutine -def test_execute_compile_error(hass, caplog): +async def test_execute_compile_error(hass, caplog): """Test compile error logs error.""" caplog.set_level(logging.ERROR) source = """ @@ -110,13 +103,12 @@ this is not valid Python """ hass.async_add_job(execute, hass, "test.py", source, {}) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert "Error loading script test.py" in caplog.text -@asyncio.coroutine -def test_execute_runtime_error(hass, caplog): +async def test_execute_runtime_error(hass, caplog): """Test compile error logs error.""" caplog.set_level(logging.ERROR) source = """ @@ -124,13 +116,12 @@ raise Exception('boom') """ hass.async_add_job(execute, hass, "test.py", source, {}) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert "Error executing script: boom" in caplog.text -@asyncio.coroutine -def test_accessing_async_methods(hass, caplog): +async def test_accessing_async_methods(hass, caplog): """Test compile error logs error.""" caplog.set_level(logging.ERROR) source = """ @@ -138,13 +129,12 @@ hass.async_stop() """ hass.async_add_job(execute, hass, "test.py", source, {}) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert "Not allowed to access async methods" in caplog.text -@asyncio.coroutine -def test_using_complex_structures(hass, caplog): +async def test_using_complex_structures(hass, caplog): """Test that dicts and lists work.""" caplog.set_level(logging.INFO) source = """ @@ -154,13 +144,12 @@ logger.info('Logging from inside script: %s %s' % (mydict["a"], mylist[2])) """ hass.async_add_job(execute, hass, "test.py", source, {}) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert "Logging from inside script: 1 3" in caplog.text -@asyncio.coroutine -def test_accessing_forbidden_methods(hass, caplog): +async def test_accessing_forbidden_methods(hass, caplog): """Test compile error logs error.""" caplog.set_level(logging.ERROR) @@ -172,12 +161,11 @@ def test_accessing_forbidden_methods(hass, caplog): }.items(): caplog.records.clear() hass.async_add_job(execute, hass, "test.py", source, {}) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert "Not allowed to access {}".format(name) in caplog.text -@asyncio.coroutine -def test_iterating(hass): +async def test_iterating(hass): """Test compile error logs error.""" source = """ for i in [1, 2]: @@ -185,14 +173,13 @@ for i in [1, 2]: """ hass.async_add_job(execute, hass, "test.py", source, {}) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert hass.states.is_state("hello.1", "world") assert hass.states.is_state("hello.2", "world") -@asyncio.coroutine -def test_unpacking_sequence(hass, caplog): +async def test_unpacking_sequence(hass, caplog): """Test compile error logs error.""" caplog.set_level(logging.ERROR) source = """ @@ -204,7 +191,7 @@ hass.states.set('hello.ab_list', '{}'.format(ab_list)) """ hass.async_add_job(execute, hass, "test.py", source, {}) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert hass.states.is_state("hello.a", "1") assert hass.states.is_state("hello.b", "2") @@ -214,8 +201,7 @@ hass.states.set('hello.ab_list', '{}'.format(ab_list)) assert caplog.text == "" -@asyncio.coroutine -def test_execute_sorted(hass, caplog): +async def test_execute_sorted(hass, caplog): """Test sorted() function.""" caplog.set_level(logging.ERROR) source = """ @@ -226,7 +212,7 @@ hass.states.set('hello.b', a[1]) hass.states.set('hello.c', a[2]) """ hass.async_add_job(execute, hass, "test.py", source, {}) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert hass.states.is_state("hello.a", "1") assert hass.states.is_state("hello.b", "2") @@ -235,8 +221,7 @@ hass.states.set('hello.c', a[2]) assert caplog.text == "" -@asyncio.coroutine -def test_exposed_modules(hass, caplog): +async def test_exposed_modules(hass, caplog): """Test datetime and time modules exposed.""" caplog.set_level(logging.ERROR) source = """ @@ -248,7 +233,7 @@ hass.states.set('module.datetime', """ hass.async_add_job(execute, hass, "test.py", source, {}) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert hass.states.is_state("module.time", "1986") assert hass.states.is_state("module.time_strptime", "12:34") @@ -258,8 +243,29 @@ hass.states.set('module.datetime', assert caplog.text == "" -@asyncio.coroutine -def test_reload(hass): +async def test_execute_functions(hass, caplog): + """Test functions defined in script can call one another.""" + caplog.set_level(logging.ERROR) + source = """ +def a(): + hass.states.set('hello.a', 'one') + +def b(): + a() + hass.states.set('hello.b', 'two') + +b() +""" + hass.async_add_job(execute, hass, "test.py", source, {}) + await hass.async_block_till_done() + + assert hass.states.is_state("hello.a", "one") + assert hass.states.is_state("hello.b", "two") + # No errors logged = good + assert caplog.text == "" + + +async def test_reload(hass): """Test we can re-discover scripts.""" scripts = [ "/some/config/dir/python_scripts/hello.py", @@ -268,7 +274,7 @@ def test_reload(hass): with patch( "homeassistant.components.python_script.os.path.isdir", return_value=True ), patch("homeassistant.components.python_script.glob.iglob", return_value=scripts): - res = yield from async_setup_component(hass, "python_script", {}) + res = await async_setup_component(hass, "python_script", {}) assert res assert hass.services.has_service("python_script", "hello") @@ -282,9 +288,7 @@ def test_reload(hass): with patch( "homeassistant.components.python_script.os.path.isdir", return_value=True ), patch("homeassistant.components.python_script.glob.iglob", return_value=scripts): - yield from hass.services.async_call( - "python_script", "reload", {}, blocking=True - ) + await hass.services.async_call("python_script", "reload", {}, blocking=True) assert not hass.services.has_service("python_script", "hello") assert hass.services.has_service("python_script", "hello2") @@ -387,8 +391,7 @@ async def test_service_descriptions(hass): ) -@asyncio.coroutine -def test_sleep_warns_one(hass, caplog): +async def test_sleep_warns_one(hass, caplog): """Test time.sleep warns once.""" caplog.set_level(logging.WARNING) source = """ @@ -398,6 +401,6 @@ time.sleep(5) with patch("homeassistant.components.python_script.time.sleep"): hass.async_add_job(execute, hass, "test.py", source, {}) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert caplog.text.count("time.sleep") == 1 diff --git a/tests/components/qld_bushfire/test_geo_location.py b/tests/components/qld_bushfire/test_geo_location.py index 59de4643cb8..ad9bdd7c536 100644 --- a/tests/components/qld_bushfire/test_geo_location.py +++ b/tests/components/qld_bushfire/test_geo_location.py @@ -1,6 +1,6 @@ """The tests for the Queensland Bushfire Alert Feed platform.""" import datetime -from unittest.mock import patch, MagicMock, call +from unittest.mock import MagicMock, call, patch from homeassistant.components import geo_location from homeassistant.components.geo_location import ATTR_SOURCE @@ -25,9 +25,10 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_START, ) from homeassistant.setup import async_setup_component -from tests.common import assert_setup_component, async_fire_time_changed import homeassistant.util.dt as dt_util +from tests.common import assert_setup_component, async_fire_time_changed + CONFIG = {geo_location.DOMAIN: [{"platform": "qld_bushfire", CONF_RADIUS: 200}]} CONFIG_WITH_CUSTOM_LOCATION = { @@ -88,7 +89,7 @@ async def test_setup(hass): # Patching 'utcnow' to gain more control over the timed update. utcnow = dt_util.utcnow() with patch("homeassistant.util.dt.utcnow", return_value=utcnow), patch( - "georss_qld_bushfire_alert_client." "QldBushfireAlertFeed" + "georss_qld_bushfire_alert_client.QldBushfireAlertFeed" ) as mock_feed: mock_feed.return_value.update.return_value = ( "OK", @@ -192,7 +193,7 @@ async def test_setup_with_custom_location(hass): "1234", "Title 1", 20.5, (38.1, -3.1), category="Category 1" ) - with patch("georss_qld_bushfire_alert_client." "QldBushfireAlertFeed") as mock_feed: + with patch("georss_qld_bushfire_alert_client.QldBushfireAlertFeed") as mock_feed: mock_feed.return_value.update.return_value = "OK", [mock_entry_1] with assert_setup_component(1, geo_location.DOMAIN): diff --git a/tests/components/qwikswitch/test_init.py b/tests/components/qwikswitch/test_init.py index e573e8cc293..d9c2a8d0ba6 100644 --- a/tests/components/qwikswitch/test_init.py +++ b/tests/components/qwikswitch/test_init.py @@ -1,14 +1,14 @@ """Test qwikswitch sensors.""" import logging +from aiohttp.client_exceptions import ClientError import pytest -from homeassistant.const import EVENT_HOMEASSISTANT_START -from homeassistant.components.qwikswitch import DOMAIN as QWIKSWITCH from homeassistant.bootstrap import async_setup_component -from tests.test_util.aiohttp import mock_aiohttp_client -from aiohttp.client_exceptions import ClientError +from homeassistant.components.qwikswitch import DOMAIN as QWIKSWITCH +from homeassistant.const import EVENT_HOMEASSISTANT_START +from tests.test_util.aiohttp import mock_aiohttp_client _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/radarr/test_sensor.py b/tests/components/radarr/test_sensor.py index ecd0c501ee8..114daa7d2f7 100644 --- a/tests/components/radarr/test_sensor.py +++ b/tests/components/radarr/test_sensor.py @@ -182,7 +182,7 @@ def mocked_requests_get(*args, **kwargs): "sqliteVersion": "3.16.2", "urlBase": "", "runtimeVersion": ( - "4.6.1 " "(Stable 4.6.1.3/abb06f1 " "Mon Oct 3 07:57:59 UTC 2016)" + "4.6.1 (Stable 4.6.1.3/abb06f1 Mon Oct 3 07:57:59 UTC 2016)" ), }, 200, diff --git a/tests/components/rainmachine/test_config_flow.py b/tests/components/rainmachine/test_config_flow.py index f888f9d4642..9e43f647301 100644 --- a/tests/components/rainmachine/test_config_flow.py +++ b/tests/components/rainmachine/test_config_flow.py @@ -1,14 +1,16 @@ """Define tests for the OpenUV config flow.""" from unittest.mock import patch +from regenmaschine.errors import RainMachineError + from homeassistant import data_entry_flow from homeassistant.components.rainmachine import DOMAIN, config_flow from homeassistant.const import ( CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT, - CONF_SSL, CONF_SCAN_INTERVAL, + CONF_SSL, ) from tests.common import MockConfigEntry, mock_coro @@ -33,8 +35,6 @@ async def test_duplicate_error(hass): async def test_invalid_password(hass): """Test that an invalid password throws an error.""" - from regenmaschine.errors import RainMachineError - conf = { CONF_IP_ADDRESS: "192.168.1.100", CONF_PASSWORD: "bad_password", @@ -46,7 +46,8 @@ async def test_invalid_password(hass): flow.hass = hass with patch( - "regenmaschine.login", return_value=mock_coro(exception=RainMachineError) + "homeassistant.components.rainmachine.config_flow.login", + return_value=mock_coro(exception=RainMachineError), ): result = await flow.async_step_user(user_input=conf) assert result["errors"] == {CONF_PASSWORD: "invalid_credentials"} @@ -75,7 +76,10 @@ async def test_step_import(hass): flow = config_flow.RainMachineFlowHandler() flow.hass = hass - with patch("regenmaschine.login", return_value=mock_coro(True)): + with patch( + "homeassistant.components.rainmachine.config_flow.login", + return_value=mock_coro(True), + ): result = await flow.async_step_import(import_config=conf) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -101,7 +105,10 @@ async def test_step_user(hass): flow = config_flow.RainMachineFlowHandler() flow.hass = hass - with patch("regenmaschine.login", return_value=mock_coro(True)): + with patch( + "homeassistant.components.rainmachine.config_flow.login", + return_value=mock_coro(True), + ): result = await flow.async_step_user(user_input=conf) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY diff --git a/tests/components/random/test_binary_sensor.py b/tests/components/random/test_binary_sensor.py index 5bc332e6da3..a11b571dd83 100644 --- a/tests/components/random/test_binary_sensor.py +++ b/tests/components/random/test_binary_sensor.py @@ -18,7 +18,7 @@ class TestRandomSensor(unittest.TestCase): """Stop everything that was started.""" self.hass.stop() - @patch("random.getrandbits", return_value=1) + @patch("homeassistant.components.random.binary_sensor.getrandbits", return_value=1) def test_random_binary_sensor_on(self, mocked): """Test the Random binary sensor.""" config = {"binary_sensor": {"platform": "random", "name": "test"}} @@ -29,7 +29,9 @@ class TestRandomSensor(unittest.TestCase): assert state.state == "on" - @patch("random.getrandbits", return_value=False) + @patch( + "homeassistant.components.random.binary_sensor.getrandbits", return_value=False + ) def test_random_binary_sensor_off(self, mocked): """Test the Random binary sensor.""" config = {"binary_sensor": {"platform": "random", "name": "test"}} diff --git a/tests/components/recorder/models_original.py b/tests/components/recorder/models_original.py index 526116aae7b..25978ef6d55 100644 --- a/tests/components/recorder/models_original.py +++ b/tests/components/recorder/models_original.py @@ -4,8 +4,8 @@ This file contains the original models definitions before schema tracking was implemented. It is used to test the schema migration logic. """ -import json from datetime import datetime +import json import logging from sqlalchemy import ( @@ -21,9 +21,9 @@ from sqlalchemy import ( ) from sqlalchemy.ext.declarative import declarative_base -import homeassistant.util.dt as dt_util from homeassistant.core import Event, EventOrigin, State, split_entity_id from homeassistant.helpers.json import JSONEncoder +import homeassistant.util.dt as dt_util # SQLAlchemy Schema # pylint: disable=invalid-name diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 0f5ee24e6e8..2ee3126b9fa 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -5,13 +5,13 @@ from unittest.mock import patch import pytest -from homeassistant.core import callback -from homeassistant.const import MATCH_ALL -from homeassistant.setup import async_setup_component from homeassistant.components.recorder import Recorder from homeassistant.components.recorder.const import DATA_INSTANCE +from homeassistant.components.recorder.models import Events, States from homeassistant.components.recorder.util import session_scope -from homeassistant.components.recorder.models import States, Events +from homeassistant.const import MATCH_ALL +from homeassistant.core import callback +from homeassistant.setup import async_setup_component from tests.common import get_test_home_assistant, init_recorder_component diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index 81e0423a723..7947ba5ccef 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -1,13 +1,14 @@ """The tests for the Recorder component.""" # pylint: disable=protected-access -from unittest.mock import patch, call +from unittest.mock import call, patch import pytest from sqlalchemy import create_engine from sqlalchemy.pool import StaticPool from homeassistant.bootstrap import async_setup_component -from homeassistant.components.recorder import migration, const, models +from homeassistant.components.recorder import const, migration, models + from tests.components.recorder import models_original diff --git a/tests/components/recorder/test_models.py b/tests/components/recorder/test_models.py index 5d4ac46102e..276194b5d6c 100644 --- a/tests/components/recorder/test_models.py +++ b/tests/components/recorder/test_models.py @@ -1,14 +1,14 @@ """The tests for the Recorder component.""" -import unittest from datetime import datetime +import unittest from sqlalchemy import create_engine from sqlalchemy.orm import scoped_session, sessionmaker -import homeassistant.core as ha +from homeassistant.components.recorder.models import Base, Events, RecorderRuns, States from homeassistant.const import EVENT_STATE_CHANGED +import homeassistant.core as ha from homeassistant.util import dt -from homeassistant.components.recorder.models import Base, Events, States, RecorderRuns ENGINE = None SESSION = None diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py index 7e06dcd1e5e..e0993b8cffc 100644 --- a/tests/components/recorder/test_purge.py +++ b/tests/components/recorder/test_purge.py @@ -1,14 +1,15 @@ """Test data purging.""" -import json from datetime import datetime, timedelta +import json import unittest from unittest.mock import patch from homeassistant.components import recorder from homeassistant.components.recorder.const import DATA_INSTANCE +from homeassistant.components.recorder.models import Events, States from homeassistant.components.recorder.purge import purge_old_data -from homeassistant.components.recorder.models import States, Events from homeassistant.components.recorder.util import session_scope + from tests.common import get_test_home_assistant, init_recorder_component diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index 8dcfac3c001..47f2bf4beca 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -1,10 +1,11 @@ """Test util methods.""" -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch import pytest from homeassistant.components.recorder import util from homeassistant.components.recorder.const import DATA_INSTANCE + from tests.common import get_test_home_assistant, init_recorder_component diff --git a/tests/components/reddit/test_sensor.py b/tests/components/reddit/test_sensor.py index a421f6f417c..c44c62fe080 100644 --- a/tests/components/reddit/test_sensor.py +++ b/tests/components/reddit/test_sensor.py @@ -5,23 +5,22 @@ from unittest.mock import patch from homeassistant.components.reddit import sensor as reddit_sensor from homeassistant.components.reddit.sensor import ( - DOMAIN, - ATTR_SUBREDDIT, - ATTR_POSTS, - CONF_SORT_BY, - ATTR_ID, - ATTR_URL, - ATTR_TITLE, - ATTR_SCORE, + ATTR_BODY, ATTR_COMMENTS_NUMBER, ATTR_CREATED, - ATTR_BODY, + ATTR_ID, + ATTR_POSTS, + ATTR_SCORE, + ATTR_SUBREDDIT, + ATTR_TITLE, + ATTR_URL, + CONF_SORT_BY, + DOMAIN, ) -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_MAXIMUM +from homeassistant.const import CONF_MAXIMUM, CONF_PASSWORD, CONF_USERNAME from homeassistant.setup import setup_component -from tests.common import get_test_home_assistant, MockDependency - +from tests.common import MockDependency, get_test_home_assistant VALID_CONFIG = { "sensor": { diff --git a/tests/components/remember_the_milk/test_init.py b/tests/components/remember_the_milk/test_init.py index f27e41451a3..e09e1e01dab 100644 --- a/tests/components/remember_the_milk/test_init.py +++ b/tests/components/remember_the_milk/test_init.py @@ -1,9 +1,9 @@ """Tests for the Remember The Milk component.""" -import logging import json +import logging import unittest -from unittest.mock import patch, mock_open, Mock +from unittest.mock import Mock, mock_open, patch import homeassistant.components.remember_the_milk as rtm diff --git a/tests/components/remote/common.py b/tests/components/remote/common.py index a35489f1780..1f4a5268440 100644 --- a/tests/components/remote/common.py +++ b/tests/components/remote/common.py @@ -15,12 +15,17 @@ from homeassistant.components.remote import ( SERVICE_LEARN_COMMAND, SERVICE_SEND_COMMAND, ) -from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON +from homeassistant.const import ( + ATTR_ENTITY_ID, + ENTITY_MATCH_ALL, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) from homeassistant.loader import bind_hass @bind_hass -def turn_on(hass, activity=None, entity_id=None): +def turn_on(hass, activity=None, entity_id=ENTITY_MATCH_ALL): """Turn all or specified remote on.""" data = { key: value @@ -31,7 +36,7 @@ def turn_on(hass, activity=None, entity_id=None): @bind_hass -def turn_off(hass, activity=None, entity_id=None): +def turn_off(hass, activity=None, entity_id=ENTITY_MATCH_ALL): """Turn all or specified remote off.""" data = {} if activity: @@ -45,7 +50,12 @@ def turn_off(hass, activity=None, entity_id=None): @bind_hass def send_command( - hass, command, entity_id=None, device=None, num_repeats=None, delay_secs=None + hass, + command, + entity_id=ENTITY_MATCH_ALL, + device=None, + num_repeats=None, + delay_secs=None, ): """Send a command to a device.""" data = {ATTR_COMMAND: command} @@ -66,7 +76,12 @@ def send_command( @bind_hass def learn_command( - hass, entity_id=None, device=None, command=None, alternative=None, timeout=None + hass, + entity_id=ENTITY_MATCH_ALL, + device=None, + command=None, + alternative=None, + timeout=None, ): """Learn a command from a device.""" data = {} diff --git a/tests/components/remote/test_init.py b/tests/components/remote/test_init.py index 723f38baced..392f0e6fa61 100644 --- a/tests/components/remote/test_init.py +++ b/tests/components/remote/test_init.py @@ -3,17 +3,17 @@ import unittest +import homeassistant.components.remote as remote from homeassistant.const import ( ATTR_ENTITY_ID, - STATE_ON, - STATE_OFF, CONF_PLATFORM, - SERVICE_TURN_ON, SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, ) -import homeassistant.components.remote as remote -from tests.common import mock_service, get_test_home_assistant +from tests.common import get_test_home_assistant, mock_service from tests.components.remote import common TEST_PLATFORM = {remote.DOMAIN: {CONF_PLATFORM: "test"}} diff --git a/tests/components/rest/test_binary_sensor.py b/tests/components/rest/test_binary_sensor.py index 63a7d3ff273..a4850793ca7 100644 --- a/tests/components/rest/test_binary_sensor.py +++ b/tests/components/rest/test_binary_sensor.py @@ -1,21 +1,21 @@ """The tests for the REST binary sensor platform.""" import unittest -from pytest import raises -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch +import pytest +from pytest import raises import requests -from requests.exceptions import Timeout, MissingSchema +from requests.exceptions import Timeout import requests_mock -from homeassistant.exceptions import PlatformNotReady -from homeassistant.setup import setup_component import homeassistant.components.binary_sensor as binary_sensor import homeassistant.components.rest.binary_sensor as rest -from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import template +from homeassistant.setup import setup_component -from tests.common import get_test_home_assistant, assert_setup_component -import pytest +from tests.common import assert_setup_component, get_test_home_assistant class TestRestBinarySensorSetup(unittest.TestCase): @@ -47,7 +47,7 @@ class TestRestBinarySensorSetup(unittest.TestCase): def test_setup_missing_schema(self): """Test setup with resource missing schema.""" - with pytest.raises(MissingSchema): + with pytest.raises(PlatformNotReady): rest.setup_platform( self.hass, {"platform": "rest", "resource": "localhost", "method": "GET"}, @@ -60,7 +60,7 @@ class TestRestBinarySensorSetup(unittest.TestCase): with raises(PlatformNotReady): rest.setup_platform( self.hass, - {"platform": "rest", "resource": "http://localhost"}, + {"platform": "rest", "resource": "http://localhost", "method": "GET"}, self.add_devices, None, ) @@ -72,7 +72,7 @@ class TestRestBinarySensorSetup(unittest.TestCase): with raises(PlatformNotReady): rest.setup_platform( self.hass, - {"platform": "rest", "resource": "http://localhost"}, + {"platform": "rest", "resource": "http://localhost", "method": "GET"}, self.add_devices, None, ) diff --git a/tests/components/rest/test_sensor.py b/tests/components/rest/test_sensor.py index 50acb053347..7edbfa065ad 100644 --- a/tests/components/rest/test_sensor.py +++ b/tests/components/rest/test_sensor.py @@ -1,20 +1,20 @@ """The tests for the REST sensor platform.""" import unittest -from pytest import raises -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch +import pytest +from pytest import raises import requests -from requests.exceptions import Timeout, MissingSchema, RequestException +from requests.exceptions import RequestException, Timeout import requests_mock -from homeassistant.exceptions import PlatformNotReady -from homeassistant.setup import setup_component -import homeassistant.components.sensor as sensor import homeassistant.components.rest.sensor as rest +import homeassistant.components.sensor as sensor +from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.config_validation import template +from homeassistant.setup import setup_component -from tests.common import get_test_home_assistant, assert_setup_component -import pytest +from tests.common import assert_setup_component, get_test_home_assistant class TestRestSensorSetup(unittest.TestCase): @@ -37,7 +37,7 @@ class TestRestSensorSetup(unittest.TestCase): def test_setup_missing_schema(self): """Test setup with resource missing schema.""" - with pytest.raises(MissingSchema): + with pytest.raises(PlatformNotReady): rest.setup_platform( self.hass, {"platform": "rest", "resource": "localhost", "method": "GET"}, @@ -50,7 +50,7 @@ class TestRestSensorSetup(unittest.TestCase): with raises(PlatformNotReady): rest.setup_platform( self.hass, - {"platform": "rest", "resource": "http://localhost"}, + {"platform": "rest", "resource": "http://localhost", "method": "GET"}, lambda devices, update=True: None, ) @@ -60,7 +60,7 @@ class TestRestSensorSetup(unittest.TestCase): with raises(PlatformNotReady): rest.setup_platform( self.hass, - {"platform": "rest", "resource": "http://localhost"}, + {"platform": "rest", "resource": "http://localhost", "method": "GET"}, lambda devices, update=True: None, ) @@ -284,6 +284,26 @@ class TestRestSensor(unittest.TestCase): self.sensor.update() assert "some_json_value" == self.sensor.device_state_attributes["key"] + def test_update_with_json_attrs_list_dict(self): + """Test attributes get extracted from a JSON list[0] result.""" + self.rest.update = Mock( + "rest.RestData.update", + side_effect=self.update_side_effect('[{ "key": "another_value" }]'), + ) + self.sensor = rest.RestSensor( + self.hass, + self.rest, + self.name, + self.unit_of_measurement, + self.device_class, + None, + ["key"], + self.force_update, + self.resource_template, + ) + self.sensor.update() + assert "another_value" == self.sensor.device_state_attributes["key"] + @patch("homeassistant.components.rest.sensor._LOGGER") def test_update_with_json_attrs_no_data(self, mock_logger): """Test attributes when no JSON result fetched.""" @@ -397,7 +417,7 @@ class TestRestData(unittest.TestCase): self.rest.update() assert "test data" == self.rest.data - @patch("requests.Session", side_effect=RequestException) + @patch("requests.request", side_effect=RequestException) def test_update_request_exception(self, mock_req): """Test update when a request exception occurs.""" self.rest.update() diff --git a/tests/components/rest/test_switch.py b/tests/components/rest/test_switch.py index 81430cff349..d1e4ac05514 100644 --- a/tests/components/rest/test_switch.py +++ b/tests/components/rest/test_switch.py @@ -4,9 +4,10 @@ import asyncio import aiohttp import homeassistant.components.rest.switch as rest -from homeassistant.setup import setup_component from homeassistant.helpers.template import Template -from tests.common import get_test_home_assistant, assert_setup_component +from homeassistant.setup import setup_component + +from tests.common import assert_setup_component, get_test_home_assistant class TestRestSwitchSetup: diff --git a/tests/components/rest_command/test_init.py b/tests/components/rest_command/test_init.py index b7ac5a4be8a..0aee8ccfbcc 100644 --- a/tests/components/rest_command/test_init.py +++ b/tests/components/rest_command/test_init.py @@ -6,7 +6,7 @@ import aiohttp import homeassistant.components.rest_command as rc from homeassistant.setup import setup_component -from tests.common import get_test_home_assistant, assert_setup_component +from tests.common import assert_setup_component, get_test_home_assistant class TestRestCommandSetup: @@ -236,6 +236,19 @@ class TestRestCommandComponent: }, "content_type": "text/plain", }, + "headers_template_test": { + "headers": { + "Accept": "application/json", + "User-Agent": "Mozilla/{{ 3 + 2 }}.0", + } + }, + "headers_and_content_type_override_template_test": { + "headers": { + "Accept": "application/{{ 1 + 1 }}json", + aiohttp.hdrs.CONTENT_TYPE: "application/pdf", + }, + "content_type": "text/json", + }, } } @@ -245,7 +258,7 @@ class TestRestCommandComponent: {"url": self.url, "method": "post", "payload": "test data"} ) - with assert_setup_component(5): + with assert_setup_component(7): setup_component(self.hass, rc.DOMAIN, header_config_variations) # provide post request data @@ -257,11 +270,13 @@ class TestRestCommandComponent: "headers_test", "headers_and_content_type_test", "headers_and_content_type_override_test", + "headers_template_test", + "headers_and_content_type_override_template_test", ]: self.hass.services.call(rc.DOMAIN, test_service, {}) self.hass.block_till_done() - assert len(aioclient_mock.mock_calls) == 5 + assert len(aioclient_mock.mock_calls) == 7 # no_headers_test assert aioclient_mock.mock_calls[0][3] is None @@ -293,3 +308,16 @@ class TestRestCommandComponent: == "text/plain" ) assert aioclient_mock.mock_calls[4][3].get("Accept") == "application/json" + + # headers_template_test + assert len(aioclient_mock.mock_calls[5][3]) == 2 + assert aioclient_mock.mock_calls[5][3].get("Accept") == "application/json" + assert aioclient_mock.mock_calls[5][3].get("User-Agent") == "Mozilla/5.0" + + # headers_and_content_type_override_template_test + assert len(aioclient_mock.mock_calls[6][3]) == 2 + assert ( + aioclient_mock.mock_calls[6][3].get(aiohttp.hdrs.CONTENT_TYPE) + == "text/json" + ) + assert aioclient_mock.mock_calls[6][3].get("Accept") == "application/2json" diff --git a/tests/components/rflink/test_binary_sensor.py b/tests/components/rflink/test_binary_sensor.py index d1fdec579c9..18c4f946318 100644 --- a/tests/components/rflink/test_binary_sensor.py +++ b/tests/components/rflink/test_binary_sensor.py @@ -126,7 +126,7 @@ async def test_off_delay(hass, monkeypatch): now = dt_util.utcnow() # fake time and turn on sensor future = now + timedelta(seconds=0) - with patch(("homeassistant.helpers.event." "dt_util.utcnow"), return_value=future): + with patch(("homeassistant.helpers.event.dt_util.utcnow"), return_value=future): async_fire_time_changed(hass, future) event_callback(on_event) await hass.async_block_till_done() @@ -136,7 +136,7 @@ async def test_off_delay(hass, monkeypatch): # fake time and turn on sensor again future = now + timedelta(seconds=15) - with patch(("homeassistant.helpers.event." "dt_util.utcnow"), return_value=future): + with patch(("homeassistant.helpers.event.dt_util.utcnow"), return_value=future): async_fire_time_changed(hass, future) event_callback(on_event) await hass.async_block_till_done() @@ -146,7 +146,7 @@ async def test_off_delay(hass, monkeypatch): # fake time and verify sensor still on (de-bounce) future = now + timedelta(seconds=35) - with patch(("homeassistant.helpers.event." "dt_util.utcnow"), return_value=future): + with patch(("homeassistant.helpers.event.dt_util.utcnow"), return_value=future): async_fire_time_changed(hass, future) await hass.async_block_till_done() state = hass.states.get("binary_sensor.test2") @@ -155,7 +155,7 @@ async def test_off_delay(hass, monkeypatch): # fake time and verify sensor is off future = now + timedelta(seconds=45) - with patch(("homeassistant.helpers.event." "dt_util.utcnow"), return_value=future): + with patch(("homeassistant.helpers.event.dt_util.utcnow"), return_value=future): async_fire_time_changed(hass, future) await hass.async_block_till_done() state = hass.states.get("binary_sensor.test2") diff --git a/tests/components/rflink/test_sensor.py b/tests/components/rflink/test_sensor.py index 3fea3ef6ef4..b68e1f959f1 100644 --- a/tests/components/rflink/test_sensor.py +++ b/tests/components/rflink/test_sensor.py @@ -115,8 +115,8 @@ async def test_entity_availability(hass, monkeypatch): assert hass.states.get("sensor.test").state == STATE_UNKNOWN -async def test_aliasses(hass, monkeypatch): - """Validate the response to sensor's alias (with aliasses).""" +async def test_aliases(hass, monkeypatch): + """Validate the response to sensor's alias (with aliases).""" config = { "rflink": {"port": "/dev/ttyABC0"}, DOMAIN: { @@ -125,7 +125,7 @@ async def test_aliasses(hass, monkeypatch): "test_02": { "name": "test_02", "sensor_type": "humidity", - "aliasses": ["test_alias_02_0"], + "aliases": ["test_alias_02_0"], } }, }, diff --git a/tests/components/ring/common.py b/tests/components/ring/common.py index 1228f998618..e5042a935d6 100644 --- a/tests/components/ring/common.py +++ b/tests/components/ring/common.py @@ -1,6 +1,6 @@ """Common methods used across the tests for ring devices.""" -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, CONF_SCAN_INTERVAL from homeassistant.components.ring import DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME from homeassistant.setup import async_setup_component diff --git a/tests/components/ring/conftest.py b/tests/components/ring/conftest.py index 14a29f78aae..b61840769a2 100644 --- a/tests/components/ring/conftest.py +++ b/tests/components/ring/conftest.py @@ -1,8 +1,9 @@ """Configuration for Ring tests.""" -import requests_mock -import pytest -from tests.common import load_fixture from asynctest import patch +import pytest +import requests_mock + +from tests.common import load_fixture @pytest.fixture(name="ring_mock") diff --git a/tests/components/ring/test_binary_sensor.py b/tests/components/ring/test_binary_sensor.py index d144a630863..c0b538b8eff 100644 --- a/tests/components/ring/test_binary_sensor.py +++ b/tests/components/ring/test_binary_sensor.py @@ -1,13 +1,14 @@ """The tests for the Ring binary sensor platform.""" import os import unittest + import requests_mock -from homeassistant.components.ring import binary_sensor as ring from homeassistant.components import ring as base_ring +from homeassistant.components.ring import binary_sensor as ring -from tests.components.ring.test_init import ATTRIBUTION, VALID_CONFIG from tests.common import get_test_config_dir, get_test_home_assistant, load_fixture +from tests.components.ring.test_init import ATTRIBUTION, VALID_CONFIG class TestRingBinarySensorSetup(unittest.TestCase): diff --git a/tests/components/ring/test_init.py b/tests/components/ring/test_init.py index 1a0d0dfec16..4d3fede89a9 100644 --- a/tests/components/ring/test_init.py +++ b/tests/components/ring/test_init.py @@ -1,9 +1,11 @@ """The tests for the Ring component.""" from copy import deepcopy +from datetime import timedelta import os import unittest + import requests_mock -from datetime import timedelta + from homeassistant import setup import homeassistant.components.ring as ring diff --git a/tests/components/ring/test_light.py b/tests/components/ring/test_light.py index e07867c19b2..56d39173d63 100644 --- a/tests/components/ring/test_light.py +++ b/tests/components/ring/test_light.py @@ -1,8 +1,10 @@ """The tests for the Ring light platform.""" from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN -from tests.common import load_fixture + from .common import setup_platform +from tests.common import load_fixture + async def test_entity_registry(hass, requests_mock): """Tests that the devices are registed in the entity registry.""" diff --git a/tests/components/ring/test_sensor.py b/tests/components/ring/test_sensor.py index 8559f1acc68..dd9d36f80a1 100644 --- a/tests/components/ring/test_sensor.py +++ b/tests/components/ring/test_sensor.py @@ -1,13 +1,15 @@ """The tests for the Ring sensor platform.""" import os import unittest + import requests_mock -import homeassistant.components.ring.sensor as ring from homeassistant.components import ring as base_ring +import homeassistant.components.ring.sensor as ring from homeassistant.helpers.icon import icon_for_battery_level -from tests.components.ring.test_init import ATTRIBUTION, VALID_CONFIG + from tests.common import get_test_config_dir, get_test_home_assistant, load_fixture +from tests.components.ring.test_init import ATTRIBUTION, VALID_CONFIG class TestRingSensorSetup(unittest.TestCase): diff --git a/tests/components/ring/test_switch.py b/tests/components/ring/test_switch.py index 864d16466da..15f4dd86a39 100644 --- a/tests/components/ring/test_switch.py +++ b/tests/components/ring/test_switch.py @@ -1,8 +1,10 @@ """The tests for the Ring switch platform.""" from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN -from tests.common import load_fixture + from .common import setup_platform +from tests.common import load_fixture + async def test_entity_registry(hass, requests_mock): """Tests that the devices are registed in the entity registry.""" diff --git a/tests/components/rmvtransport/test_sensor.py b/tests/components/rmvtransport/test_sensor.py index 7cb94b281d1..b34ba3d1229 100644 --- a/tests/components/rmvtransport/test_sensor.py +++ b/tests/components/rmvtransport/test_sensor.py @@ -6,7 +6,6 @@ from homeassistant.setup import async_setup_component from tests.common import mock_coro - VALID_CONFIG_MINIMAL = { "sensor": {"platform": "rmvtransport", "next_departure": [{"station": "3000010"}]} } diff --git a/tests/components/rss_feed_template/test_init.py b/tests/components/rss_feed_template/test_init.py index 294d84987b2..b07cc8aa9b3 100644 --- a/tests/components/rss_feed_template/test_init.py +++ b/tests/components/rss_feed_template/test_init.py @@ -1,7 +1,7 @@ """The tests for the rss_feed_api component.""" import asyncio -from xml.etree import ElementTree +from defusedxml import ElementTree import pytest from homeassistant.setup import async_setup_component diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index f8cef28cdb3..daa86d73bae 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -1,15 +1,16 @@ """Tests for samsungtv component.""" import asyncio -from unittest.mock import call, patch from datetime import timedelta - import logging +from unittest.mock import call, patch + from asynctest import mock import pytest from samsungctl import exceptions -from tests.common import async_fire_time_changed import wakeonlan +from websocket import WebSocketException +from homeassistant.components import samsungtv from homeassistant.components.media_player import DEVICE_CLASS_TV from homeassistant.components.media_player.const import ( ATTR_INPUT_SOURCE, @@ -17,13 +18,12 @@ from homeassistant.components.media_player.const import ( ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_VOLUME_MUTED, DOMAIN, + MEDIA_TYPE_CHANNEL, + MEDIA_TYPE_URL, SERVICE_PLAY_MEDIA, SERVICE_SELECT_SOURCE, SUPPORT_TURN_ON, - MEDIA_TYPE_CHANNEL, - MEDIA_TYPE_URL, ) -from homeassistant.components import samsungtv from homeassistant.components.samsungtv.const import DOMAIN as SAMSUNGTV_DOMAIN from homeassistant.components.samsungtv.media_player import ( CONF_TIMEOUT, @@ -54,6 +54,7 @@ from homeassistant.const import ( ) import homeassistant.util.dt as dt_util +from tests.common import async_fire_time_changed ENTITY_ID = f"{DOMAIN}.fake" MOCK_CONFIG = { @@ -370,6 +371,17 @@ async def test_send_key_unhandled_response(hass, remote): assert state.state == STATE_ON +async def test_send_key_websocketexception(hass, remote): + """Testing unhandled response exception.""" + await setup_samsungtv(hass, MOCK_CONFIG) + remote.control = mock.Mock(side_effect=WebSocketException("Boom")) + assert await hass.services.async_call( + DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_ON + + async def test_send_key_os_error(hass, remote): """Testing broken pipe Exception.""" await setup_samsungtv(hass, MOCK_CONFIG) diff --git a/tests/components/scene/common.py b/tests/components/scene/common.py index 4f8123ca638..cdf124add29 100644 --- a/tests/components/scene/common.py +++ b/tests/components/scene/common.py @@ -4,12 +4,12 @@ All containing methods are legacy helpers that should not be used by new components. Instead call the service directly. """ from homeassistant.components.scene import DOMAIN -from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON +from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, SERVICE_TURN_ON from homeassistant.loader import bind_hass @bind_hass -def activate(hass, entity_id=None): +def activate(hass, entity_id=ENTITY_MATCH_ALL): """Activate a scene.""" data = {} diff --git a/tests/components/scene/test_init.py b/tests/components/scene/test_init.py index 5c8d46cb727..f26189eec6c 100644 --- a/tests/components/scene/test_init.py +++ b/tests/components/scene/test_init.py @@ -2,8 +2,8 @@ import io import unittest -from homeassistant.setup import setup_component from homeassistant.components import light, scene +from homeassistant.setup import setup_component from homeassistant.util.yaml import loader as yaml_loader from tests.common import get_test_home_assistant diff --git a/tests/components/script/test_init.py b/tests/components/script/test_init.py index d675034e744..697154c46b2 100644 --- a/tests/components/script/test_init.py +++ b/tests/components/script/test_init.py @@ -1,7 +1,7 @@ """The tests for the Script component.""" # pylint: disable=protected-access import unittest -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch import pytest @@ -10,21 +10,20 @@ from homeassistant.components.script import DOMAIN from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_NAME, + EVENT_SCRIPT_STARTED, SERVICE_RELOAD, SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, - EVENT_SCRIPT_STARTED, ) from homeassistant.core import Context, callback, split_entity_id +from homeassistant.exceptions import ServiceNotFound from homeassistant.helpers.service import async_get_all_descriptions from homeassistant.loader import bind_hass -from homeassistant.setup import setup_component, async_setup_component -from homeassistant.exceptions import ServiceNotFound +from homeassistant.setup import async_setup_component, setup_component from tests.common import get_test_home_assistant - ENTITY_ID = "script.test" diff --git a/tests/components/season/test_sensor.py b/tests/components/season/test_sensor.py index 9d891fe0155..2acc5f6573f 100644 --- a/tests/components/season/test_sensor.py +++ b/tests/components/season/test_sensor.py @@ -1,14 +1,13 @@ """The tests for the Season sensor platform.""" # pylint: disable=protected-access -import unittest from datetime import datetime +import unittest -from homeassistant.setup import setup_component import homeassistant.components.season.sensor as season +from homeassistant.setup import setup_component from tests.common import get_test_home_assistant - HEMISPHERE_NORTHERN = { "homeassistant": {"latitude": "48.864716", "longitude": "2.349014"}, "sensor": {"platform": "season", "type": "astronomical"}, diff --git a/tests/components/sensor/test_device_condition.py b/tests/components/sensor/test_device_condition.py index f3ff15c3ad9..bd6a6ce4928 100644 --- a/tests/components/sensor/test_device_condition.py +++ b/tests/components/sensor/test_device_condition.py @@ -1,20 +1,20 @@ """The test for sensor device automation.""" import pytest +import homeassistant.components.automation as automation from homeassistant.components.sensor import DOMAIN from homeassistant.components.sensor.device_condition import ENTITY_CONDITIONS -from homeassistant.const import STATE_UNKNOWN, CONF_PLATFORM -from homeassistant.setup import async_setup_component -import homeassistant.components.automation as automation +from homeassistant.const import CONF_PLATFORM, STATE_UNKNOWN from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, + async_get_device_automation_capabilities, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, - async_get_device_automations, - async_get_device_automation_capabilities, ) from tests.testing_config.custom_components.test.sensor import DEVICE_CLASSES diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py index b7a921fff18..7bb69388c1d 100644 --- a/tests/components/sensor/test_device_trigger.py +++ b/tests/components/sensor/test_device_trigger.py @@ -1,23 +1,24 @@ """The test for sensor device automation.""" from datetime import timedelta + import pytest +import homeassistant.components.automation as automation from homeassistant.components.sensor import DOMAIN from homeassistant.components.sensor.device_trigger import ENTITY_TRIGGERS -from homeassistant.const import STATE_UNKNOWN, CONF_PLATFORM -from homeassistant.setup import async_setup_component -import homeassistant.components.automation as automation +from homeassistant.const import CONF_PLATFORM, STATE_UNKNOWN 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, async_fire_time_changed, + async_get_device_automation_capabilities, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, - async_get_device_automations, - async_get_device_automation_capabilities, ) from tests.testing_config.custom_components.test.sensor import DEVICE_CLASSES diff --git a/tests/components/seventeentrack/test_sensor.py b/tests/components/seventeentrack/test_sensor.py index dff3cc40b9e..10ec22f8b67 100644 --- a/tests/components/seventeentrack/test_sensor.py +++ b/tests/components/seventeentrack/test_sensor.py @@ -2,17 +2,18 @@ import datetime from typing import Union -import pytest import mock from py17track.package import Package +import pytest from homeassistant.components.seventeentrack.sensor import ( CONF_SHOW_ARCHIVED, CONF_SHOW_DELIVERED, ) -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.setup import async_setup_component from homeassistant.util import utcnow + from tests.common import MockDependency, async_fire_time_changed VALID_CONFIG_MINIMAL = { @@ -119,7 +120,10 @@ def fixture_mock_py17track(): @pytest.fixture(autouse=True, name="mock_client") def fixture_mock_client(mock_py17track): """Mock py17track client.""" - with mock.patch("py17track.Client", new=ClientMock): + with mock.patch( + "homeassistant.components.seventeentrack.sensor.SeventeenTrackClient", + new=ClientMock, + ): yield ProfileMock.reset() diff --git a/tests/components/shell_command/test_init.py b/tests/components/shell_command/test_init.py index 13899da9a3e..50c3c6bfb55 100644 --- a/tests/components/shell_command/test_init.py +++ b/tests/components/shell_command/test_init.py @@ -2,12 +2,12 @@ import asyncio import os import tempfile -import unittest from typing import Tuple +import unittest from unittest.mock import Mock, patch -from homeassistant.setup import setup_component from homeassistant.components import shell_command +from homeassistant.setup import setup_component from tests.common import get_test_home_assistant @@ -111,7 +111,7 @@ class TestShellCommand(unittest.TestCase): shell_command.DOMAIN, { shell_command.DOMAIN: { - "test_service": ("ls /bin {{ states.sensor" ".test_state.state }}") + "test_service": ("ls /bin {{ states.sensor.test_state.state }}") } }, ) diff --git a/tests/components/shopping_list/conftest.py b/tests/components/shopping_list/conftest.py new file mode 100644 index 00000000000..44c8000efa2 --- /dev/null +++ b/tests/components/shopping_list/conftest.py @@ -0,0 +1,23 @@ +"""Shopping list test helpers.""" +from unittest.mock import patch + +import pytest + +from homeassistant.components.shopping_list import intent as sl_intent +from homeassistant.setup import async_setup_component + + +@pytest.fixture(autouse=True) +def mock_shopping_list_io(): + """Stub out the persistence.""" + with patch("homeassistant.components.shopping_list.ShoppingData.save"), patch( + "homeassistant.components.shopping_list.ShoppingData.async_load" + ): + yield + + +@pytest.fixture +async def sl_setup(hass): + """Set up the shopping list.""" + assert await async_setup_component(hass, "shopping_list", {}) + await sl_intent.async_setup_intents(hass) diff --git a/tests/components/shopping_list/test_init.py b/tests/components/shopping_list/test_init.py index 1d42fa60d9c..74c354848a3 100644 --- a/tests/components/shopping_list/test_init.py +++ b/tests/components/shopping_list/test_init.py @@ -1,27 +1,13 @@ """Test shopping list component.""" import asyncio -from unittest.mock import patch -import pytest - -from homeassistant.bootstrap import async_setup_component -from homeassistant.helpers import intent from homeassistant.components.websocket_api.const import TYPE_RESULT - - -@pytest.fixture(autouse=True) -def mock_shopping_list_io(): - """Stub out the persistence.""" - with patch("homeassistant.components.shopping_list.ShoppingData.save"), patch( - "homeassistant.components.shopping_list." "ShoppingData.async_load" - ): - yield +from homeassistant.helpers import intent @asyncio.coroutine -def test_add_item(hass): +def test_add_item(hass, sl_setup): """Test adding an item intent.""" - yield from async_setup_component(hass, "shopping_list", {}) response = yield from intent.async_handle( hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} @@ -31,9 +17,8 @@ def test_add_item(hass): @asyncio.coroutine -def test_recent_items_intent(hass): +def test_recent_items_intent(hass, sl_setup): """Test recent items.""" - yield from async_setup_component(hass, "shopping_list", {}) yield from intent.async_handle( hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} @@ -54,9 +39,8 @@ def test_recent_items_intent(hass): @asyncio.coroutine -def test_deprecated_api_get_all(hass, hass_client): +def test_deprecated_api_get_all(hass, hass_client, sl_setup): """Test the API.""" - yield from async_setup_component(hass, "shopping_list", {}) yield from intent.async_handle( hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} @@ -77,9 +61,8 @@ def test_deprecated_api_get_all(hass, hass_client): assert not data[1]["complete"] -async def test_ws_get_items(hass, hass_ws_client): +async def test_ws_get_items(hass, hass_ws_client, sl_setup): """Test get shopping_list items websocket command.""" - await async_setup_component(hass, "shopping_list", {}) await intent.async_handle( hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} @@ -106,9 +89,8 @@ async def test_ws_get_items(hass, hass_ws_client): @asyncio.coroutine -def test_deprecated_api_update(hass, hass_client): +def test_deprecated_api_update(hass, hass_client, sl_setup): """Test the API.""" - yield from async_setup_component(hass, "shopping_list", {}) yield from intent.async_handle( hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} @@ -142,9 +124,8 @@ def test_deprecated_api_update(hass, hass_client): assert wine == {"id": wine_id, "name": "wine", "complete": True} -async def test_ws_update_item(hass, hass_ws_client): +async def test_ws_update_item(hass, hass_ws_client, sl_setup): """Test update shopping_list item websocket command.""" - await async_setup_component(hass, "shopping_list", {}) await intent.async_handle( hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} ) @@ -186,9 +167,8 @@ async def test_ws_update_item(hass, hass_ws_client): @asyncio.coroutine -def test_api_update_fails(hass, hass_client): +def test_api_update_fails(hass, hass_client, sl_setup): """Test the API.""" - yield from async_setup_component(hass, "shopping_list", {}) yield from intent.async_handle( hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} @@ -209,9 +189,8 @@ def test_api_update_fails(hass, hass_client): assert resp.status == 400 -async def test_ws_update_item_fail(hass, hass_ws_client): +async def test_ws_update_item_fail(hass, hass_ws_client, sl_setup): """Test failure of update shopping_list item websocket command.""" - await async_setup_component(hass, "shopping_list", {}) await intent.async_handle( hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} ) @@ -234,9 +213,8 @@ async def test_ws_update_item_fail(hass, hass_ws_client): @asyncio.coroutine -def test_deprecated_api_clear_completed(hass, hass_client): +def test_deprecated_api_clear_completed(hass, hass_client, sl_setup): """Test the API.""" - yield from async_setup_component(hass, "shopping_list", {}) yield from intent.async_handle( hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} @@ -265,9 +243,8 @@ def test_deprecated_api_clear_completed(hass, hass_client): assert items[0] == {"id": wine_id, "name": "wine", "complete": False} -async def test_ws_clear_items(hass, hass_ws_client): +async def test_ws_clear_items(hass, hass_ws_client, sl_setup): """Test clearing shopping_list items websocket command.""" - await async_setup_component(hass, "shopping_list", {}) await intent.async_handle( hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} ) @@ -296,9 +273,8 @@ async def test_ws_clear_items(hass, hass_ws_client): @asyncio.coroutine -def test_deprecated_api_create(hass, hass_client): +def test_deprecated_api_create(hass, hass_client, sl_setup): """Test the API.""" - yield from async_setup_component(hass, "shopping_list", {}) client = yield from hass_client() resp = yield from client.post("/api/shopping_list/item", json={"name": "soda"}) @@ -315,9 +291,8 @@ def test_deprecated_api_create(hass, hass_client): @asyncio.coroutine -def test_deprecated_api_create_fail(hass, hass_client): +def test_deprecated_api_create_fail(hass, hass_client, sl_setup): """Test the API.""" - yield from async_setup_component(hass, "shopping_list", {}) client = yield from hass_client() resp = yield from client.post("/api/shopping_list/item", json={"name": 1234}) @@ -326,9 +301,8 @@ def test_deprecated_api_create_fail(hass, hass_client): assert len(hass.data["shopping_list"].items) == 0 -async def test_ws_add_item(hass, hass_ws_client): +async def test_ws_add_item(hass, hass_ws_client, sl_setup): """Test adding shopping_list item websocket command.""" - await async_setup_component(hass, "shopping_list", {}) client = await hass_ws_client(hass) await client.send_json({"id": 5, "type": "shopping_list/items/add", "name": "soda"}) msg = await client.receive_json() @@ -342,9 +316,8 @@ async def test_ws_add_item(hass, hass_ws_client): assert items[0]["complete"] is False -async def test_ws_add_item_fail(hass, hass_ws_client): +async def test_ws_add_item_fail(hass, hass_ws_client, sl_setup): """Test adding shopping_list item failure websocket command.""" - await async_setup_component(hass, "shopping_list", {}) client = await hass_ws_client(hass) await client.send_json({"id": 5, "type": "shopping_list/items/add", "name": 123}) msg = await client.receive_json() diff --git a/tests/components/shopping_list/test_intent.py b/tests/components/shopping_list/test_intent.py new file mode 100644 index 00000000000..d0bcb1d837c --- /dev/null +++ b/tests/components/shopping_list/test_intent.py @@ -0,0 +1,22 @@ +"""Test Shopping List intents.""" +from homeassistant.helpers import intent + + +async def test_recent_items_intent(hass, sl_setup): + """Test recent items.""" + await intent.async_handle( + hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} + ) + await intent.async_handle( + hass, "test", "HassShoppingListAddItem", {"item": {"value": "wine"}} + ) + await intent.async_handle( + hass, "test", "HassShoppingListAddItem", {"item": {"value": "soda"}} + ) + + response = await intent.async_handle(hass, "test", "HassShoppingListLastItems") + + assert ( + response.speech["plain"]["speech"] + == "These are the top 3 items on your shopping list: soda, wine, beer" + ) diff --git a/tests/components/sigfox/test_sensor.py b/tests/components/sigfox/test_sensor.py index eac1e6c2582..35534a3a126 100644 --- a/tests/components/sigfox/test_sensor.py +++ b/tests/components/sigfox/test_sensor.py @@ -1,14 +1,16 @@ """Tests for the sigfox sensor.""" import re -import requests_mock import unittest +import requests_mock + from homeassistant.components.sigfox.sensor import ( API_URL, CONF_API_LOGIN, CONF_API_PASSWORD, ) from homeassistant.setup import setup_component + from tests.common import get_test_home_assistant TEST_API_LOGIN = "foo" diff --git a/tests/components/simplisafe/test_config_flow.py b/tests/components/simplisafe/test_config_flow.py index c0920a738ee..a7a21c577d6 100644 --- a/tests/components/simplisafe/test_config_flow.py +++ b/tests/components/simplisafe/test_config_flow.py @@ -1,7 +1,7 @@ """Define tests for the SimpliSafe config flow.""" -import json from datetime import timedelta -from unittest.mock import mock_open, patch, MagicMock, PropertyMock +import json +from unittest.mock import MagicMock, PropertyMock, mock_open, patch from homeassistant import data_entry_flow from homeassistant.components.simplisafe import DOMAIN, config_flow diff --git a/tests/components/simulated/test_sensor.py b/tests/components/simulated/test_sensor.py index 14d839ee656..09e77f7b283 100644 --- a/tests/components/simulated/test_sensor.py +++ b/tests/components/simulated/test_sensor.py @@ -1,28 +1,28 @@ """The tests for the simulated sensor.""" import unittest -from tests.common import get_test_home_assistant - from homeassistant.components.simulated.sensor import ( CONF_AMP, CONF_FWHM, CONF_MEAN, CONF_PERIOD, CONF_PHASE, + CONF_RELATIVE_TO_EPOCH, CONF_SEED, CONF_UNIT, - CONF_RELATIVE_TO_EPOCH, DEFAULT_AMP, DEFAULT_FWHM, DEFAULT_MEAN, DEFAULT_NAME, DEFAULT_PHASE, - DEFAULT_SEED, DEFAULT_RELATIVE_TO_EPOCH, + DEFAULT_SEED, ) from homeassistant.const import CONF_FRIENDLY_NAME from homeassistant.setup import setup_component +from tests.common import get_test_home_assistant + class TestSimulatedSensor(unittest.TestCase): """Test the simulated sensor.""" diff --git a/tests/components/sleepiq/test_binary_sensor.py b/tests/components/sleepiq/test_binary_sensor.py index a3bf31134c5..b8c3a2cd2e8 100644 --- a/tests/components/sleepiq/test_binary_sensor.py +++ b/tests/components/sleepiq/test_binary_sensor.py @@ -4,11 +4,11 @@ from unittest.mock import MagicMock import requests_mock -from homeassistant.setup import setup_component from homeassistant.components.sleepiq import binary_sensor as sleepiq +from homeassistant.setup import setup_component -from tests.components.sleepiq.test_init import mock_responses from tests.common import get_test_home_assistant +from tests.components.sleepiq.test_init import mock_responses class TestSleepIQBinarySensorSetup(unittest.TestCase): diff --git a/tests/components/sleepiq/test_init.py b/tests/components/sleepiq/test_init.py index a418253d409..67fe19da45a 100644 --- a/tests/components/sleepiq/test_init.py +++ b/tests/components/sleepiq/test_init.py @@ -7,7 +7,7 @@ import requests_mock from homeassistant import setup import homeassistant.components.sleepiq as sleepiq -from tests.common import load_fixture, get_test_home_assistant +from tests.common import get_test_home_assistant, load_fixture def mock_responses(mock, single=False): diff --git a/tests/components/sleepiq/test_sensor.py b/tests/components/sleepiq/test_sensor.py index 9dbba3a8b0a..a049dfd2fbf 100644 --- a/tests/components/sleepiq/test_sensor.py +++ b/tests/components/sleepiq/test_sensor.py @@ -4,11 +4,11 @@ from unittest.mock import MagicMock import requests_mock -from homeassistant.setup import setup_component import homeassistant.components.sleepiq.sensor as sleepiq +from homeassistant.setup import setup_component -from tests.components.sleepiq.test_init import mock_responses from tests.common import get_test_home_assistant +from tests.components.sleepiq.test_init import mock_responses class TestSleepIQSensorSetup(unittest.TestCase): diff --git a/tests/components/smartthings/conftest.py b/tests/components/smartthings/conftest.py index b3b172e7606..0dc71ea72b9 100644 --- a/tests/components/smartthings/conftest.py +++ b/tests/components/smartthings/conftest.py @@ -1,4 +1,5 @@ """Test configuration and mocks for the SmartThings component.""" +import secrets from uuid import uuid4 from asynctest import Mock, patch @@ -160,7 +161,7 @@ def installed_apps_fixture(installed_app, locations, app): @pytest.fixture(name="config_file") def config_file_fixture(): """Fixture representing the local config file contents.""" - return {CONF_INSTANCE_ID: str(uuid4()), CONF_WEBHOOK_ID: webhook.generate_secret()} + return {CONF_INSTANCE_ID: str(uuid4()), CONF_WEBHOOK_ID: secrets.token_hex()} @pytest.fixture(name="smartthings_mock") diff --git a/tests/components/smartthings/test_climate.py b/tests/components/smartthings/test_climate.py index 630174a0661..79919a376cd 100644 --- a/tests/components/smartthings/test_climate.py +++ b/tests/components/smartthings/test_climate.py @@ -551,7 +551,9 @@ async def test_set_turn_off(hass, air_conditioner): await setup_platform(hass, CLIMATE_DOMAIN, devices=[air_conditioner]) state = hass.states.get("climate.air_conditioner") assert state.state == HVAC_MODE_HEAT_COOL - await hass.services.async_call(CLIMATE_DOMAIN, SERVICE_TURN_OFF, blocking=True) + await hass.services.async_call( + CLIMATE_DOMAIN, SERVICE_TURN_OFF, {"entity_id": "all"}, blocking=True + ) state = hass.states.get("climate.air_conditioner") assert state.state == HVAC_MODE_OFF @@ -562,7 +564,9 @@ async def test_set_turn_on(hass, air_conditioner): await setup_platform(hass, CLIMATE_DOMAIN, devices=[air_conditioner]) state = hass.states.get("climate.air_conditioner") assert state.state == HVAC_MODE_OFF - await hass.services.async_call(CLIMATE_DOMAIN, SERVICE_TURN_ON, blocking=True) + await hass.services.async_call( + CLIMATE_DOMAIN, SERVICE_TURN_ON, {"entity_id": "all"}, blocking=True + ) state = hass.states.get("climate.air_conditioner") assert state.state == HVAC_MODE_HEAT_COOL diff --git a/tests/components/smartthings/test_config_flow.py b/tests/components/smartthings/test_config_flow.py index 521f1c6a6a8..f299727b948 100644 --- a/tests/components/smartthings/test_config_flow.py +++ b/tests/components/smartthings/test_config_flow.py @@ -6,8 +6,6 @@ from asynctest import Mock, patch from pysmartthings import APIResponseError from homeassistant import data_entry_flow -from homeassistant.setup import async_setup_component -from homeassistant.components import cloud from homeassistant.components.smartthings import smartapp from homeassistant.components.smartthings.config_flow import SmartThingsFlowHandler from homeassistant.components.smartthings.const import ( @@ -17,8 +15,9 @@ from homeassistant.components.smartthings.const import ( CONF_REFRESH_TOKEN, DOMAIN, ) +from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, mock_coro async def test_step_user(hass): @@ -211,9 +210,11 @@ async def test_cloudhook_app_created_then_show_wait_form( await smartapp.unload_smartapp_endpoint(hass) with patch.object( - cloud, "async_active_subscription", return_value=True + hass.components.cloud, "async_active_subscription", return_value=True ), patch.object( - cloud, "async_create_cloudhook", return_value="http://cloud.test" + hass.components.cloud, + "async_create_cloudhook", + return_value=mock_coro("http://cloud.test"), ) as mock_create_cloudhook: await smartapp.setup_smartapp_endpoint(hass) diff --git a/tests/components/smartthings/test_cover.py b/tests/components/smartthings/test_cover.py index 19a2ec3463a..26b68c0cb1f 100644 --- a/tests/components/smartthings/test_cover.py +++ b/tests/components/smartthings/test_cover.py @@ -114,7 +114,10 @@ async def test_set_cover_position(hass, device_factory): await setup_platform(hass, COVER_DOMAIN, devices=[device]) # Act await hass.services.async_call( - COVER_DOMAIN, SERVICE_SET_COVER_POSITION, {ATTR_POSITION: 50}, blocking=True + COVER_DOMAIN, + SERVICE_SET_COVER_POSITION, + {ATTR_POSITION: 50, "entity_id": "all"}, + blocking=True, ) state = hass.states.get("cover.shade") @@ -136,7 +139,10 @@ async def test_set_cover_position_unsupported(hass, device_factory): await setup_platform(hass, COVER_DOMAIN, devices=[device]) # Act await hass.services.async_call( - COVER_DOMAIN, SERVICE_SET_COVER_POSITION, {ATTR_POSITION: 50}, blocking=True + COVER_DOMAIN, + SERVICE_SET_COVER_POSITION, + {"entity_id": "all", ATTR_POSITION: 50}, + blocking=True, ) state = hass.states.get("cover.shade") diff --git a/tests/components/smartthings/test_init.py b/tests/components/smartthings/test_init.py index b8cd65f5a0b..0c9d889d558 100644 --- a/tests/components/smartthings/test_init.py +++ b/tests/components/smartthings/test_init.py @@ -6,7 +6,6 @@ from asynctest import Mock, patch from pysmartthings import InstalledAppStatus, OAuthToken import pytest -from homeassistant.setup import async_setup_component from homeassistant.components import cloud, smartthings from homeassistant.components.smartthings.const import ( CONF_CLOUDHOOK_URL, @@ -20,6 +19,7 @@ from homeassistant.components.smartthings.const import ( ) from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry diff --git a/tests/components/smhi/common.py b/tests/components/smhi/common.py index 74c8a99b51f..6f215840324 100644 --- a/tests/components/smhi/common.py +++ b/tests/components/smhi/common.py @@ -5,7 +5,7 @@ from unittest.mock import Mock class AsyncMock(Mock): """Implements Mock async.""" - # pylint: disable=W0235 + # pylint: disable=useless-super-delegation async def __call__(self, *args, **kwargs): """Hack for async support for Mock.""" return super().__call__(*args, **kwargs) diff --git a/tests/components/smhi/test_config_flow.py b/tests/components/smhi/test_config_flow.py index ab317fb829a..ceccd75e08d 100644 --- a/tests/components/smhi/test_config_flow.py +++ b/tests/components/smhi/test_config_flow.py @@ -3,13 +3,13 @@ from unittest.mock import Mock, patch from smhi.smhi_lib import Smhi as SmhiApi, SmhiForecastException +from homeassistant.components.smhi import config_flow +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE + from tests.common import mock_coro -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE -from homeassistant.components.smhi import config_flow - -# pylint: disable=W0212 +# pylint: disable=protected-access async def test_homeassistant_location_exists() -> None: """Test if homeassistant location exists it should return True.""" hass = Mock() diff --git a/tests/components/smhi/test_weather.py b/tests/components/smhi/test_weather.py index 76b32895758..92557f9d543 100644 --- a/tests/components/smhi/test_weather.py +++ b/tests/components/smhi/test_weather.py @@ -1,31 +1,30 @@ """Test for the smhi weather entity.""" import asyncio -import logging from datetime import datetime +import logging from unittest.mock import Mock, patch +from homeassistant.components.smhi import weather as weather_smhi +from homeassistant.components.smhi.const import ATTR_SMHI_CLOUDINESS from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, + ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_TEMP, + ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, - ATTR_WEATHER_TEMPERATURE, + ATTR_WEATHER_ATTRIBUTION, ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_PRESSURE, - ATTR_FORECAST_TEMP_LOW, + ATTR_WEATHER_TEMPERATURE, ATTR_WEATHER_VISIBILITY, - ATTR_WEATHER_ATTRIBUTION, ATTR_WEATHER_WIND_BEARING, ATTR_WEATHER_WIND_SPEED, - ATTR_FORECAST_PRECIPITATION, DOMAIN as WEATHER_DOMAIN, ) -from homeassistant.components.smhi import weather as weather_smhi from homeassistant.const import TEMP_CELSIUS from homeassistant.core import HomeAssistant -from tests.common import load_fixture, MockConfigEntry - -from homeassistant.components.smhi.const import ATTR_SMHI_CLOUDINESS +from tests.common import MockConfigEntry, load_fixture _LOGGER = logging.getLogger(__name__) @@ -99,7 +98,7 @@ def test_properties_no_data(hass: HomeAssistant) -> None: assert weather.temperature_unit == TEMP_CELSIUS -# pylint: disable=W0212 +# pylint: disable=protected-access def test_properties_unknown_symbol() -> None: """Test behaviour when unknown symbol from API.""" hass = Mock() @@ -152,7 +151,7 @@ def test_properties_unknown_symbol() -> None: assert forecast[ATTR_FORECAST_CONDITION] is None -# pylint: disable=W0212 +# pylint: disable=protected-access async def test_refresh_weather_forecast_exceeds_retries(hass) -> None: """Test the refresh weather forecast function.""" from smhi.smhi_lib import SmhiForecastException diff --git a/tests/components/smtp/test_notify.py b/tests/components/smtp/test_notify.py index daef7ef130e..c79633dd02d 100644 --- a/tests/components/smtp/test_notify.py +++ b/tests/components/smtp/test_notify.py @@ -1,11 +1,11 @@ """The tests for the notify smtp platform.""" +import re import unittest from unittest.mock import patch from homeassistant.components.smtp.notify import MailNotificationService from tests.common import get_test_home_assistant -import re class MockSMTP(MailNotificationService): diff --git a/tests/components/snips/test_init.py b/tests/components/snips/test_init.py index fa6fbe0b254..40fb30ddd19 100644 --- a/tests/components/snips/test_init.py +++ b/tests/components/snips/test_init.py @@ -9,11 +9,12 @@ from homeassistant.bootstrap import async_setup_component from homeassistant.components.mqtt import MQTT_PUBLISH_SCHEMA import homeassistant.components.snips as snips from homeassistant.helpers.intent import ServiceIntentHandler, async_register + from tests.common import ( async_fire_mqtt_message, async_mock_intent, - async_mock_service, async_mock_mqtt_component, + async_mock_service, ) diff --git a/tests/components/solaredge/test_config_flow.py b/tests/components/solaredge/test_config_flow.py index c1183147bac..46f40dd80ef 100644 --- a/tests/components/solaredge/test_config_flow.py +++ b/tests/components/solaredge/test_config_flow.py @@ -1,12 +1,13 @@ """Tests for the SolarEdge config flow.""" +from unittest.mock import Mock, patch + import pytest -from requests.exceptions import HTTPError, ConnectTimeout -from unittest.mock import patch, Mock +from requests.exceptions import ConnectTimeout, HTTPError from homeassistant import data_entry_flow from homeassistant.components.solaredge import config_flow from homeassistant.components.solaredge.const import CONF_SITE_ID, DEFAULT_NAME -from homeassistant.const import CONF_NAME, CONF_API_KEY +from homeassistant.const import CONF_API_KEY, CONF_NAME from tests.common import MockConfigEntry diff --git a/tests/components/solarlog/test_config_flow.py b/tests/components/solarlog/test_config_flow.py index 86f3b05d975..cd05cf13185 100644 --- a/tests/components/solarlog/test_config_flow.py +++ b/tests/components/solarlog/test_config_flow.py @@ -1,9 +1,9 @@ """Test the solarlog config flow.""" from unittest.mock import patch + import pytest -from homeassistant import data_entry_flow -from homeassistant import config_entries, setup +from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.solarlog import config_flow from homeassistant.components.solarlog.const import DEFAULT_HOST, DOMAIN from homeassistant.const import CONF_HOST, CONF_NAME diff --git a/tests/components/soma/test_config_flow.py b/tests/components/soma/test_config_flow.py index 764a18d1b8b..1d00f83a608 100644 --- a/tests/components/soma/test_config_flow.py +++ b/tests/components/soma/test_config_flow.py @@ -5,9 +5,9 @@ from api.soma_api import SomaApi from requests import RequestException from homeassistant import data_entry_flow -from homeassistant.components.soma import config_flow, DOMAIN -from tests.common import MockConfigEntry +from homeassistant.components.soma import DOMAIN, config_flow +from tests.common import MockConfigEntry MOCK_HOST = "123.45.67.89" MOCK_PORT = 3000 @@ -35,11 +35,31 @@ async def test_import_create(hass): """Test configuration from YAML.""" flow = config_flow.SomaFlowHandler() flow.hass = hass - with patch.object(SomaApi, "list_devices", return_value={}): + with patch.object(SomaApi, "list_devices", return_value={"result": "success"}): result = await flow.async_step_import({"host": MOCK_HOST, "port": MOCK_PORT}) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY +async def test_error_status(hass): + """Test Connect successfully returning error status.""" + flow = config_flow.SomaFlowHandler() + flow.hass = hass + with patch.object(SomaApi, "list_devices", return_value={"result": "error"}): + result = await flow.async_step_import({"host": MOCK_HOST, "port": MOCK_PORT}) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "result_error" + + +async def test_key_error(hass): + """Test Connect returning empty string.""" + flow = config_flow.SomaFlowHandler() + flow.hass = hass + with patch.object(SomaApi, "list_devices", return_value={}): + result = await flow.async_step_import({"host": MOCK_HOST, "port": MOCK_PORT}) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "connection_error" + + async def test_exception(hass): """Test if RequestException fires when no connection can be made.""" flow = config_flow.SomaFlowHandler() @@ -55,6 +75,6 @@ async def test_full_flow(hass): hass.data[DOMAIN] = {} flow = config_flow.SomaFlowHandler() flow.hass = hass - with patch.object(SomaApi, "list_devices", return_value={}): + with patch.object(SomaApi, "list_devices", return_value={"result": "success"}): result = await flow.async_step_user({"host": MOCK_HOST, "port": MOCK_PORT}) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY diff --git a/tests/components/somfy/test_config_flow.py b/tests/components/somfy/test_config_flow.py index d42e7b8e367..f195b640240 100644 --- a/tests/components/somfy/test_config_flow.py +++ b/tests/components/somfy/test_config_flow.py @@ -4,8 +4,8 @@ from unittest.mock import patch import pytest -from homeassistant import data_entry_flow, setup, config_entries -from homeassistant.components.somfy import config_flow, DOMAIN +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.somfy import DOMAIN, config_flow from homeassistant.helpers import config_entry_oauth2_flow from tests.common import MockConfigEntry diff --git a/tests/components/sonarr/test_sensor.py b/tests/components/sonarr/test_sensor.py index 43a53eeadcc..38382dc70ab 100644 --- a/tests/components/sonarr/test_sensor.py +++ b/tests/components/sonarr/test_sensor.py @@ -1,7 +1,7 @@ """The tests for the Sonarr platform.""" -import unittest -import time from datetime import datetime +import time +import unittest import pytest diff --git a/tests/components/soundtouch/test_media_player.py b/tests/components/soundtouch/test_media_player.py index bf6d2f72b4a..8789db1ca1f 100644 --- a/tests/components/soundtouch/test_media_player.py +++ b/tests/components/soundtouch/test_media_player.py @@ -2,10 +2,12 @@ import logging import unittest from unittest import mock -from libsoundtouch.device import SoundTouchDevice as STD, Status, Volume, Preset, Config + +from libsoundtouch.device import Config, Preset, SoundTouchDevice as STD, Status, Volume from homeassistant.components.soundtouch import media_player as soundtouch from homeassistant.const import STATE_OFF, STATE_PAUSED, STATE_PLAYING + from tests.common import get_test_home_assistant @@ -148,7 +150,10 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): logging.disable(logging.NOTSET) self.hass.stop() - @mock.patch("libsoundtouch.soundtouch_device", side_effect=None) + @mock.patch( + "homeassistant.components.soundtouch.media_player.soundtouch_device", + side_effect=None, + ) def test_ensure_setup_config(self, mocked_soundtouch_device): """Test setup OK with custom config.""" soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) @@ -158,7 +163,10 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): assert all_devices[0].config["port"] == 8090 assert mocked_soundtouch_device.call_count == 1 - @mock.patch("libsoundtouch.soundtouch_device", side_effect=None) + @mock.patch( + "homeassistant.components.soundtouch.media_player.soundtouch_device", + side_effect=None, + ) def test_ensure_setup_discovery(self, mocked_soundtouch_device): """Test setup with discovery.""" new_device = { @@ -174,7 +182,10 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): assert all_devices[0].config["host"] == "192.168.1.1" assert mocked_soundtouch_device.call_count == 1 - @mock.patch("libsoundtouch.soundtouch_device", side_effect=None) + @mock.patch( + "homeassistant.components.soundtouch.media_player.soundtouch_device", + side_effect=None, + ) def test_ensure_setup_discovery_no_duplicate(self, mocked_soundtouch_device): """Test setup OK if device already exists.""" soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) @@ -203,7 +214,10 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): @mock.patch("libsoundtouch.device.SoundTouchDevice.volume") @mock.patch("libsoundtouch.device.SoundTouchDevice.status") - @mock.patch("libsoundtouch.soundtouch_device", side_effect=_mock_soundtouch_device) + @mock.patch( + "homeassistant.components.soundtouch.media_player.soundtouch_device", + side_effect=_mock_soundtouch_device, + ) def test_update(self, mocked_soundtouch_device, mocked_status, mocked_volume): """Test update device state.""" soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) @@ -218,7 +232,10 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): @mock.patch( "libsoundtouch.device.SoundTouchDevice.status", side_effect=MockStatusPlaying ) - @mock.patch("libsoundtouch.soundtouch_device", side_effect=_mock_soundtouch_device) + @mock.patch( + "homeassistant.components.soundtouch.media_player.soundtouch_device", + side_effect=_mock_soundtouch_device, + ) def test_playing_media( self, mocked_soundtouch_device, mocked_status, mocked_volume ): @@ -240,7 +257,10 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): @mock.patch( "libsoundtouch.device.SoundTouchDevice.status", side_effect=MockStatusUnknown ) - @mock.patch("libsoundtouch.soundtouch_device", side_effect=_mock_soundtouch_device) + @mock.patch( + "homeassistant.components.soundtouch.media_player.soundtouch_device", + side_effect=_mock_soundtouch_device, + ) def test_playing_unknown_media( self, mocked_soundtouch_device, mocked_status, mocked_volume ): @@ -257,7 +277,10 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): "libsoundtouch.device.SoundTouchDevice.status", side_effect=MockStatusPlayingRadio, ) - @mock.patch("libsoundtouch.soundtouch_device", side_effect=_mock_soundtouch_device) + @mock.patch( + "homeassistant.components.soundtouch.media_player.soundtouch_device", + side_effect=_mock_soundtouch_device, + ) def test_playing_radio( self, mocked_soundtouch_device, mocked_status, mocked_volume ): @@ -277,7 +300,10 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): @mock.patch("libsoundtouch.device.SoundTouchDevice.volume", side_effect=MockVolume) @mock.patch("libsoundtouch.device.SoundTouchDevice.status") - @mock.patch("libsoundtouch.soundtouch_device", side_effect=_mock_soundtouch_device) + @mock.patch( + "homeassistant.components.soundtouch.media_player.soundtouch_device", + side_effect=_mock_soundtouch_device, + ) def test_get_volume_level( self, mocked_soundtouch_device, mocked_status, mocked_volume ): @@ -293,7 +319,10 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): @mock.patch( "libsoundtouch.device.SoundTouchDevice.status", side_effect=MockStatusStandby ) - @mock.patch("libsoundtouch.soundtouch_device", side_effect=_mock_soundtouch_device) + @mock.patch( + "homeassistant.components.soundtouch.media_player.soundtouch_device", + side_effect=_mock_soundtouch_device, + ) def test_get_state_off( self, mocked_soundtouch_device, mocked_status, mocked_volume ): @@ -309,7 +338,10 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): @mock.patch( "libsoundtouch.device.SoundTouchDevice.status", side_effect=MockStatusPause ) - @mock.patch("libsoundtouch.soundtouch_device", side_effect=_mock_soundtouch_device) + @mock.patch( + "homeassistant.components.soundtouch.media_player.soundtouch_device", + side_effect=_mock_soundtouch_device, + ) def test_get_state_pause( self, mocked_soundtouch_device, mocked_status, mocked_volume ): @@ -325,7 +357,10 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): "libsoundtouch.device.SoundTouchDevice.volume", side_effect=MockVolumeMuted ) @mock.patch("libsoundtouch.device.SoundTouchDevice.status") - @mock.patch("libsoundtouch.soundtouch_device", side_effect=_mock_soundtouch_device) + @mock.patch( + "homeassistant.components.soundtouch.media_player.soundtouch_device", + side_effect=_mock_soundtouch_device, + ) def test_is_muted(self, mocked_soundtouch_device, mocked_status, mocked_volume): """Test device volume is muted.""" soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) @@ -335,7 +370,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] assert all_devices[0].is_volume_muted is True - @mock.patch("libsoundtouch.soundtouch_device") + @mock.patch("homeassistant.components.soundtouch.media_player.soundtouch_device") def test_media_commands(self, mocked_soundtouch_device): """Test supported media commands.""" soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) @@ -346,7 +381,10 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): @mock.patch("libsoundtouch.device.SoundTouchDevice.power_off") @mock.patch("libsoundtouch.device.SoundTouchDevice.volume") @mock.patch("libsoundtouch.device.SoundTouchDevice.status") - @mock.patch("libsoundtouch.soundtouch_device", side_effect=_mock_soundtouch_device) + @mock.patch( + "homeassistant.components.soundtouch.media_player.soundtouch_device", + side_effect=_mock_soundtouch_device, + ) def test_should_turn_off( self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_power_off ): @@ -362,7 +400,10 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): @mock.patch("libsoundtouch.device.SoundTouchDevice.power_on") @mock.patch("libsoundtouch.device.SoundTouchDevice.volume") @mock.patch("libsoundtouch.device.SoundTouchDevice.status") - @mock.patch("libsoundtouch.soundtouch_device", side_effect=_mock_soundtouch_device) + @mock.patch( + "homeassistant.components.soundtouch.media_player.soundtouch_device", + side_effect=_mock_soundtouch_device, + ) def test_should_turn_on( self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_power_on ): @@ -378,7 +419,10 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): @mock.patch("libsoundtouch.device.SoundTouchDevice.volume_up") @mock.patch("libsoundtouch.device.SoundTouchDevice.volume") @mock.patch("libsoundtouch.device.SoundTouchDevice.status") - @mock.patch("libsoundtouch.soundtouch_device", side_effect=_mock_soundtouch_device) + @mock.patch( + "homeassistant.components.soundtouch.media_player.soundtouch_device", + side_effect=_mock_soundtouch_device, + ) def test_volume_up( self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_volume_up ): @@ -394,7 +438,10 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): @mock.patch("libsoundtouch.device.SoundTouchDevice.volume_down") @mock.patch("libsoundtouch.device.SoundTouchDevice.volume") @mock.patch("libsoundtouch.device.SoundTouchDevice.status") - @mock.patch("libsoundtouch.soundtouch_device", side_effect=_mock_soundtouch_device) + @mock.patch( + "homeassistant.components.soundtouch.media_player.soundtouch_device", + side_effect=_mock_soundtouch_device, + ) def test_volume_down( self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_volume_down ): @@ -410,7 +457,10 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): @mock.patch("libsoundtouch.device.SoundTouchDevice.set_volume") @mock.patch("libsoundtouch.device.SoundTouchDevice.volume") @mock.patch("libsoundtouch.device.SoundTouchDevice.status") - @mock.patch("libsoundtouch.soundtouch_device", side_effect=_mock_soundtouch_device) + @mock.patch( + "homeassistant.components.soundtouch.media_player.soundtouch_device", + side_effect=_mock_soundtouch_device, + ) def test_set_volume_level( self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_set_volume ): @@ -426,7 +476,10 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): @mock.patch("libsoundtouch.device.SoundTouchDevice.mute") @mock.patch("libsoundtouch.device.SoundTouchDevice.volume") @mock.patch("libsoundtouch.device.SoundTouchDevice.status") - @mock.patch("libsoundtouch.soundtouch_device", side_effect=_mock_soundtouch_device) + @mock.patch( + "homeassistant.components.soundtouch.media_player.soundtouch_device", + side_effect=_mock_soundtouch_device, + ) def test_mute( self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_mute ): @@ -442,7 +495,10 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): @mock.patch("libsoundtouch.device.SoundTouchDevice.play") @mock.patch("libsoundtouch.device.SoundTouchDevice.volume") @mock.patch("libsoundtouch.device.SoundTouchDevice.status") - @mock.patch("libsoundtouch.soundtouch_device", side_effect=_mock_soundtouch_device) + @mock.patch( + "homeassistant.components.soundtouch.media_player.soundtouch_device", + side_effect=_mock_soundtouch_device, + ) def test_play( self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_play ): @@ -458,7 +514,10 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): @mock.patch("libsoundtouch.device.SoundTouchDevice.pause") @mock.patch("libsoundtouch.device.SoundTouchDevice.volume") @mock.patch("libsoundtouch.device.SoundTouchDevice.status") - @mock.patch("libsoundtouch.soundtouch_device", side_effect=_mock_soundtouch_device) + @mock.patch( + "homeassistant.components.soundtouch.media_player.soundtouch_device", + side_effect=_mock_soundtouch_device, + ) def test_pause( self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_pause ): @@ -474,7 +533,10 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): @mock.patch("libsoundtouch.device.SoundTouchDevice.play_pause") @mock.patch("libsoundtouch.device.SoundTouchDevice.volume") @mock.patch("libsoundtouch.device.SoundTouchDevice.status") - @mock.patch("libsoundtouch.soundtouch_device", side_effect=_mock_soundtouch_device) + @mock.patch( + "homeassistant.components.soundtouch.media_player.soundtouch_device", + side_effect=_mock_soundtouch_device, + ) def test_play_pause_play( self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_play_pause ): @@ -491,7 +553,10 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): @mock.patch("libsoundtouch.device.SoundTouchDevice.next_track") @mock.patch("libsoundtouch.device.SoundTouchDevice.volume") @mock.patch("libsoundtouch.device.SoundTouchDevice.status") - @mock.patch("libsoundtouch.soundtouch_device", side_effect=_mock_soundtouch_device) + @mock.patch( + "homeassistant.components.soundtouch.media_player.soundtouch_device", + side_effect=_mock_soundtouch_device, + ) def test_next_previous_track( self, mocked_soundtouch_device, @@ -519,7 +584,10 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): ) @mock.patch("libsoundtouch.device.SoundTouchDevice.volume") @mock.patch("libsoundtouch.device.SoundTouchDevice.status") - @mock.patch("libsoundtouch.soundtouch_device", side_effect=_mock_soundtouch_device) + @mock.patch( + "homeassistant.components.soundtouch.media_player.soundtouch_device", + side_effect=_mock_soundtouch_device, + ) def test_play_media( self, mocked_soundtouch_device, @@ -544,7 +612,10 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): @mock.patch("libsoundtouch.device.SoundTouchDevice.play_url") @mock.patch("libsoundtouch.device.SoundTouchDevice.volume") @mock.patch("libsoundtouch.device.SoundTouchDevice.status") - @mock.patch("libsoundtouch.soundtouch_device", side_effect=_mock_soundtouch_device) + @mock.patch( + "homeassistant.components.soundtouch.media_player.soundtouch_device", + side_effect=_mock_soundtouch_device, + ) def test_play_media_url( self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_play_url ): @@ -560,7 +631,10 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): @mock.patch("libsoundtouch.device.SoundTouchDevice.create_zone") @mock.patch("libsoundtouch.device.SoundTouchDevice.volume") @mock.patch("libsoundtouch.device.SoundTouchDevice.status") - @mock.patch("libsoundtouch.soundtouch_device", side_effect=_mock_soundtouch_device) + @mock.patch( + "homeassistant.components.soundtouch.media_player.soundtouch_device", + side_effect=_mock_soundtouch_device, + ) def test_play_everywhere( self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_create_zone ): @@ -605,7 +679,10 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): @mock.patch("libsoundtouch.device.SoundTouchDevice.create_zone") @mock.patch("libsoundtouch.device.SoundTouchDevice.volume") @mock.patch("libsoundtouch.device.SoundTouchDevice.status") - @mock.patch("libsoundtouch.soundtouch_device", side_effect=_mock_soundtouch_device) + @mock.patch( + "homeassistant.components.soundtouch.media_player.soundtouch_device", + side_effect=_mock_soundtouch_device, + ) def test_create_zone( self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_create_zone ): @@ -649,7 +726,10 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): @mock.patch("libsoundtouch.device.SoundTouchDevice.remove_zone_slave") @mock.patch("libsoundtouch.device.SoundTouchDevice.volume") @mock.patch("libsoundtouch.device.SoundTouchDevice.status") - @mock.patch("libsoundtouch.soundtouch_device", side_effect=_mock_soundtouch_device) + @mock.patch( + "homeassistant.components.soundtouch.media_player.soundtouch_device", + side_effect=_mock_soundtouch_device, + ) def test_remove_zone_slave( self, mocked_soundtouch_device, @@ -697,7 +777,10 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): @mock.patch("libsoundtouch.device.SoundTouchDevice.add_zone_slave") @mock.patch("libsoundtouch.device.SoundTouchDevice.volume") @mock.patch("libsoundtouch.device.SoundTouchDevice.status") - @mock.patch("libsoundtouch.soundtouch_device", side_effect=_mock_soundtouch_device) + @mock.patch( + "homeassistant.components.soundtouch.media_player.soundtouch_device", + side_effect=_mock_soundtouch_device, + ) def test_add_zone_slave( self, mocked_soundtouch_device, diff --git a/tests/components/spaceapi/test_init.py b/tests/components/spaceapi/test_init.py index 58c417831a9..840931d073b 100644 --- a/tests/components/spaceapi/test_init.py +++ b/tests/components/spaceapi/test_init.py @@ -3,11 +3,12 @@ from unittest.mock import patch import pytest -from tests.common import mock_coro from homeassistant.components.spaceapi import DOMAIN, SPACEAPI_VERSION, URL_API_SPACEAPI from homeassistant.setup import async_setup_component +from tests.common import mock_coro + CONFIG = { DOMAIN: { "space": "Home", diff --git a/tests/components/spc/test_init.py b/tests/components/spc/test_init.py index 25b9515e44d..f08abac261f 100644 --- a/tests/components/spc/test_init.py +++ b/tests/components/spc/test_init.py @@ -1,5 +1,5 @@ """Tests for Vanderbilt SPC component.""" -from unittest.mock import patch, PropertyMock, Mock +from unittest.mock import Mock, PropertyMock, patch from homeassistant.bootstrap import async_setup_component from homeassistant.components.spc import DATA_API @@ -13,7 +13,8 @@ async def test_valid_device_config(hass, monkeypatch): config = {"spc": {"api_url": "http://localhost/", "ws_url": "ws://localhost/"}} with patch( - "pyspcwebgw.SpcWebGateway.async_load_parameters", return_value=mock_coro(True) + "homeassistant.components.spc.SpcWebGateway.async_load_parameters", + return_value=mock_coro(True), ): assert await async_setup_component(hass, "spc", config) is True @@ -23,7 +24,8 @@ async def test_invalid_device_config(hass, monkeypatch): config = {"spc": {"api_url": "http://localhost/"}} with patch( - "pyspcwebgw.SpcWebGateway.async_load_parameters", return_value=mock_coro(True) + "homeassistant.components.spc.SpcWebGateway.async_load_parameters", + return_value=mock_coro(True), ): assert await async_setup_component(hass, "spc", config) is False @@ -45,11 +47,11 @@ async def test_update_alarm_device(hass): area_mock.verified_alarm = False with patch( - "pyspcwebgw.SpcWebGateway.areas", new_callable=PropertyMock + "homeassistant.components.spc.SpcWebGateway.areas", new_callable=PropertyMock ) as mock_areas: mock_areas.return_value = {"1": area_mock} with patch( - "pyspcwebgw.SpcWebGateway.async_load_parameters", + "homeassistant.components.spc.SpcWebGateway.async_load_parameters", return_value=mock_coro(True), ): assert await async_setup_component(hass, "spc", config) is True diff --git a/tests/components/splunk/test_init.py b/tests/components/splunk/test_init.py index 1fe4c6061cd..256c78af502 100644 --- a/tests/components/splunk/test_init.py +++ b/tests/components/splunk/test_init.py @@ -3,12 +3,12 @@ import json import unittest from unittest import mock -from homeassistant.setup import setup_component import homeassistant.components.splunk as splunk -from homeassistant.const import STATE_ON, STATE_OFF, EVENT_STATE_CHANGED -from homeassistant.helpers import state as state_helper -import homeassistant.util.dt as dt_util +from homeassistant.const import EVENT_STATE_CHANGED, STATE_OFF, STATE_ON from homeassistant.core import State +from homeassistant.helpers import state as state_helper +from homeassistant.setup import setup_component +import homeassistant.util.dt as dt_util from tests.common import get_test_home_assistant, mock_state_change_event diff --git a/tests/components/ssdp/test_init.py b/tests/components/ssdp/test_init.py index 56b937cf9d9..62b7c2bbde2 100644 --- a/tests/components/ssdp/test_init.py +++ b/tests/components/ssdp/test_init.py @@ -1,12 +1,12 @@ """Test the SSDP integration.""" import asyncio -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch import aiohttp import pytest -from homeassistant.generated import ssdp as gn_ssdp from homeassistant.components import ssdp +from homeassistant.generated import ssdp as gn_ssdp from tests.common import mock_coro @@ -27,7 +27,9 @@ async def test_scan_match_st(hass): assert mock_init.mock_calls[0][2]["context"] == {"source": "ssdp"} -@pytest.mark.parametrize("key", ("manufacturer", "deviceType")) +@pytest.mark.parametrize( + "key", (ssdp.ATTR_UPNP_MANUFACTURER, ssdp.ATTR_UPNP_DEVICE_TYPE) +) async def test_scan_match_upnp_devicedesc(hass, aioclient_mock, key): """Test matching based on UPnP device description data.""" aioclient_mock.get( @@ -74,7 +76,14 @@ async def test_scan_not_all_present(hass, aioclient_mock): return_value=[Mock(st="mock-st", location="http://1.1.1.1")], ), patch.dict( gn_ssdp.SSDP, - {"mock-domain": [{"deviceType": "Paulus", "manufacturer": "Paulus"}]}, + { + "mock-domain": [ + { + ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus", + ssdp.ATTR_UPNP_MANUFACTURER: "Paulus", + } + ] + }, ), patch.object( hass.config_entries.flow, "async_init", return_value=mock_coro() ) as mock_init: @@ -103,7 +112,14 @@ async def test_scan_not_all_match(hass, aioclient_mock): return_value=[Mock(st="mock-st", location="http://1.1.1.1")], ), patch.dict( gn_ssdp.SSDP, - {"mock-domain": [{"deviceType": "Paulus", "manufacturer": "Not-Paulus"}]}, + { + "mock-domain": [ + { + ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus", + ssdp.ATTR_UPNP_MANUFACTURER: "Not-Paulus", + } + ] + }, ), patch.object( hass.config_entries.flow, "async_init", return_value=mock_coro() ) as mock_init: diff --git a/tests/components/starline/__init__.py b/tests/components/starline/__init__.py new file mode 100644 index 00000000000..58f50c0f1b9 --- /dev/null +++ b/tests/components/starline/__init__.py @@ -0,0 +1 @@ +"""Tests for the StarLine component.""" diff --git a/tests/components/starline/test_config_flow.py b/tests/components/starline/test_config_flow.py new file mode 100644 index 00000000000..3ca52f849bc --- /dev/null +++ b/tests/components/starline/test_config_flow.py @@ -0,0 +1,127 @@ +"""Tests for StarLine config flow.""" +import requests_mock + +from homeassistant.components.starline import config_flow + +TEST_APP_ID = "666" +TEST_APP_SECRET = "appsecret" +TEST_APP_CODE = "appcode" +TEST_APP_TOKEN = "apptoken" +TEST_APP_SLNET = "slnettoken" +TEST_APP_SLID = "slidtoken" +TEST_APP_UID = "123" +TEST_APP_USERNAME = "sluser" +TEST_APP_PASSWORD = "slpassword" + + +async def test_flow_works(hass): + """Test that config flow works.""" + with requests_mock.Mocker() as mock: + mock.get( + "https://id.starline.ru/apiV3/application/getCode/", + text='{"state": 1, "desc": {"code": "' + TEST_APP_CODE + '"}}', + ) + mock.get( + "https://id.starline.ru/apiV3/application/getToken/", + text='{"state": 1, "desc": {"token": "' + TEST_APP_TOKEN + '"}}', + ) + mock.post( + "https://id.starline.ru/apiV3/user/login/", + text='{"state": 1, "desc": {"user_token": "' + TEST_APP_SLID + '"}}', + ) + mock.post( + "https://developer.starline.ru/json/v2/auth.slid", + text='{"code": 200, "user_id": "' + TEST_APP_UID + '"}', + cookies={"slnet": TEST_APP_SLNET}, + ) + mock.get( + "https://developer.starline.ru/json/v2/user/{}/user_info".format( + TEST_APP_UID + ), + text='{"code": 200, "devices": [{"device_id": "123", "imei": "123", "alias": "123", "battery": "123", "ctemp": "123", "etemp": "123", "fw_version": "123", "gsm_lvl": "123", "phone": "123", "status": "1", "ts_activity": "123", "typename": "123", "balance": {}, "car_state": {}, "car_alr_state": {}, "functions": [], "position": {}}], "shared_devices": []}', + ) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + assert result["type"] == "form" + assert result["step_id"] == "auth_app" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + config_flow.CONF_APP_ID: TEST_APP_ID, + config_flow.CONF_APP_SECRET: TEST_APP_SECRET, + }, + ) + assert result["type"] == "form" + assert result["step_id"] == "auth_user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + config_flow.CONF_USERNAME: TEST_APP_USERNAME, + config_flow.CONF_PASSWORD: TEST_APP_PASSWORD, + }, + ) + assert result["type"] == "create_entry" + assert result["title"] == "Application {}".format(TEST_APP_ID) + + +async def test_step_auth_app_code_falls(hass): + """Test config flow works when app auth code fails.""" + with requests_mock.Mocker() as mock: + mock.get( + "https://id.starline.ru/apiV3/application/getCode/", text='{"state": 0}}' + ) + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": "user"}, + data={ + config_flow.CONF_APP_ID: TEST_APP_ID, + config_flow.CONF_APP_SECRET: TEST_APP_SECRET, + }, + ) + assert result["type"] == "form" + assert result["step_id"] == "auth_app" + assert result["errors"] == {"base": "error_auth_app"} + + +async def test_step_auth_app_token_falls(hass): + """Test config flow works when app auth token fails.""" + with requests_mock.Mocker() as mock: + mock.get( + "https://id.starline.ru/apiV3/application/getCode/", + text='{"state": 1, "desc": {"code": "' + TEST_APP_CODE + '"}}', + ) + mock.get( + "https://id.starline.ru/apiV3/application/getToken/", text='{"state": 0}' + ) + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": "user"}, + data={ + config_flow.CONF_APP_ID: TEST_APP_ID, + config_flow.CONF_APP_SECRET: TEST_APP_SECRET, + }, + ) + assert result["type"] == "form" + assert result["step_id"] == "auth_app" + assert result["errors"] == {"base": "error_auth_app"} + + +async def test_step_auth_user_falls(hass): + """Test config flow works when user fails.""" + with requests_mock.Mocker() as mock: + mock.post("https://id.starline.ru/apiV3/user/login/", text='{"state": 0}') + flow = config_flow.StarlineFlowHandler() + flow.hass = hass + result = await flow.async_step_auth_user( + user_input={ + config_flow.CONF_USERNAME: TEST_APP_USERNAME, + config_flow.CONF_PASSWORD: TEST_APP_PASSWORD, + } + ) + assert result["type"] == "form" + assert result["step_id"] == "auth_user" + assert result["errors"] == {"base": "error_auth_user"} diff --git a/tests/components/startca/test_sensor.py b/tests/components/startca/test_sensor.py index 9ecfed3ce81..eac75a3b4e7 100644 --- a/tests/components/startca/test_sensor.py +++ b/tests/components/startca/test_sensor.py @@ -1,12 +1,10 @@ """Tests for the Start.ca sensor platform.""" -import asyncio from homeassistant.bootstrap import async_setup_component from homeassistant.components.startca.sensor import StartcaData from homeassistant.helpers.aiohttp_client import async_get_clientsession -@asyncio.coroutine -def test_capped_setup(hass, aioclient_mock): +async def test_capped_setup(hass, aioclient_mock): """Test the default setup.""" config = { "platform": "startca", @@ -47,10 +45,10 @@ def test_capped_setup(hass, aioclient_mock): "" ) aioclient_mock.get( - "https://www.start.ca/support/usage/api?key=" "NOTAKEY", text=result + "https://www.start.ca/support/usage/api?key=NOTAKEY", text=result ) - yield from async_setup_component(hass, "sensor", {"sensor": config}) + await async_setup_component(hass, "sensor", {"sensor": config}) state = hass.states.get("sensor.start_ca_usage_ratio") assert state.attributes.get("unit_of_measurement") == "%" @@ -101,8 +99,7 @@ def test_capped_setup(hass, aioclient_mock): assert state.state == "95.05" -@asyncio.coroutine -def test_unlimited_setup(hass, aioclient_mock): +async def test_unlimited_setup(hass, aioclient_mock): """Test the default setup.""" config = { "platform": "startca", @@ -143,10 +140,10 @@ def test_unlimited_setup(hass, aioclient_mock): "" ) aioclient_mock.get( - "https://www.start.ca/support/usage/api?key=" "NOTAKEY", text=result + "https://www.start.ca/support/usage/api?key=NOTAKEY", text=result ) - yield from async_setup_component(hass, "sensor", {"sensor": config}) + await async_setup_component(hass, "sensor", {"sensor": config}) state = hass.states.get("sensor.start_ca_usage_ratio") assert state.attributes.get("unit_of_measurement") == "%" @@ -197,27 +194,23 @@ def test_unlimited_setup(hass, aioclient_mock): assert state.state == "inf" -@asyncio.coroutine -def test_bad_return_code(hass, aioclient_mock): +async def test_bad_return_code(hass, aioclient_mock): """Test handling a return code that isn't HTTP OK.""" - aioclient_mock.get( - "https://www.start.ca/support/usage/api?key=" "NOTAKEY", status=404 - ) + aioclient_mock.get("https://www.start.ca/support/usage/api?key=NOTAKEY", status=404) scd = StartcaData(hass.loop, async_get_clientsession(hass), "NOTAKEY", 400) - result = yield from scd.async_update() + result = await scd.async_update() assert result is False -@asyncio.coroutine -def test_bad_json_decode(hass, aioclient_mock): +async def test_bad_json_decode(hass, aioclient_mock): """Test decoding invalid json result.""" aioclient_mock.get( - "https://www.start.ca/support/usage/api?key=" "NOTAKEY", text="this is not xml" + "https://www.start.ca/support/usage/api?key=NOTAKEY", text="this is not xml" ) scd = StartcaData(hass.loop, async_get_clientsession(hass), "NOTAKEY", 400) - result = yield from scd.async_update() + result = await scd.async_update() assert result is False diff --git a/tests/components/statistics/test_sensor.py b/tests/components/statistics/test_sensor.py index 2a28876f552..6a38ea6c391 100644 --- a/tests/components/statistics/test_sensor.py +++ b/tests/components/statistics/test_sensor.py @@ -1,18 +1,18 @@ """The test for the statistics sensor platform.""" -import unittest +from datetime import datetime, timedelta import statistics +import unittest +from unittest.mock import patch import pytest -from homeassistant.setup import setup_component -from homeassistant.components.statistics.sensor import StatisticsSensor -from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, STATE_UNKNOWN -from homeassistant.util import dt as dt_util -from tests.common import get_test_home_assistant -from unittest.mock import patch -from datetime import datetime, timedelta -from tests.common import init_recorder_component from homeassistant.components import recorder +from homeassistant.components.statistics.sensor import StatisticsSensor +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, TEMP_CELSIUS +from homeassistant.setup import setup_component +from homeassistant.util import dt as dt_util + +from tests.common import get_test_home_assistant, init_recorder_component class TestStatisticsSensor(unittest.TestCase): diff --git a/tests/components/stt/test_init.py b/tests/components/stt/test_init.py index 5627d7d3e53..1e69fa2494a 100644 --- a/tests/components/stt/test_init.py +++ b/tests/components/stt/test_init.py @@ -1,7 +1,7 @@ """Test STT component setup.""" -from homeassistant.setup import async_setup_component from homeassistant.components import stt +from homeassistant.setup import async_setup_component async def test_setup_comp(hass): diff --git a/tests/components/sun/test_init.py b/tests/components/sun/test_init.py index 5346f97308f..e04de7e2578 100644 --- a/tests/components/sun/test_init.py +++ b/tests/components/sun/test_init.py @@ -5,10 +5,10 @@ from unittest.mock import patch from pytest import mark import homeassistant.components.sun as sun -import homeassistant.core as ha -import homeassistant.util.dt as dt_util from homeassistant.const import EVENT_STATE_CHANGED +import homeassistant.core as ha from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util async def test_setting_rising(hass): diff --git a/tests/components/switch/common.py b/tests/components/switch/common.py index 4491b07cd73..1123b1de6c1 100644 --- a/tests/components/switch/common.py +++ b/tests/components/switch/common.py @@ -4,29 +4,34 @@ All containing methods are legacy helpers that should not be used by new components. Instead call the service directly. """ from homeassistant.components.switch import DOMAIN -from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON +from homeassistant.const import ( + ATTR_ENTITY_ID, + ENTITY_MATCH_ALL, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) from homeassistant.loader import bind_hass @bind_hass -def turn_on(hass, entity_id=None): +def turn_on(hass, entity_id=ENTITY_MATCH_ALL): """Turn all or specified switch on.""" hass.add_job(async_turn_on, hass, entity_id) -async def async_turn_on(hass, entity_id=None): +async def async_turn_on(hass, entity_id=ENTITY_MATCH_ALL): """Turn all or specified switch on.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None await hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data, blocking=True) @bind_hass -def turn_off(hass, entity_id=None): +def turn_off(hass, entity_id=ENTITY_MATCH_ALL): """Turn all or specified switch off.""" hass.add_job(async_turn_off, hass, entity_id) -async def async_turn_off(hass, entity_id=None): +async def async_turn_off(hass, entity_id=ENTITY_MATCH_ALL): """Turn all or specified switch off.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None await hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data, blocking=True) diff --git a/tests/components/switch/test_device_action.py b/tests/components/switch/test_device_action.py index 888e06e0214..06ad7323ead 100644 --- a/tests/components/switch/test_device_action.py +++ b/tests/components/switch/test_device_action.py @@ -1,14 +1,14 @@ """The test for switch device automation.""" import pytest -from homeassistant.components.switch import DOMAIN -from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM -from homeassistant.setup import async_setup_component import homeassistant.components.automation as automation from homeassistant.components.device_automation import ( _async_get_device_automations as async_get_device_automations, ) +from homeassistant.components.switch import DOMAIN +from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, diff --git a/tests/components/switch/test_device_condition.py b/tests/components/switch/test_device_condition.py index e673527fada..d51a00ddf79 100644 --- a/tests/components/switch/test_device_condition.py +++ b/tests/components/switch/test_device_condition.py @@ -1,22 +1,23 @@ """The test for switch device automation.""" from datetime import timedelta -import pytest from unittest.mock import patch -from homeassistant.components.switch import DOMAIN -from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM -from homeassistant.setup import async_setup_component +import pytest + import homeassistant.components.automation as automation +from homeassistant.components.switch import DOMAIN +from homeassistant.const import CONF_PLATFORM, 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, + async_get_device_automation_capabilities, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, - async_get_device_automations, - async_get_device_automation_capabilities, ) diff --git a/tests/components/switch/test_device_trigger.py b/tests/components/switch/test_device_trigger.py index 31fb6d30f60..19588ebfba0 100644 --- a/tests/components/switch/test_device_trigger.py +++ b/tests/components/switch/test_device_trigger.py @@ -1,22 +1,23 @@ """The test for switch device automation.""" from datetime import timedelta + import pytest -from homeassistant.components.switch import DOMAIN -from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM -from homeassistant.setup import async_setup_component import homeassistant.components.automation as automation +from homeassistant.components.switch import DOMAIN +from homeassistant.const import CONF_PLATFORM, 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, async_fire_time_changed, + async_get_device_automation_capabilities, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, - async_get_device_automations, - async_get_device_automation_capabilities, ) diff --git a/tests/components/switch/test_init.py b/tests/components/switch/test_init.py index a9463cb78f4..bebebafc763 100644 --- a/tests/components/switch/test_init.py +++ b/tests/components/switch/test_init.py @@ -2,10 +2,10 @@ # pylint: disable=protected-access import unittest -from homeassistant.setup import setup_component, async_setup_component from homeassistant import core from homeassistant.components import switch -from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM +from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON +from homeassistant.setup import async_setup_component, setup_component from tests.common import get_test_home_assistant, mock_entity_platform from tests.components.switch import common diff --git a/tests/components/switch/test_light.py b/tests/components/switch/test_light.py index e5c5e5c0aed..3034877c6d6 100644 --- a/tests/components/switch/test_light.py +++ b/tests/components/switch/test_light.py @@ -1,6 +1,7 @@ """The tests for the Light Switch platform.""" from homeassistant.setup import async_setup_component + from tests.components.light import common from tests.components.switch import common as switch_common diff --git a/tests/components/switcher_kis/conftest.py b/tests/components/switcher_kis/conftest.py index 888ffd46c3b..2b0150cae67 100644 --- a/tests/components/switcher_kis/conftest.py +++ b/tests/components/switcher_kis/conftest.py @@ -103,10 +103,22 @@ def mock_bridge_fixture() -> Generator[None, Any, None]: mock_bridge = CoroutineMock() patchers = [ - patch("aioswitcher.bridge.SwitcherV2Bridge.start", new=mock_bridge), - patch("aioswitcher.bridge.SwitcherV2Bridge.stop", new=mock_bridge), - patch("aioswitcher.bridge.SwitcherV2Bridge.queue", get=mock_queue), - patch("aioswitcher.bridge.SwitcherV2Bridge.running", return_value=True), + patch( + "homeassistant.components.switcher_kis.SwitcherV2Bridge.start", + new=mock_bridge, + ), + patch( + "homeassistant.components.switcher_kis.SwitcherV2Bridge.stop", + new=mock_bridge, + ), + patch( + "homeassistant.components.switcher_kis.SwitcherV2Bridge.queue", + get=mock_queue, + ), + patch( + "homeassistant.components.switcher_kis.SwitcherV2Bridge.running", + return_value=True, + ), ] for patcher in patchers: @@ -127,9 +139,18 @@ def mock_failed_bridge_fixture() -> Generator[None, Any, None]: raise RuntimeError patchers = [ - patch("aioswitcher.bridge.SwitcherV2Bridge.start", return_value=None), - patch("aioswitcher.bridge.SwitcherV2Bridge.stop", return_value=None), - patch("aioswitcher.bridge.SwitcherV2Bridge.queue", get=mock_queue), + patch( + "homeassistant.components.switcher_kis.SwitcherV2Bridge.start", + return_value=None, + ), + patch( + "homeassistant.components.switcher_kis.SwitcherV2Bridge.stop", + return_value=None, + ), + patch( + "homeassistant.components.switcher_kis.SwitcherV2Bridge.queue", + get=mock_queue, + ), ] for patcher in patchers: @@ -147,8 +168,13 @@ def mock_api_fixture() -> Generator[CoroutineMock, Any, None]: mock_api = CoroutineMock() patchers = [ - patch("aioswitcher.api.SwitcherV2Api.connect", new=mock_api), - patch("aioswitcher.api.SwitcherV2Api.disconnect", new=mock_api), + patch( + "homeassistant.components.switcher_kis.SwitcherV2Api.connect", new=mock_api + ), + patch( + "homeassistant.components.switcher_kis.SwitcherV2Api.disconnect", + new=mock_api, + ), ] for patcher in patchers: diff --git a/tests/components/switcher_kis/test_init.py b/tests/components/switcher_kis/test_init.py index ad6bafd0643..9e262fafa7e 100644 --- a/tests/components/switcher_kis/test_init.py +++ b/tests/components/switcher_kis/test_init.py @@ -1,28 +1,26 @@ """Test cases for the switcher_kis component.""" from datetime import timedelta -from typing import Any, Generator, TYPE_CHECKING +from typing import TYPE_CHECKING, Any, Generator from pytest import raises -from homeassistant.const import CONF_ENTITY_ID from homeassistant.components.switcher_kis import ( CONF_AUTO_OFF, - DOMAIN, DATA_DEVICE, + DOMAIN, SERVICE_SET_AUTO_OFF_NAME, SERVICE_SET_AUTO_OFF_SCHEMA, SIGNAL_SWITCHER_DEVICE_UPDATE, ) -from homeassistant.core import callback, Context +from homeassistant.const import CONF_ENTITY_ID +from homeassistant.core import Context, callback +from homeassistant.exceptions import Unauthorized, UnknownUser from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import HomeAssistantType -from homeassistant.exceptions import Unauthorized, UnknownUser from homeassistant.setup import async_setup_component from homeassistant.util import dt -from tests.common import async_mock_service, async_fire_time_changed - from .consts import ( DUMMY_AUTO_OFF_SET, DUMMY_DEVICE_ID, @@ -38,6 +36,8 @@ from .consts import ( SWITCH_ENTITY_ID, ) +from tests.common import async_fire_time_changed, async_mock_service + if TYPE_CHECKING: from tests.common import MockUser from aioswitcher.devices import SwitcherV2Device diff --git a/tests/components/system_log/test_init.py b/tests/components/system_log/test_init.py index 55d69db8fb4..c21d4842b45 100644 --- a/tests/components/system_log/test_init.py +++ b/tests/components/system_log/test_init.py @@ -2,9 +2,9 @@ import logging from unittest.mock import MagicMock, patch -from homeassistant.core import callback from homeassistant.bootstrap import async_setup_component from homeassistant.components import system_log +from homeassistant.core import callback _LOGGER = logging.getLogger("test_logger") BASIC_CONFIG = {"system_log": {"max_entries": 2}} diff --git a/tests/components/teksavvy/test_sensor.py b/tests/components/teksavvy/test_sensor.py index 9e2714f0388..30bb98911f8 100644 --- a/tests/components/teksavvy/test_sensor.py +++ b/tests/components/teksavvy/test_sensor.py @@ -1,12 +1,10 @@ """Tests for the TekSavvy sensor platform.""" -import asyncio from homeassistant.bootstrap import async_setup_component from homeassistant.components.teksavvy.sensor import TekSavvyData from homeassistant.helpers.aiohttp_client import async_get_clientsession -@asyncio.coroutine -def test_capped_setup(hass, aioclient_mock): +async def test_capped_setup(hass, aioclient_mock): """Test the default setup.""" config = { "platform": "teksavvy", @@ -44,7 +42,7 @@ def test_capped_setup(hass, aioclient_mock): text=result, ) - yield from async_setup_component(hass, "sensor", {"sensor": config}) + await async_setup_component(hass, "sensor", {"sensor": config}) state = hass.states.get("sensor.teksavvy_data_limit") assert state.attributes.get("unit_of_measurement") == "GB" @@ -87,8 +85,7 @@ def test_capped_setup(hass, aioclient_mock): assert state.state == "173.25" -@asyncio.coroutine -def test_unlimited_setup(hass, aioclient_mock): +async def test_unlimited_setup(hass, aioclient_mock): """Test the default setup.""" config = { "platform": "teksavvy", @@ -126,7 +123,7 @@ def test_unlimited_setup(hass, aioclient_mock): text=result, ) - yield from async_setup_component(hass, "sensor", {"sensor": config}) + await async_setup_component(hass, "sensor", {"sensor": config}) state = hass.states.get("sensor.teksavvy_data_limit") assert state.attributes.get("unit_of_measurement") == "GB" @@ -169,8 +166,7 @@ def test_unlimited_setup(hass, aioclient_mock): assert state.state == "inf" -@asyncio.coroutine -def test_bad_return_code(hass, aioclient_mock): +async def test_bad_return_code(hass, aioclient_mock): """Test handling a return code that isn't HTTP OK.""" aioclient_mock.get( "https://api.teksavvy.com/" @@ -181,12 +177,11 @@ def test_bad_return_code(hass, aioclient_mock): tsd = TekSavvyData(hass.loop, async_get_clientsession(hass), "notakey", 400) - result = yield from tsd.async_update() + result = await tsd.async_update() assert result is False -@asyncio.coroutine -def test_bad_json_decode(hass, aioclient_mock): +async def test_bad_json_decode(hass, aioclient_mock): """Test decoding invalid json result.""" aioclient_mock.get( "https://api.teksavvy.com/" @@ -197,5 +192,5 @@ def test_bad_json_decode(hass, aioclient_mock): tsd = TekSavvyData(hass.loop, async_get_clientsession(hass), "notakey", 400) - result = yield from tsd.async_update() + result = await tsd.async_update() assert result is False diff --git a/tests/components/tellduslive/test_config_flow.py b/tests/components/tellduslive/test_config_flow.py index c615c8e6aea..f4972ada2c7 100644 --- a/tests/components/tellduslive/test_config_flow.py +++ b/tests/components/tellduslive/test_config_flow.py @@ -15,7 +15,7 @@ from homeassistant.components.tellduslive import ( ) from homeassistant.const import CONF_HOST -from tests.common import MockConfigEntry, MockDependency, mock_coro +from tests.common import MockConfigEntry, mock_coro def init_config_flow(hass, side_effect=None): @@ -42,13 +42,17 @@ def authorize(): @pytest.fixture def mock_tellduslive(supports_local_api, authorize): """Mock tellduslive.""" - with MockDependency("tellduslive") as mock_tellduslive_: - mock_tellduslive_.supports_local_api.return_value = supports_local_api - mock_tellduslive_.Session().authorize.return_value = authorize - mock_tellduslive_.Session().access_token = "token" - mock_tellduslive_.Session().access_token_secret = "token_secret" - mock_tellduslive_.Session().authorize_url = "https://example.com" - yield mock_tellduslive_ + with patch( + "homeassistant.components.tellduslive.config_flow.Session" + ) as Session, patch( + "homeassistant.components.tellduslive.config_flow.supports_local_api" + ) as tellduslive_supports_local_api: + tellduslive_supports_local_api.return_value = supports_local_api + Session().authorize.return_value = authorize + Session().access_token = "token" + Session().access_token_secret = "token_secret" + Session().authorize_url = "https://example.com" + yield Session, tellduslive_supports_local_api async def test_abort_if_already_setup(hass): diff --git a/tests/components/template/test_binary_sensor.py b/tests/components/template/test_binary_sensor.py index 143811da209..b024bbc311f 100644 --- a/tests/components/template/test_binary_sensor.py +++ b/tests/components/template/test_binary_sensor.py @@ -3,24 +3,24 @@ from datetime import timedelta import unittest from unittest import mock -from homeassistant.const import ( - MATCH_ALL, - EVENT_HOMEASSISTANT_START, - STATE_UNAVAILABLE, - STATE_ON, - STATE_OFF, -) from homeassistant import setup from homeassistant.components.template import binary_sensor as template +from homeassistant.const import ( + EVENT_HOMEASSISTANT_START, + MATCH_ALL, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, +) from homeassistant.exceptions import TemplateError from homeassistant.helpers import template as template_hlpr from homeassistant.util.async_ import run_callback_threadsafe import homeassistant.util.dt as dt_util from tests.common import ( - get_test_home_assistant, assert_setup_component, async_fire_time_changed, + get_test_home_assistant, ) @@ -576,22 +576,22 @@ async def test_no_update_template_match_all(hass, caplog): await hass.async_block_till_done() assert len(hass.states.async_all()) == 5 assert ( - "Template binary sensor all_state has no entity ids " + "Template binary sensor 'all_state' has no entity ids " "configured to track nor were we able to extract the entities to " "track from the value template" ) in caplog.text assert ( - "Template binary sensor all_icon has no entity ids " + "Template binary sensor 'all_icon' has no entity ids " "configured to track nor were we able to extract the entities to " "track from the icon template" ) in caplog.text assert ( - "Template binary sensor all_entity_picture has no entity ids " + "Template binary sensor 'all_entity_picture' has no entity ids " "configured to track nor were we able to extract the entities to " "track from the entity_picture template" ) in caplog.text assert ( - "Template binary sensor all_attribute has no entity ids " + "Template binary sensor 'all_attribute' has no entity ids " "configured to track nor were we able to extract the entities to " "track from the test_attribute template" ) in caplog.text diff --git a/tests/components/template/test_cover.py b/tests/components/template/test_cover.py index d3be01cbdc3..c3e1f2843fd 100644 --- a/tests/components/template/test_cover.py +++ b/tests/components/template/test_cover.py @@ -1,5 +1,6 @@ """The tests the cover command line platform.""" import logging + import pytest from homeassistant import setup @@ -8,18 +9,18 @@ from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_CLOSE_COVER, SERVICE_CLOSE_COVER_TILT, - SERVICE_TOGGLE, - SERVICE_TOGGLE_COVER_TILT, SERVICE_OPEN_COVER, SERVICE_OPEN_COVER_TILT, SERVICE_SET_COVER_POSITION, SERVICE_SET_COVER_TILT_POSITION, SERVICE_STOP_COVER, + SERVICE_TOGGLE, + SERVICE_TOGGLE_COVER_TILT, STATE_CLOSED, - STATE_UNAVAILABLE, - STATE_OPEN, - STATE_ON, STATE_OFF, + STATE_ON, + STATE_OPEN, + STATE_UNAVAILABLE, ) from tests.common import assert_setup_component, async_mock_service diff --git a/tests/components/template/test_fan.py b/tests/components/template/test_fan.py index 5753684795b..981b87ff43e 100644 --- a/tests/components/template/test_fan.py +++ b/tests/components/template/test_fan.py @@ -1,23 +1,23 @@ """The tests for the Template fan platform.""" import logging -import pytest +import pytest import voluptuous as vol from homeassistant import setup -from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNAVAILABLE from homeassistant.components.fan import ( - ATTR_SPEED, - ATTR_OSCILLATING, - SPEED_LOW, - SPEED_MEDIUM, - SPEED_HIGH, ATTR_DIRECTION, + ATTR_OSCILLATING, + ATTR_SPEED, DIRECTION_FORWARD, DIRECTION_REVERSE, + SPEED_HIGH, + SPEED_LOW, + SPEED_MEDIUM, ) +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE -from tests.common import async_mock_service, assert_setup_component +from tests.common import assert_setup_component, async_mock_service from tests.components.fan import common _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/template/test_light.py b/tests/components/template/test_light.py index c2dd49a76fb..62d377a9337 100644 --- a/tests/components/template/test_light.py +++ b/tests/components/template/test_light.py @@ -1,12 +1,12 @@ """The tests for the Template light platform.""" import logging -from homeassistant.core import callback from homeassistant import setup from homeassistant.components.light import ATTR_BRIGHTNESS -from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNAVAILABLE +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE +from homeassistant.core import callback -from tests.common import get_test_home_assistant, assert_setup_component +from tests.common import assert_setup_component, get_test_home_assistant from tests.components.light import common _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/template/test_lock.py b/tests/components/template/test_lock.py index d1d30207375..10f959efed8 100644 --- a/tests/components/template/test_lock.py +++ b/tests/components/template/test_lock.py @@ -1,13 +1,12 @@ """The tests for the Template lock platform.""" import logging -from homeassistant.core import callback from homeassistant import setup from homeassistant.components import lock -from homeassistant.const import ATTR_ENTITY_ID -from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNAVAILABLE +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, STATE_UNAVAILABLE +from homeassistant.core import callback -from tests.common import get_test_home_assistant, assert_setup_component +from tests.common import assert_setup_component, get_test_home_assistant _LOGGER = logging.getLogger(__name__) @@ -254,10 +253,9 @@ class TestTemplateLock: assert state.state == lock.STATE_UNLOCKED assert ( - "Template lock 'Template Lock' has no entity ids configured " - "to track nor were we able to extract the entities to track " - "from the 'value_template' template. This entity will only " - "be able to be updated manually." + "Template lock 'Template Lock' has no entity ids configured to track " + "nor were we able to extract the entities to track from the value " + "template(s). This entity will only be able to be updated manually" ) in caplog.text self.hass.states.set("lock.template_lock", lock.STATE_LOCKED) @@ -343,7 +341,7 @@ async def test_available_template_with_entities(hass): { "lock": { "platform": "template", - "value_template": "{{ 'on' }}", + "value_template": "{{ states('switch.test_state') }}", "lock": {"service": "switch.turn_on", "entity_id": "switch.test_state"}, "unlock": { "service": "switch.turn_off", diff --git a/tests/components/template/test_sensor.py b/tests/components/template/test_sensor.py index b3813da1766..e9033b3dfe3 100644 --- a/tests/components/template/test_sensor.py +++ b/tests/components/template/test_sensor.py @@ -1,9 +1,13 @@ """The test for the Template sensor platform.""" -from homeassistant.const import EVENT_HOMEASSISTANT_START -from homeassistant.setup import setup_component, async_setup_component +from homeassistant.const import ( + EVENT_HOMEASSISTANT_START, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, +) +from homeassistant.setup import async_setup_component, setup_component -from tests.common import get_test_home_assistant, assert_setup_component -from homeassistant.const import STATE_UNAVAILABLE, STATE_ON, STATE_OFF +from tests.common import assert_setup_component, get_test_home_assistant class TestTemplateSensor: @@ -377,6 +381,49 @@ class TestTemplateSensor: state = self.hass.states.get("sensor.test2") assert "device_class" not in state.attributes + def test_available_template_with_entities(self): + """Test availability tempalates with values from other entities.""" + + with assert_setup_component(1): + assert setup_component( + self.hass, + "sensor", + { + "sensor": { + "platform": "template", + "sensors": { + "test_template_sensor": { + "value_template": "{{ states.sensor.test_state.state }}", + "availability_template": "{{ is_state('availability_boolean.state', 'on') }}", + } + }, + } + }, + ) + + self.hass.start() + self.hass.block_till_done() + + # When template returns true.. + self.hass.states.set("availability_boolean.state", STATE_ON) + self.hass.block_till_done() + + # Device State should not be unavailable + assert ( + self.hass.states.get("sensor.test_template_sensor").state + != STATE_UNAVAILABLE + ) + + # When Availability template returns false + self.hass.states.set("availability_boolean.state", STATE_OFF) + self.hass.block_till_done() + + # device state should be unavailable + assert ( + self.hass.states.get("sensor.test_template_sensor").state + == STATE_UNAVAILABLE + ) + async def test_available_template_with_entities(hass): """Test availability tempalates with values from other entities.""" @@ -511,27 +558,27 @@ async def test_no_template_match_all(hass, caplog): await hass.async_block_till_done() assert len(hass.states.async_all()) == 6 assert ( - "Template sensor invalid_state has no entity ids " + "Template sensor 'invalid_state' has no entity ids " "configured to track nor were we able to extract the entities to " "track from the value template" ) in caplog.text assert ( - "Template sensor invalid_icon has no entity ids " + "Template sensor 'invalid_icon' has no entity ids " "configured to track nor were we able to extract the entities to " "track from the icon template" ) in caplog.text assert ( - "Template sensor invalid_entity_picture has no entity ids " + "Template sensor 'invalid_entity_picture' has no entity ids " "configured to track nor were we able to extract the entities to " "track from the entity_picture template" ) in caplog.text assert ( - "Template sensor invalid_friendly_name has no entity ids " + "Template sensor 'invalid_friendly_name' has no entity ids " "configured to track nor were we able to extract the entities to " "track from the friendly_name template" ) in caplog.text assert ( - "Template sensor invalid_attribute has no entity ids " + "Template sensor 'invalid_attribute' has no entity ids " "configured to track nor were we able to extract the entities to " "track from the test_attribute template" ) in caplog.text diff --git a/tests/components/template/test_switch.py b/tests/components/template/test_switch.py index 3adc5dcad46..a66028a318f 100644 --- a/tests/components/template/test_switch.py +++ b/tests/components/template/test_switch.py @@ -1,9 +1,9 @@ """The tests for the Template switch platform.""" -from homeassistant.core import callback from homeassistant import setup -from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNAVAILABLE +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE +from homeassistant.core import callback -from tests.common import get_test_home_assistant, assert_setup_component +from tests.common import assert_setup_component, get_test_home_assistant from tests.components.switch import common diff --git a/tests/components/template/test_vacuum.py b/tests/components/template/test_vacuum.py index da0e8e59ede..4080b75f46a 100644 --- a/tests/components/template/test_vacuum.py +++ b/tests/components/template/test_vacuum.py @@ -1,9 +1,9 @@ """The tests for the Template vacuum platform.""" import logging + import pytest from homeassistant import setup -from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNKNOWN, STATE_UNAVAILABLE from homeassistant.components.vacuum import ( ATTR_BATTERY_LEVEL, STATE_CLEANING, @@ -12,8 +12,9 @@ from homeassistant.components.vacuum import ( STATE_PAUSED, STATE_RETURNING, ) +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN -from tests.common import async_mock_service, assert_setup_component +from tests.common import assert_setup_component, async_mock_service from tests.components.vacuum import common _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/tesla/__init__.py b/tests/components/tesla/__init__.py new file mode 100644 index 00000000000..89b1e1c0c54 --- /dev/null +++ b/tests/components/tesla/__init__.py @@ -0,0 +1 @@ +"""Tests for the Tesla integration.""" diff --git a/tests/components/tesla/test_config_flow.py b/tests/components/tesla/test_config_flow.py new file mode 100644 index 00000000000..b6eeff54a50 --- /dev/null +++ b/tests/components/tesla/test_config_flow.py @@ -0,0 +1,160 @@ +"""Test the Tesla config flow.""" +from unittest.mock import patch + +from teslajsonpy import TeslaException + +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.tesla.const import DOMAIN +from homeassistant.const import ( + CONF_ACCESS_TOKEN, + CONF_PASSWORD, + CONF_SCAN_INTERVAL, + CONF_TOKEN, + CONF_USERNAME, +) + +from tests.common import MockConfigEntry, mock_coro + + +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.tesla.config_flow.TeslaAPI.connect", + return_value=mock_coro(("test-refresh-token", "test-access-token")), + ), patch( + "homeassistant.components.tesla.async_setup", return_value=mock_coro(True) + ) as mock_setup, patch( + "homeassistant.components.tesla.async_setup_entry", return_value=mock_coro(True) + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_PASSWORD: "test", CONF_USERNAME: "test@email.com"} + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "test@email.com" + assert result2["data"] == { + "token": "test-refresh-token", + "access_token": "test-access-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( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.tesla.config_flow.TeslaAPI.connect", + side_effect=TeslaException(401), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"}, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "invalid_credentials"} + + +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.tesla.config_flow.TeslaAPI.connect", + side_effect=TeslaException(code=404), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PASSWORD: "test-password", CONF_USERNAME: "test-username"}, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "connection_error"} + + +async def test_form_repeat_identifier(hass): + """Test we handle repeat identifiers.""" + entry = MockConfigEntry(domain=DOMAIN, title="test-username", data={}, options=None) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + with patch( + "homeassistant.components.tesla.config_flow.TeslaAPI.connect", + return_value=mock_coro(("test-refresh-token", "test-access-token")), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"}, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {CONF_USERNAME: "identifier_exists"} + + +async def test_import(hass): + """Test import step.""" + + with patch( + "homeassistant.components.tesla.config_flow.TeslaAPI.connect", + return_value=mock_coro(("test-refresh-token", "test-access-token")), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={CONF_PASSWORD: "test-password", CONF_USERNAME: "test-username"}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "test-username" + assert result["data"][CONF_ACCESS_TOKEN] == "test-access-token" + assert result["data"][CONF_TOKEN] == "test-refresh-token" + assert result["description_placeholders"] is None + + +async def test_option_flow(hass): + """Test config flow options.""" + entry = MockConfigEntry(domain=DOMAIN, data={}, options=None) + entry.add_to_hass(hass) + + result = await hass.config_entries.options.flow.async_init(entry.entry_id) + + assert result["type"] == "form" + assert result["step_id"] == "init" + + result = await hass.config_entries.options.flow.async_configure( + result["flow_id"], user_input={CONF_SCAN_INTERVAL: 350} + ) + assert result["type"] == "create_entry" + assert result["data"] == {CONF_SCAN_INTERVAL: 350} + + +async def test_option_flow_input_floor(hass): + """Test config flow options.""" + entry = MockConfigEntry(domain=DOMAIN, data={}, options=None) + entry.add_to_hass(hass) + + result = await hass.config_entries.options.flow.async_init(entry.entry_id) + + assert result["type"] == "form" + assert result["step_id"] == "init" + + result = await hass.config_entries.options.flow.async_configure( + result["flow_id"], user_input={CONF_SCAN_INTERVAL: 1} + ) + assert result["type"] == "create_entry" + assert result["data"] == {CONF_SCAN_INTERVAL: 300} diff --git a/tests/components/threshold/test_binary_sensor.py b/tests/components/threshold/test_binary_sensor.py index 147e2e37cb4..3eb6299b3be 100644 --- a/tests/components/threshold/test_binary_sensor.py +++ b/tests/components/threshold/test_binary_sensor.py @@ -1,8 +1,8 @@ """The test for the threshold sensor platform.""" import unittest -from homeassistant.setup import setup_component from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, TEMP_CELSIUS +from homeassistant.setup import setup_component from tests.common import get_test_home_assistant diff --git a/tests/components/time_date/test_sensor.py b/tests/components/time_date/test_sensor.py index cdd3bd7e9f3..2aae99f93a5 100644 --- a/tests/components/time_date/test_sensor.py +++ b/tests/components/time_date/test_sensor.py @@ -1,4 +1,4 @@ -"""The tests for Kira sensor platform.""" +"""The tests for time_date sensor platform.""" import unittest from unittest.mock import patch @@ -9,7 +9,7 @@ from tests.common import get_test_home_assistant class TestTimeDateSensor(unittest.TestCase): - """Tests the Kira Sensor platform.""" + """Tests the time_date Sensor platform.""" # pylint: disable=invalid-name DEVICES = [] @@ -67,6 +67,14 @@ class TestTimeDateSensor(unittest.TestCase): device._update_internal_state(now) assert device.state == "00:54" + device = time_date.TimeDateSensor(self.hass, "date_time") + device._update_internal_state(now) + assert device.state == "2017-05-18, 00:54" + + device = time_date.TimeDateSensor(self.hass, "date_time_utc") + device._update_internal_state(now) + assert device.state == "2017-05-18, 00:54" + device = time_date.TimeDateSensor(self.hass, "beat") device._update_internal_state(now) assert device.state == "@079" @@ -75,6 +83,41 @@ class TestTimeDateSensor(unittest.TestCase): device._update_internal_state(now) assert device.state == "2017-05-18T00:54:00" + def test_states_non_default_timezone(self): + """Test states of sensors in a timezone other than UTC.""" + new_tz = dt_util.get_time_zone("America/New_York") + assert new_tz is not None + dt_util.set_default_time_zone(new_tz) + + now = dt_util.utc_from_timestamp(1495068856) + device = time_date.TimeDateSensor(self.hass, "time") + device._update_internal_state(now) + assert device.state == "20:54" + + device = time_date.TimeDateSensor(self.hass, "date") + device._update_internal_state(now) + assert device.state == "2017-05-17" + + device = time_date.TimeDateSensor(self.hass, "time_utc") + device._update_internal_state(now) + assert device.state == "00:54" + + device = time_date.TimeDateSensor(self.hass, "date_time") + device._update_internal_state(now) + assert device.state == "2017-05-17, 20:54" + + device = time_date.TimeDateSensor(self.hass, "date_time_utc") + device._update_internal_state(now) + assert device.state == "2017-05-18, 00:54" + + device = time_date.TimeDateSensor(self.hass, "beat") + device._update_internal_state(now) + assert device.state == "@079" + + device = time_date.TimeDateSensor(self.hass, "date_time_iso") + device._update_internal_state(now) + assert device.state == "2017-05-17T20:54:00" + # pylint: disable=no-member def test_timezone_intervals(self): """Test date sensor behavior in a timezone besides UTC.""" @@ -122,5 +165,7 @@ class TestTimeDateSensor(unittest.TestCase): assert device.icon == "mdi:calendar" device = time_date.TimeDateSensor(self.hass, "date_time") assert device.icon == "mdi:calendar-clock" + device = time_date.TimeDateSensor(self.hass, "date_time_utc") + assert device.icon == "mdi:calendar-clock" device = time_date.TimeDateSensor(self.hass, "date_time_iso") assert device.icon == "mdi:calendar-clock" diff --git a/tests/components/timer/test_init.py b/tests/components/timer/test_init.py index 5d57cd2f2d8..93493fc3a55 100644 --- a/tests/components/timer/test_init.py +++ b/tests/components/timer/test_init.py @@ -1,31 +1,39 @@ """The tests for the timer component.""" # pylint: disable=protected-access -import asyncio -import logging from datetime import timedelta +import logging +from unittest.mock import patch + +import pytest -from homeassistant.core import CoreState -from homeassistant.setup import async_setup_component from homeassistant.components.timer import ( - DOMAIN, + ATTR_DURATION, CONF_DURATION, + CONF_ICON, CONF_NAME, + DOMAIN, + EVENT_TIMER_CANCELLED, + EVENT_TIMER_FINISHED, + EVENT_TIMER_PAUSED, + EVENT_TIMER_RESTARTED, + EVENT_TIMER_STARTED, + SERVICE_CANCEL, + SERVICE_FINISH, + SERVICE_PAUSE, + SERVICE_START, STATUS_ACTIVE, STATUS_IDLE, STATUS_PAUSED, - CONF_ICON, - ATTR_DURATION, - EVENT_TIMER_FINISHED, - EVENT_TIMER_CANCELLED, - EVENT_TIMER_STARTED, - EVENT_TIMER_RESTARTED, - EVENT_TIMER_PAUSED, - SERVICE_START, - SERVICE_PAUSE, - SERVICE_CANCEL, - SERVICE_FINISH, ) -from homeassistant.const import ATTR_ICON, ATTR_FRIENDLY_NAME, CONF_ENTITY_ID +from homeassistant.const import ( + ATTR_FRIENDLY_NAME, + ATTR_ICON, + CONF_ENTITY_ID, + SERVICE_RELOAD, +) +from homeassistant.core import Context, CoreState +from homeassistant.exceptions import Unauthorized +from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow from tests.common import async_fire_time_changed @@ -80,14 +88,11 @@ async def test_config_options(hass): assert "0:00:10" == state_2.attributes.get(ATTR_DURATION) -@asyncio.coroutine -def test_methods_and_events(hass): +async def test_methods_and_events(hass): """Test methods and events.""" hass.state = CoreState.starting - yield from async_setup_component( - hass, DOMAIN, {DOMAIN: {"test1": {CONF_DURATION: 10}}} - ) + await async_setup_component(hass, DOMAIN, {DOMAIN: {"test1": {CONF_DURATION: 10}}}) state = hass.states.get("timer.test1") assert state @@ -120,10 +125,10 @@ def test_methods_and_events(hass): expectedEvents = 0 for step in steps: if step["call"] is not None: - yield from hass.services.async_call( + await hass.services.async_call( DOMAIN, step["call"], {CONF_ENTITY_ID: "timer.test1"} ) - yield from hass.async_block_till_done() + await hass.async_block_till_done() state = hass.states.get("timer.test1") assert state @@ -136,14 +141,11 @@ def test_methods_and_events(hass): assert len(results) == expectedEvents -@asyncio.coroutine -def test_wait_till_timer_expires(hass): +async def test_wait_till_timer_expires(hass): """Test for a timer to end.""" hass.state = CoreState.starting - yield from async_setup_component( - hass, DOMAIN, {DOMAIN: {"test1": {CONF_DURATION: 10}}} - ) + await async_setup_component(hass, DOMAIN, {DOMAIN: {"test1": {CONF_DURATION: 10}}}) state = hass.states.get("timer.test1") assert state @@ -160,10 +162,10 @@ def test_wait_till_timer_expires(hass): hass.bus.async_listen(EVENT_TIMER_FINISHED, fake_event_listener) hass.bus.async_listen(EVENT_TIMER_CANCELLED, fake_event_listener) - yield from hass.services.async_call( + await hass.services.async_call( DOMAIN, SERVICE_START, {CONF_ENTITY_ID: "timer.test1"} ) - yield from hass.async_block_till_done() + await hass.async_block_till_done() state = hass.states.get("timer.test1") assert state @@ -173,7 +175,7 @@ def test_wait_till_timer_expires(hass): assert len(results) == 1 async_fire_time_changed(hass, utcnow() + timedelta(seconds=10)) - yield from hass.async_block_till_done() + await hass.async_block_till_done() state = hass.states.get("timer.test1") assert state @@ -183,15 +185,102 @@ def test_wait_till_timer_expires(hass): assert len(results) == 2 -@asyncio.coroutine -def test_no_initial_state_and_no_restore_state(hass): +async def test_no_initial_state_and_no_restore_state(hass): """Ensure that entity is create without initial and restore feature.""" hass.state = CoreState.starting - yield from async_setup_component( - hass, DOMAIN, {DOMAIN: {"test1": {CONF_DURATION: 10}}} - ) + await async_setup_component(hass, DOMAIN, {DOMAIN: {"test1": {CONF_DURATION: 10}}}) state = hass.states.get("timer.test1") assert state assert state.state == STATUS_IDLE + + +async def test_config_reload(hass, hass_admin_user, hass_read_only_user): + """Test reload service.""" + count_start = len(hass.states.async_entity_ids()) + + _LOGGER.debug("ENTITIES @ start: %s", hass.states.async_entity_ids()) + + config = { + DOMAIN: { + "test_1": {}, + "test_2": { + CONF_NAME: "Hello World", + CONF_ICON: "mdi:work", + CONF_DURATION: 10, + }, + } + } + + assert await async_setup_component(hass, "timer", config) + await hass.async_block_till_done() + + assert count_start + 2 == len(hass.states.async_entity_ids()) + await hass.async_block_till_done() + + state_1 = hass.states.get("timer.test_1") + state_2 = hass.states.get("timer.test_2") + state_3 = hass.states.get("timer.test_3") + + assert state_1 is not None + assert state_2 is not None + assert state_3 is None + + assert STATUS_IDLE == state_1.state + 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) + + with patch( + "homeassistant.config.load_yaml_config_file", + autospec=True, + return_value={ + DOMAIN: { + "test_2": { + CONF_NAME: "Hello World reloaded", + CONF_ICON: "mdi:work-reloaded", + CONF_DURATION: 20, + }, + "test_3": {}, + } + }, + ): + with patch("homeassistant.config.find_config_file", return_value=""): + with pytest.raises(Unauthorized): + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + blocking=True, + context=Context(user_id=hass_read_only_user.id), + ) + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + blocking=True, + context=Context(user_id=hass_admin_user.id), + ) + await hass.async_block_till_done() + + assert count_start + 2 == len(hass.states.async_entity_ids()) + + state_1 = hass.states.get("timer.test_1") + state_2 = hass.states.get("timer.test_2") + state_3 = hass.states.get("timer.test_3") + + assert state_1 is None + assert state_2 is not None + assert state_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 STATUS_IDLE == state_3.state + assert ATTR_ICON not in state_3.attributes + assert ATTR_FRIENDLY_NAME not in state_3.attributes diff --git a/tests/components/timer/test_reproduce_state.py b/tests/components/timer/test_reproduce_state.py index 5539d8610c3..80205a40f5d 100644 --- a/tests/components/timer/test_reproduce_state.py +++ b/tests/components/timer/test_reproduce_state.py @@ -9,6 +9,7 @@ from homeassistant.components.timer import ( STATUS_PAUSED, ) from homeassistant.core import State + from tests.common import async_mock_service diff --git a/tests/components/tod/test_binary_sensor.py b/tests/components/tod/test_binary_sensor.py index 2ef361e1dac..03581d16c09 100644 --- a/tests/components/tod/test_binary_sensor.py +++ b/tests/components/tod/test_binary_sensor.py @@ -1,16 +1,18 @@ """Test Times of the Day Binary Sensor.""" +from datetime import datetime, timedelta import unittest from unittest.mock import patch -from datetime import timedelta, datetime + import pytz from homeassistant import setup -import homeassistant.core as ha from homeassistant.const import STATE_OFF, STATE_ON -import homeassistant.util.dt as dt_util -from homeassistant.setup import setup_component -from tests.common import get_test_home_assistant, assert_setup_component +import homeassistant.core as ha from homeassistant.helpers.sun import get_astral_event_date, get_astral_event_next +from homeassistant.setup import setup_component +import homeassistant.util.dt as dt_util + +from tests.common import assert_setup_component, get_test_home_assistant class TestBinarySensorTod(unittest.TestCase): diff --git a/tests/components/tomato/test_device_tracker.py b/tests/components/tomato/test_device_tracker.py index 89ca2a6e1aa..cbc8316f7c8 100644 --- a/tests/components/tomato/test_device_tracker.py +++ b/tests/components/tomato/test_device_tracker.py @@ -1,5 +1,6 @@ """The tests for the Tomato device tracker platform.""" from unittest import mock + import pytest import requests import requests_mock @@ -9,11 +10,11 @@ from homeassistant.components.device_tracker import DOMAIN import homeassistant.components.tomato.device_tracker as tomato from homeassistant.const import ( CONF_HOST, - CONF_USERNAME, CONF_PASSWORD, + CONF_PLATFORM, CONF_PORT, CONF_SSL, - CONF_PLATFORM, + CONF_USERNAME, CONF_VERIFY_SSL, ) @@ -50,7 +51,7 @@ def mock_session_response(*args, **kwargs): def mock_exception_logger(): """Mock pyunifi.""" with mock.patch( - "homeassistant.components.tomato.device_tracker" "._LOGGER.exception" + "homeassistant.components.tomato.device_tracker._LOGGER.exception" ) as mock_exception_logger: yield mock_exception_logger @@ -311,7 +312,7 @@ def test_config_bad_credentials(hass, mock_exception_logger): assert mock_exception_logger.call_count == 1 assert mock_exception_logger.mock_calls[0] == mock.call( - "Failed to authenticate, " "please check your username and password" + "Failed to authenticate, please check your username and password" ) @@ -381,7 +382,7 @@ def test_bad_connection(hass, mock_exception_logger): tomato.get_scanner(hass, config) assert mock_exception_logger.call_count == 1 assert mock_exception_logger.mock_calls[0] == mock.call( - "Failed to connect to the router " "or invalid http_id supplied" + "Failed to connect to the router or invalid http_id supplied" ) diff --git a/tests/components/toon/test_config_flow.py b/tests/components/toon/test_config_flow.py index a4d7c760ca1..45d16908446 100644 --- a/tests/components/toon/test_config_flow.py +++ b/tests/components/toon/test_config_flow.py @@ -22,7 +22,7 @@ from homeassistant.components.toon.const import ( from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry, MockDependency +from tests.common import MockConfigEntry FIXTURE_APP = { DOMAIN: {CONF_CLIENT_ID: "1234567890abcdef", CONF_CLIENT_SECRET: "1234567890abcdef"} @@ -40,9 +40,9 @@ FIXTURE_DISPLAY = {CONF_DISPLAY: "display1"} @pytest.fixture def mock_toonapilib(): """Mock toonapilib.""" - with MockDependency("toonapilib") as mock_toonapilib_: - mock_toonapilib_.Toon().display_names = [FIXTURE_DISPLAY[CONF_DISPLAY]] - yield mock_toonapilib_ + with patch("homeassistant.components.toon.config_flow.Toon") as Toon: + Toon().display_names = [FIXTURE_DISPLAY[CONF_DISPLAY]] + yield Toon async def setup_component(hass): @@ -90,7 +90,7 @@ async def test_toon_abort(hass, mock_toonapilib, side_effect, reason): flow = config_flow.ToonFlowHandler() flow.hass = hass - mock_toonapilib.Toon.side_effect = side_effect + mock_toonapilib.side_effect = side_effect result = await flow.async_step_authenticate(user_input=FIXTURE_CREDENTIALS) @@ -100,7 +100,7 @@ async def test_toon_abort(hass, mock_toonapilib, side_effect, reason): async def test_invalid_credentials(hass, mock_toonapilib): """Test we show authentication form on Toon auth error.""" - mock_toonapilib.Toon.side_effect = InvalidCredentials + mock_toonapilib.side_effect = InvalidCredentials await setup_component(hass) @@ -140,7 +140,7 @@ async def test_no_displays(hass, mock_toonapilib): """Test abort when there are no displays.""" await setup_component(hass) - mock_toonapilib.Toon().display_names = [] + mock_toonapilib().display_names = [] flow = config_flow.ToonFlowHandler() flow.hass = hass @@ -177,7 +177,7 @@ async def test_abort_last_minute_fail(hass, mock_toonapilib): flow.hass = hass await flow.async_step_user(user_input=FIXTURE_CREDENTIALS) - mock_toonapilib.Toon.side_effect = Exception + mock_toonapilib.side_effect = Exception result = await flow.async_step_display(user_input=FIXTURE_DISPLAY) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT diff --git a/tests/components/tplink/test_init.py b/tests/components/tplink/test_init.py index 9428bf05483..97512dfc9bd 100644 --- a/tests/components/tplink/test_init.py +++ b/tests/components/tplink/test_init.py @@ -242,7 +242,7 @@ async def test_unload(hass, platform): with patch( "homeassistant.components.tplink.common.SmartDevice._query_helper" ), patch( - "homeassistant.components.tplink.{}" ".async_setup_entry".format(platform), + "homeassistant.components.tplink.{}.async_setup_entry".format(platform), return_value=mock_coro(True), ) as light_setup: config = { diff --git a/tests/components/tplink/test_light.py b/tests/components/tplink/test_light.py new file mode 100644 index 00000000000..8d1d4d94738 --- /dev/null +++ b/tests/components/tplink/test_light.py @@ -0,0 +1,220 @@ +"""Tests for light platform.""" +from unittest.mock import patch + +from pyHS100 import SmartBulb + +from homeassistant.components import tplink +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, + ATTR_HS_COLOR, + DOMAIN as LIGHT_DOMAIN, +) +from homeassistant.components.tplink.common import CONF_DISCOVERY, CONF_LIGHT +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_HOST, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + + +async def test_light(hass: HomeAssistant) -> None: + """Test function.""" + 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": "LB120", + "alias": "light1", + } + + light_state = { + "on_off": SmartBulb.BULB_STATE_ON, + "dft_on_state": { + "brightness": 12, + "color_temp": 3200, + "hue": 100, + "saturation": 200, + }, + "brightness": 13, + "color_temp": 3300, + "hue": 110, + "saturation": 210, + } + + def set_light_state(state): + nonlocal light_state + light_state.update(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, get_light_state_patch, current_consumption_patch, get_sysinfo_patch, get_emeter_daily_patch, get_emeter_monthly_patch: + 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() + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.light1"}, + blocking=True, + ) + + assert hass.states.get("light.light1").state == "off" + assert light_state["on_off"] == 0 + + await hass.async_block_till_done() + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: "light.light1", + ATTR_COLOR_TEMP: 312, + ATTR_BRIGHTNESS: 50, + }, + blocking=True, + ) + + await hass.async_block_till_done() + + state = hass.states.get("light.light1") + assert state.state == "on" + assert state.attributes["brightness"] == 48.45 + assert state.attributes["hs_color"] == (110, 210) + assert state.attributes["color_temp"] == 312 + assert light_state["on_off"] == 1 + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: "light.light1", + ATTR_BRIGHTNESS: 55, + ATTR_HS_COLOR: (23, 27), + }, + blocking=True, + ) + + await hass.async_block_till_done() + + state = hass.states.get("light.light1") + assert state.state == "on" + assert state.attributes["brightness"] == 53.55 + assert state.attributes["hs_color"] == (23, 27) + assert state.attributes["color_temp"] == 312 + assert light_state["brightness"] == 21 + assert light_state["hue"] == 23 + assert light_state["saturation"] == 27 + + light_state["on_off"] = 0 + light_state["dft_on_state"]["on_off"] = 0 + light_state["brightness"] = 66 + light_state["dft_on_state"]["brightness"] = 66 + light_state["color_temp"] = 6400 + light_state["dft_on_state"]["color_temp"] = 123 + light_state["hue"] = 77 + light_state["dft_on_state"]["hue"] = 77 + light_state["saturation"] = 78 + light_state["dft_on_state"]["saturation"] = 78 + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.light1"}, + blocking=True, + ) + + await hass.async_block_till_done() + + state = hass.states.get("light.light1") + assert state.state == "off" + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.light1"}, + blocking=True, + ) + + await hass.async_block_till_done() + + state = hass.states.get("light.light1") + assert state.attributes["brightness"] == 168.3 + assert state.attributes["hs_color"] == (77, 78) + assert state.attributes["color_temp"] == 156 + assert light_state["brightness"] == 66 + assert light_state["hue"] == 77 + assert light_state["saturation"] == 78 diff --git a/tests/components/tradfri/conftest.py b/tests/components/tradfri/conftest.py index 7ebd4bbcd7c..1c6e572b81f 100644 --- a/tests/components/tradfri/conftest.py +++ b/tests/components/tradfri/conftest.py @@ -8,6 +8,6 @@ import pytest def mock_gateway_info(): """Mock get_gateway_info.""" with patch( - "homeassistant.components.tradfri.config_flow." "get_gateway_info" + "homeassistant.components.tradfri.config_flow.get_gateway_info" ) as mock_gateway: yield mock_gateway diff --git a/tests/components/tradfri/test_config_flow.py b/tests/components/tradfri/test_config_flow.py index cc5c6be4c72..ad7386c530f 100644 --- a/tests/components/tradfri/test_config_flow.py +++ b/tests/components/tradfri/test_config_flow.py @@ -6,14 +6,14 @@ import pytest from homeassistant import data_entry_flow from homeassistant.components.tradfri import config_flow -from tests.common import mock_coro, MockConfigEntry +from tests.common import MockConfigEntry, mock_coro @pytest.fixture def mock_auth(): """Mock authenticate.""" with patch( - "homeassistant.components.tradfri.config_flow." "authenticate" + "homeassistant.components.tradfri.config_flow.authenticate" ) as mock_auth: yield mock_auth @@ -21,7 +21,7 @@ def mock_auth(): @pytest.fixture def mock_entry_setup(): """Mock entry setup.""" - with patch("homeassistant.components.tradfri." "async_setup_entry") as mock_setup: + with patch("homeassistant.components.tradfri.async_setup_entry") as mock_setup: mock_setup.return_value = mock_coro(True) yield mock_setup diff --git a/tests/components/tradfri/test_light.py b/tests/components/tradfri/test_light.py index 4c691f66af8..90449120aaf 100644 --- a/tests/components/tradfri/test_light.py +++ b/tests/components/tradfri/test_light.py @@ -1,7 +1,7 @@ """Tradfri lights platform tests.""" from copy import deepcopy -from unittest.mock import Mock, MagicMock, patch, PropertyMock +from unittest.mock import MagicMock, Mock, PropertyMock, patch import pytest from pytradfri.device import Device @@ -12,7 +12,6 @@ from homeassistant.components import tradfri from tests.common import MockConfigEntry - DEFAULT_TEST_FEATURES = { "can_set_dimmer": False, "can_set_color": False, diff --git a/tests/components/transmission/test_config_flow.py b/tests/components/transmission/test_config_flow.py index 28fbed9ff42..80e6bd55017 100644 --- a/tests/components/transmission/test_config_flow.py +++ b/tests/components/transmission/test_config_flow.py @@ -98,7 +98,6 @@ async def test_flow_works(hass, api): assert result["data"][CONF_NAME] == NAME assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_PORT] == PORT - # assert result["data"]["options"][CONF_SCAN_INTERVAL] == DEFAULT_SCAN_INTERVAL # test with all provided result = await flow.async_step_user(MOCK_ENTRY) @@ -110,7 +109,6 @@ async def test_flow_works(hass, api): assert result["data"][CONF_USERNAME] == USERNAME assert result["data"][CONF_PASSWORD] == PASSWORD assert result["data"][CONF_PORT] == PORT - # assert result["data"]["options"][CONF_SCAN_INTERVAL] == DEFAULT_SCAN_INTERVAL async def test_options(hass): diff --git a/tests/components/trend/test_binary_sensor.py b/tests/components/trend/test_binary_sensor.py index e4b5cf5df6c..d78cf793d2f 100644 --- a/tests/components/trend/test_binary_sensor.py +++ b/tests/components/trend/test_binary_sensor.py @@ -5,7 +5,7 @@ from unittest.mock import patch from homeassistant import setup import homeassistant.util.dt as dt_util -from tests.common import get_test_home_assistant, assert_setup_component +from tests.common import assert_setup_component, get_test_home_assistant class TestTrendBinarySensor: diff --git a/tests/components/tts/test_init.py b/tests/components/tts/test_init.py index 1107aace133..f8dc11069d8 100644 --- a/tests/components/tts/test_init.py +++ b/tests/components/tts/test_init.py @@ -2,27 +2,27 @@ import ctypes import os import shutil -from unittest.mock import patch, PropertyMock +from unittest.mock import PropertyMock, patch import pytest import requests -import homeassistant.components.http as http -import homeassistant.components.tts as tts from homeassistant.components.demo.tts import DemoProvider +import homeassistant.components.http as http from homeassistant.components.media_player.const import ( - SERVICE_PLAY_MEDIA, - MEDIA_TYPE_MUSIC, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, DOMAIN as DOMAIN_MP, + MEDIA_TYPE_MUSIC, + SERVICE_PLAY_MEDIA, ) -from homeassistant.setup import setup_component, async_setup_component +import homeassistant.components.tts as tts +from homeassistant.setup import async_setup_component, setup_component from tests.common import ( + assert_setup_component, get_test_home_assistant, get_test_instance_port, - assert_setup_component, mock_service, ) @@ -99,7 +99,7 @@ class TestTTS: assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC assert calls[0].data[ ATTR_MEDIA_CONTENT_ID - ] == "{}/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd" "_en_-_demo.mp3".format( + ] == "{}/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd_en_-_demo.mp3".format( self.hass.config.api.base_url ) assert os.path.isfile( @@ -129,7 +129,7 @@ class TestTTS: assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC assert calls[0].data[ ATTR_MEDIA_CONTENT_ID - ] == "{}/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd" "_de_-_demo.mp3".format( + ] == "{}/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd_de_-_demo.mp3".format( self.hass.config.api.base_url ) assert os.path.isfile( @@ -169,7 +169,7 @@ class TestTTS: assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC assert calls[0].data[ ATTR_MEDIA_CONTENT_ID - ] == "{}/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd" "_de_-_demo.mp3".format( + ] == "{}/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd_de_-_demo.mp3".format( self.hass.config.api.base_url ) assert os.path.isfile( @@ -232,7 +232,7 @@ class TestTTS: assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC assert calls[0].data[ ATTR_MEDIA_CONTENT_ID - ] == "{}/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd" "_de_{}_demo.mp3".format( + ] == "{}/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd_de_{}_demo.mp3".format( self.hass.config.api.base_url, opt_hash ) assert os.path.isfile( @@ -273,7 +273,7 @@ class TestTTS: assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC assert calls[0].data[ ATTR_MEDIA_CONTENT_ID - ] == "{}/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd" "_de_{}_demo.mp3".format( + ] == "{}/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd_de_{}_demo.mp3".format( self.hass.config.api.base_url, opt_hash ) assert os.path.isfile( @@ -449,7 +449,7 @@ class TestTTS: self.hass.start() url = ( - "{}/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd" "_en_-_demo.mp3" + "{}/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd_en_-_demo.mp3" ).format(self.hass.config.api.base_url) req = requests.get(url) @@ -465,7 +465,7 @@ class TestTTS: self.hass.start() url = ( - "{}/api/tts_proxy/265944dsk32c1b2a621be5930510bb2cd" "_en_-_demo.mp3" + "{}/api/tts_proxy/265944dsk32c1b2a621be5930510bb2cd_en_-_demo.mp3" ).format(self.hass.config.api.base_url) req = requests.get(url) @@ -542,7 +542,7 @@ class TestTTS: setup_component(self.hass, tts.DOMAIN, config) with patch( - "homeassistant.components.demo.tts.DemoProvider." "get_tts_audio", + "homeassistant.components.demo.tts.DemoProvider.get_tts_audio", return_value=(None, None), ): self.hass.services.call( @@ -555,7 +555,7 @@ class TestTTS: assert len(calls) == 1 assert calls[0].data[ ATTR_MEDIA_CONTENT_ID - ] == "{}/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd" "_en_-_demo.mp3".format( + ] == "{}/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd_en_-_demo.mp3".format( self.hass.config.api.base_url ) @@ -601,7 +601,7 @@ class TestTTS: self.hass.start() url = ( - "{}/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd" "_en_-_demo.mp3" + "{}/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd_en_-_demo.mp3" ).format(self.hass.config.api.base_url) req = requests.get(url) diff --git a/tests/components/twilio/test_init.py b/tests/components/twilio/test_init.py index 5795967e492..196d0e99991 100644 --- a/tests/components/twilio/test_init.py +++ b/tests/components/twilio/test_init.py @@ -4,6 +4,7 @@ from unittest.mock import patch from homeassistant import data_entry_flow from homeassistant.components import twilio from homeassistant.core import callback + from tests.common import MockDependency diff --git a/tests/components/uk_transport/test_sensor.py b/tests/components/uk_transport/test_sensor.py index c55de0dd389..ab115d02187 100644 --- a/tests/components/uk_transport/test_sensor.py +++ b/tests/components/uk_transport/test_sensor.py @@ -1,23 +1,24 @@ """The tests for the uk_transport platform.""" import re - -import requests_mock import unittest +import requests_mock + from homeassistant.components.uk_transport.sensor import ( - UkTransportSensor, ATTR_ATCOCODE, - ATTR_LOCALITY, - ATTR_STOP_NAME, - ATTR_NEXT_BUSES, - ATTR_STATION_CODE, ATTR_CALLING_AT, + ATTR_LOCALITY, + ATTR_NEXT_BUSES, ATTR_NEXT_TRAINS, - CONF_API_APP_KEY, + ATTR_STATION_CODE, + ATTR_STOP_NAME, CONF_API_APP_ID, + CONF_API_APP_KEY, + UkTransportSensor, ) from homeassistant.setup import setup_component -from tests.common import load_fixture, get_test_home_assistant + +from tests.common import get_test_home_assistant, load_fixture BUS_ATCOCODE = "340000368SHE" BUS_DIRECTION = "Wantage" diff --git a/tests/components/unifi/test_config_flow.py b/tests/components/unifi/test_config_flow.py index aea4d565f3d..1b973aee9a5 100644 --- a/tests/components/unifi/test_config_flow.py +++ b/tests/components/unifi/test_config_flow.py @@ -1,10 +1,10 @@ """Test UniFi config flow.""" +import aiounifi from asynctest import patch from homeassistant.components import unifi from homeassistant.components.unifi import config_flow from homeassistant.components.unifi.const import CONF_CONTROLLER, CONF_SITE_ID - from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -15,8 +15,6 @@ from homeassistant.const import ( from tests.common import MockConfigEntry -import aiounifi - async def test_flow_works(hass, aioclient_mock): """Test config flow.""" diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py index 2b64e56cd99..ddd16f948cb 100644 --- a/tests/components/unifi/test_controller.py +++ b/tests/components/unifi/test_controller.py @@ -2,12 +2,11 @@ from collections import deque from datetime import timedelta +import aiounifi from asynctest import Mock, patch - import pytest from homeassistant import config_entries -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.components import unifi from homeassistant.components.unifi.const import ( CONF_CONTROLLER, @@ -22,7 +21,7 @@ from homeassistant.const import ( CONF_USERNAME, CONF_VERIFY_SSL, ) -import aiounifi +from homeassistant.exceptions import ConfigEntryNotReady CONTROLLER_HOST = { "hostname": "controller_host", @@ -60,6 +59,7 @@ async def setup_unifi_integration( clients_response, devices_response, clients_all_response, + known_wireless_clients=None, ): """Create the UniFi controller.""" if UNIFI_CONFIG not in hass.data: @@ -77,6 +77,11 @@ async def setup_unifi_integration( entry_id=1, ) + if known_wireless_clients: + hass.data[UNIFI_WIRELESS_CLIENTS].update_data( + known_wireless_clients, config_entry + ) + mock_client_responses = deque() mock_client_responses.append(clients_response) diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 29b16553757..5c1505653a3 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -2,8 +2,11 @@ from copy import copy from datetime import timedelta +from asynctest import patch + from homeassistant import config_entries from homeassistant.components import unifi +import homeassistant.components.device_tracker as device_tracker from homeassistant.components.unifi.const import ( CONF_SSID_FILTER, CONF_TRACK_DEVICES, @@ -12,14 +15,10 @@ from homeassistant.components.unifi.const import ( from homeassistant.const import STATE_UNAVAILABLE from homeassistant.helpers import entity_registry from homeassistant.setup import async_setup_component - -import homeassistant.components.device_tracker as device_tracker import homeassistant.util.dt as dt_util from .test_controller import ENTRY_CONFIG, SITES, setup_unifi_integration -DEFAULT_DETECTION_TIME = timedelta(seconds=300) - CLIENT_1 = { "essid": "ssid", "hostname": "client_1", @@ -44,6 +43,14 @@ CLIENT_3 = { "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", +} DEVICE_1 = { "board_rev": 3, @@ -103,16 +110,20 @@ async def test_no_clients(hass): async def test_tracked_devices(hass): """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()) + controller = await setup_unifi_integration( hass, ENTRY_CONFIG, options={CONF_SSID_FILTER: ["ssid"]}, sites=SITES, - clients_response=[CLIENT_1, CLIENT_2, CLIENT_3], + clients_response=[CLIENT_1, CLIENT_2, CLIENT_3, client_4_copy], devices_response=[DEVICE_1, DEVICE_2], clients_all_response={}, + known_wireless_clients=(CLIENT_4["mac"],), ) - assert len(hass.states.async_all()) == 5 + assert len(hass.states.async_all()) == 6 client_1 = hass.states.get("device_tracker.client_1") assert client_1 is not None @@ -125,6 +136,11 @@ async def test_tracked_devices(hass): client_3 = hass.states.get("device_tracker.client_3") assert client_3 is None + # 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" + device_1 = hass.states.get("device_tracker.device_1") assert device_1 is not None assert device_1.state == "not_home" @@ -203,7 +219,20 @@ async def test_wireless_client_go_wired_issue(hass): await hass.async_block_till_done() client_1 = hass.states.get("device_tracker.client_1") - assert client_1.state == "not_home" + assert client_1.state == "home" + + with patch.object( + unifi.device_tracker.dt_util, + "utcnow", + return_value=(dt_util.utcnow() + timedelta(minutes=5)), + ): + controller.mock_client_responses.append([client_1_client]) + controller.mock_device_responses.append({}) + await controller.async_update() + await hass.async_block_till_done() + + client_1 = hass.states.get("device_tracker.client_1") + assert client_1.state == "not_home" client_1_client["is_wired"] = False client_1_client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) diff --git a/tests/components/unifi/test_init.py b/tests/components/unifi/test_init.py index 6b17b803390..1f5a3852e16 100644 --- a/tests/components/unifi/test_init.py +++ b/tests/components/unifi/test_init.py @@ -2,11 +2,9 @@ from unittest.mock import Mock, patch from homeassistant.components import unifi - from homeassistant.setup import async_setup_component - -from tests.common import mock_coro, MockConfigEntry +from tests.common import MockConfigEntry, mock_coro async def test_setup_with_no_config(hass): diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index f591801a966..8819f2f33ae 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -2,9 +2,8 @@ from copy import deepcopy from homeassistant.components import unifi -from homeassistant.setup import async_setup_component - import homeassistant.components.sensor as sensor +from homeassistant.setup import async_setup_component from .test_controller import ENTRY_CONFIG, SITES, setup_unifi_integration diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py index 3d754fb5dff..30f9e990421 100644 --- a/tests/components/unifi/test_switch.py +++ b/tests/components/unifi/test_switch.py @@ -3,11 +3,10 @@ from copy import deepcopy from homeassistant import config_entries from homeassistant.components import unifi +import homeassistant.components.switch as switch from homeassistant.helpers import entity_registry from homeassistant.setup import async_setup_component -import homeassistant.components.switch as switch - from .test_controller import ( CONTROLLER_HOST, ENTRY_CONFIG, diff --git a/tests/components/unifi_direct/test_device_tracker.py b/tests/components/unifi_direct/test_device_tracker.py index cda6dff8413..3d62a451af2 100644 --- a/tests/components/unifi_direct/test_device_tracker.py +++ b/tests/components/unifi_direct/test_device_tracker.py @@ -1,29 +1,29 @@ """The tests for the Unifi direct device tracker platform.""" -import os from datetime import timedelta -from asynctest import mock, patch +import os +from asynctest import mock, patch import pytest import voluptuous as vol -from homeassistant.setup import async_setup_component -from homeassistant.components.device_tracker.legacy import YAML_DEVICES from homeassistant.components.device_tracker import ( - CONF_CONSIDER_HOME, - CONF_TRACK_NEW, CONF_AWAY_HIDE, + CONF_CONSIDER_HOME, CONF_NEW_DEVICE_DEFAULTS, + CONF_TRACK_NEW, ) +from homeassistant.components.device_tracker.legacy import YAML_DEVICES from homeassistant.components.unifi_direct.device_tracker import ( - DOMAIN, CONF_PORT, + DOMAIN, PLATFORM_SCHEMA, _response_to_json, get_scanner, ) -from homeassistant.const import CONF_PLATFORM, CONF_PASSWORD, CONF_USERNAME, CONF_HOST +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PLATFORM, CONF_USERNAME +from homeassistant.setup import async_setup_component -from tests.common import assert_setup_component, mock_component, load_fixture +from tests.common import assert_setup_component, load_fixture, mock_component scanner_path = ( "homeassistant.components.unifi_direct.device_tracker." + "UnifiDeviceScanner" diff --git a/tests/components/universal/test_media_player.py b/tests/components/universal/test_media_player.py index 67d826f576b..cf985621351 100644 --- a/tests/components/universal/test_media_player.py +++ b/tests/components/universal/test_media_player.py @@ -5,14 +5,14 @@ import unittest from voluptuous.error import MultipleInvalid -from homeassistant.const import STATE_OFF, STATE_ON, STATE_PLAYING, STATE_PAUSED -import homeassistant.components.switch as switch import homeassistant.components.input_number as input_number import homeassistant.components.input_select as input_select import homeassistant.components.media_player as media_player +import homeassistant.components.switch as switch import homeassistant.components.universal.media_player as universal +from homeassistant.const import STATE_OFF, STATE_ON, STATE_PAUSED, STATE_PLAYING -from tests.common import mock_service, get_test_home_assistant +from tests.common import get_test_home_assistant, mock_service def validate_config(config): @@ -539,10 +539,10 @@ class TestMediaPlayer(unittest.TestCase): ump = universal.UniversalMediaPlayer(self.hass, **config) - assert "0" == ump.volume_level + assert 0 == ump.volume_level self.hass.states.set(self.mock_volume_id, 100) - assert "100" == ump.volume_level + assert 100 == ump.volume_level def test_is_volume_muted_children_and_attr(self): """Test is volume muted property w/ children and attrs.""" diff --git a/tests/components/upnp/test_init.py b/tests/components/upnp/test_init.py index 5e2106ff208..86ed017ae8d 100644 --- a/tests/components/upnp/test_init.py +++ b/tests/components/upnp/test_init.py @@ -1,23 +1,21 @@ """Test UPnP/IGD setup process.""" from ipaddress import ip_address -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch -from homeassistant.setup import async_setup_component from homeassistant.components import upnp from homeassistant.components.upnp.device import Device from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry -from tests.common import MockDependency -from tests.common import mock_coro +from tests.common import MockConfigEntry, MockDependency, mock_coro class MockDevice(Device): """Mock device for Device.""" def __init__(self, udn): - """Initializer.""" + """Initialize mock device.""" device = MagicMock() device.manufacturer = "mock-manuf" device.name = "mock-name" diff --git a/tests/components/uptime/test_sensor.py b/tests/components/uptime/test_sensor.py index b3dcddfba6a..0a9d227681b 100644 --- a/tests/components/uptime/test_sensor.py +++ b/tests/components/uptime/test_sensor.py @@ -1,11 +1,12 @@ """The tests for the uptime sensor platform.""" import asyncio +from datetime import timedelta import unittest from unittest.mock import patch -from datetime import timedelta -from homeassistant.setup import setup_component from homeassistant.components.uptime.sensor import UptimeSensor +from homeassistant.setup import setup_component + from tests.common import get_test_home_assistant diff --git a/tests/components/usgs_earthquakes_feed/test_geo_location.py b/tests/components/usgs_earthquakes_feed/test_geo_location.py index 65ceec4d425..646878c97bd 100644 --- a/tests/components/usgs_earthquakes_feed/test_geo_location.py +++ b/tests/components/usgs_earthquakes_feed/test_geo_location.py @@ -1,37 +1,38 @@ """The tests for the USGS Earthquake Hazards Program Feed platform.""" import datetime -from unittest.mock import patch, MagicMock, call +from unittest.mock import MagicMock, call, patch from homeassistant.components import geo_location from homeassistant.components.geo_location import ATTR_SOURCE from homeassistant.components.usgs_earthquakes_feed.geo_location import ( ATTR_ALERT, ATTR_EXTERNAL_ID, - SCAN_INTERVAL, - ATTR_PLACE, ATTR_MAGNITUDE, + ATTR_PLACE, ATTR_STATUS, - ATTR_TYPE, ATTR_TIME, + ATTR_TYPE, ATTR_UPDATED, CONF_FEED_TYPE, + SCAN_INTERVAL, ) from homeassistant.const import ( - EVENT_HOMEASSISTANT_START, - CONF_RADIUS, + ATTR_ATTRIBUTION, + ATTR_FRIENDLY_NAME, + ATTR_ICON, ATTR_LATITUDE, ATTR_LONGITUDE, - ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, - ATTR_ATTRIBUTION, CONF_LATITUDE, CONF_LONGITUDE, - ATTR_ICON, + CONF_RADIUS, + EVENT_HOMEASSISTANT_START, ) from homeassistant.setup import async_setup_component -from tests.common import assert_setup_component, async_fire_time_changed import homeassistant.util.dt as dt_util +from tests.common import assert_setup_component, async_fire_time_changed + CONFIG = { geo_location.DOMAIN: [ { diff --git a/tests/components/utility_meter/test_init.py b/tests/components/utility_meter/test_init.py index 784a3647b8d..719ea9445cc 100644 --- a/tests/components/utility_meter/test_init.py +++ b/tests/components/utility_meter/test_init.py @@ -1,20 +1,19 @@ """The tests for the utility_meter component.""" -import logging - from datetime import timedelta +import logging from unittest.mock import patch -from homeassistant.const import EVENT_HOMEASSISTANT_START, ATTR_ENTITY_ID +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.utility_meter.const import ( - SERVICE_RESET, - SERVICE_SELECT_TARIFF, - SERVICE_SELECT_NEXT_TARIFF, ATTR_TARIFF, + DOMAIN, + SERVICE_RESET, + SERVICE_SELECT_NEXT_TARIFF, + SERVICE_SELECT_TARIFF, ) +from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from homeassistant.components.utility_meter.const import DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/utility_meter/test_sensor.py b/tests/components/utility_meter/test_sensor.py index 1bdaa01c2e6..fcfe97804e4 100644 --- a/tests/components/utility_meter/test_sensor.py +++ b/tests/components/utility_meter/test_sensor.py @@ -1,20 +1,20 @@ """The tests for the utility_meter sensor platform.""" -import logging - -from datetime import timedelta -from unittest.mock import patch from contextlib import contextmanager +from datetime import timedelta +import logging +from unittest.mock import patch -from tests.common import async_fire_time_changed -from homeassistant.const import EVENT_HOMEASSISTANT_START, ATTR_ENTITY_ID -from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.utility_meter.const import ( + ATTR_TARIFF, DOMAIN, SERVICE_SELECT_TARIFF, - ATTR_TARIFF, ) -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util + +from tests.common import async_fire_time_changed _LOGGER = logging.getLogger(__name__) @@ -240,6 +240,13 @@ async def test_self_reset_monthly(hass): ) +async def test_self_reset_quarterly(hass): + """Test quarterly reset of meter.""" + await _test_self_reset( + hass, gen_config("quarterly"), "2017-03-31T23:59:00.000000+00:00" + ) + + async def test_self_reset_yearly(hass): """Test yearly reset of meter.""" await _test_self_reset( diff --git a/tests/components/uvc/test_camera.py b/tests/components/uvc/test_camera.py index f4be8ac6a8b..c77b5d83749 100644 --- a/tests/components/uvc/test_camera.py +++ b/tests/components/uvc/test_camera.py @@ -3,15 +3,15 @@ import socket import unittest from unittest import mock +import pytest import requests -from uvcclient import camera -from uvcclient import nvr +from uvcclient import camera, nvr +from homeassistant.components.uvc import camera as uvc from homeassistant.exceptions import PlatformNotReady from homeassistant.setup import setup_component -from homeassistant.components.uvc import camera as uvc + from tests.common import get_test_home_assistant -import pytest class TestUVCSetup(unittest.TestCase): diff --git a/tests/components/vacuum/common.py b/tests/components/vacuum/common.py index 7d9f645449f..6cecbda9968 100644 --- a/tests/components/vacuum/common.py +++ b/tests/components/vacuum/common.py @@ -10,16 +10,17 @@ from homeassistant.components.vacuum import ( SERVICE_CLEAN_SPOT, SERVICE_LOCATE, SERVICE_PAUSE, + SERVICE_RETURN_TO_BASE, SERVICE_SEND_COMMAND, SERVICE_SET_FAN_SPEED, SERVICE_START, SERVICE_START_PAUSE, SERVICE_STOP, - SERVICE_RETURN_TO_BASE, ) from homeassistant.const import ( ATTR_COMMAND, ATTR_ENTITY_ID, + ENTITY_MATCH_ALL, SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, @@ -28,132 +29,132 @@ from homeassistant.loader import bind_hass @bind_hass -def turn_on(hass, entity_id=None): +def turn_on(hass, entity_id=ENTITY_MATCH_ALL): """Turn all or specified vacuum on.""" hass.add_job(async_turn_on, hass, entity_id) -async def async_turn_on(hass, entity_id=None): +async def async_turn_on(hass, entity_id=ENTITY_MATCH_ALL): """Turn all or specified vacuum on.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None await hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data, blocking=True) @bind_hass -def turn_off(hass, entity_id=None): +def turn_off(hass, entity_id=ENTITY_MATCH_ALL): """Turn all or specified vacuum off.""" hass.add_job(async_turn_off, hass, entity_id) -async def async_turn_off(hass, entity_id=None): +async def async_turn_off(hass, entity_id=ENTITY_MATCH_ALL): """Turn all or specified vacuum off.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None await hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data, blocking=True) @bind_hass -def toggle(hass, entity_id=None): +def toggle(hass, entity_id=ENTITY_MATCH_ALL): """Toggle all or specified vacuum.""" hass.add_job(async_toggle, hass, entity_id) -async def async_toggle(hass, entity_id=None): +async def async_toggle(hass, entity_id=ENTITY_MATCH_ALL): """Toggle all or specified vacuum.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None await hass.services.async_call(DOMAIN, SERVICE_TOGGLE, data, blocking=True) @bind_hass -def locate(hass, entity_id=None): +def locate(hass, entity_id=ENTITY_MATCH_ALL): """Locate all or specified vacuum.""" hass.add_job(async_locate, hass, entity_id) -async def async_locate(hass, entity_id=None): +async def async_locate(hass, entity_id=ENTITY_MATCH_ALL): """Locate all or specified vacuum.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None await hass.services.async_call(DOMAIN, SERVICE_LOCATE, data, blocking=True) @bind_hass -def clean_spot(hass, entity_id=None): +def clean_spot(hass, entity_id=ENTITY_MATCH_ALL): """Tell all or specified vacuum to perform a spot clean-up.""" hass.add_job(async_clean_spot, hass, entity_id) -async def async_clean_spot(hass, entity_id=None): +async def async_clean_spot(hass, entity_id=ENTITY_MATCH_ALL): """Tell all or specified vacuum to perform a spot clean-up.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None await hass.services.async_call(DOMAIN, SERVICE_CLEAN_SPOT, data, blocking=True) @bind_hass -def return_to_base(hass, entity_id=None): +def return_to_base(hass, entity_id=ENTITY_MATCH_ALL): """Tell all or specified vacuum to return to base.""" hass.add_job(async_return_to_base, hass, entity_id) -async def async_return_to_base(hass, entity_id=None): +async def async_return_to_base(hass, entity_id=ENTITY_MATCH_ALL): """Tell all or specified vacuum to return to base.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None await hass.services.async_call(DOMAIN, SERVICE_RETURN_TO_BASE, data, blocking=True) @bind_hass -def start_pause(hass, entity_id=None): +def start_pause(hass, entity_id=ENTITY_MATCH_ALL): """Tell all or specified vacuum to start or pause the current task.""" hass.add_job(async_start_pause, hass, entity_id) -async def async_start_pause(hass, entity_id=None): +async def async_start_pause(hass, entity_id=ENTITY_MATCH_ALL): """Tell all or specified vacuum to start or pause the current task.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None await hass.services.async_call(DOMAIN, SERVICE_START_PAUSE, data, blocking=True) @bind_hass -def start(hass, entity_id=None): +def start(hass, entity_id=ENTITY_MATCH_ALL): """Tell all or specified vacuum to start or resume the current task.""" hass.add_job(async_start, hass, entity_id) -async def async_start(hass, entity_id=None): +async def async_start(hass, entity_id=ENTITY_MATCH_ALL): """Tell all or specified vacuum to start or resume the current task.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None await hass.services.async_call(DOMAIN, SERVICE_START, data, blocking=True) @bind_hass -def pause(hass, entity_id=None): +def pause(hass, entity_id=ENTITY_MATCH_ALL): """Tell all or the specified vacuum to pause the current task.""" hass.add_job(async_pause, hass, entity_id) -async def async_pause(hass, entity_id=None): +async def async_pause(hass, entity_id=ENTITY_MATCH_ALL): """Tell all or the specified vacuum to pause the current task.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None await hass.services.async_call(DOMAIN, SERVICE_PAUSE, data, blocking=True) @bind_hass -def stop(hass, entity_id=None): +def stop(hass, entity_id=ENTITY_MATCH_ALL): """Stop all or specified vacuum.""" hass.add_job(async_stop, hass, entity_id) -async def async_stop(hass, entity_id=None): +async def async_stop(hass, entity_id=ENTITY_MATCH_ALL): """Stop all or specified vacuum.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None await hass.services.async_call(DOMAIN, SERVICE_STOP, data, blocking=True) @bind_hass -def set_fan_speed(hass, fan_speed, entity_id=None): +def set_fan_speed(hass, fan_speed, entity_id=ENTITY_MATCH_ALL): """Set fan speed for all or specified vacuum.""" hass.add_job(async_set_fan_speed, hass, fan_speed, entity_id) -async def async_set_fan_speed(hass, fan_speed, entity_id=None): +async def async_set_fan_speed(hass, fan_speed, entity_id=ENTITY_MATCH_ALL): """Set fan speed for all or specified vacuum.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} data[ATTR_FAN_SPEED] = fan_speed @@ -161,12 +162,12 @@ async def async_set_fan_speed(hass, fan_speed, entity_id=None): @bind_hass -def send_command(hass, command, params=None, entity_id=None): +def send_command(hass, command, params=None, entity_id=ENTITY_MATCH_ALL): """Send command to all or specified vacuum.""" hass.add_job(async_send_command, hass, command, params, entity_id) -async def async_send_command(hass, command, params=None, entity_id=None): +async def async_send_command(hass, command, params=None, entity_id=ENTITY_MATCH_ALL): """Send command to all or specified vacuum.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} data[ATTR_COMMAND] = command diff --git a/tests/components/vacuum/test_device_action.py b/tests/components/vacuum/test_device_action.py index a7c0859408f..47ce5423f7d 100644 --- a/tests/components/vacuum/test_device_action.py +++ b/tests/components/vacuum/test_device_action.py @@ -1,18 +1,18 @@ """The tests for Vacuum device actions.""" import pytest -from homeassistant.components.vacuum import DOMAIN -from homeassistant.setup import async_setup_component import homeassistant.components.automation as automation +from homeassistant.components.vacuum import DOMAIN from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, assert_lists_same, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, - async_get_device_automations, ) diff --git a/tests/components/vacuum/test_device_condition.py b/tests/components/vacuum/test_device_condition.py index 80e7b72c36f..7be944305da 100644 --- a/tests/components/vacuum/test_device_condition.py +++ b/tests/components/vacuum/test_device_condition.py @@ -1,23 +1,23 @@ """The tests for Vacuum device conditions.""" import pytest +import homeassistant.components.automation as automation from homeassistant.components.vacuum import ( DOMAIN, STATE_CLEANING, STATE_DOCKED, STATE_RETURNING, ) -from homeassistant.setup import async_setup_component -import homeassistant.components.automation as automation from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, assert_lists_same, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, - async_get_device_automations, ) diff --git a/tests/components/vacuum/test_device_trigger.py b/tests/components/vacuum/test_device_trigger.py index 680b6482186..1f9666b1774 100644 --- a/tests/components/vacuum/test_device_trigger.py +++ b/tests/components/vacuum/test_device_trigger.py @@ -1,18 +1,18 @@ """The tests for Vacuum device triggers.""" import pytest -from homeassistant.components.vacuum import DOMAIN, STATE_DOCKED, STATE_CLEANING -from homeassistant.setup import async_setup_component 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 from tests.common import ( MockConfigEntry, assert_lists_same, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, - async_get_device_automations, ) diff --git a/tests/components/velbus/test_config_flow.py b/tests/components/velbus/test_config_flow.py index 271f0b3dd3a..66273e01f43 100644 --- a/tests/components/velbus/test_config_flow.py +++ b/tests/components/velbus/test_config_flow.py @@ -1,11 +1,11 @@ """Tests for the Velbus config flow.""" -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch import pytest from homeassistant import data_entry_flow from homeassistant.components.velbus import config_flow -from homeassistant.const import CONF_PORT, CONF_NAME +from homeassistant.const import CONF_NAME, CONF_PORT from tests.common import MockConfigEntry diff --git a/tests/components/vera/__init__.py b/tests/components/vera/__init__.py new file mode 100644 index 00000000000..4e0919be042 --- /dev/null +++ b/tests/components/vera/__init__.py @@ -0,0 +1 @@ +"""Tests for the Vera component.""" diff --git a/tests/components/vera/common.py b/tests/components/vera/common.py new file mode 100644 index 00000000000..bd87e8bc9f2 --- /dev/null +++ b/tests/components/vera/common.py @@ -0,0 +1,61 @@ +"""Common code for tests.""" + +from typing import Callable, NamedTuple, Tuple + +from mock import MagicMock +from pyvera import VeraController, VeraDevice, VeraScene + +from homeassistant.components.vera import CONF_CONTROLLER, DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +ComponentData = NamedTuple("ComponentData", (("controller", VeraController),)) + + +class ComponentFactory: + """Factory class.""" + + def __init__(self, init_controller_mock): + """Initialize component factory.""" + self.init_controller_mock = init_controller_mock + + async def configure_component( + self, + hass: HomeAssistant, + devices: Tuple[VeraDevice] = (), + scenes: Tuple[VeraScene] = (), + setup_callback: Callable[[VeraController], None] = None, + ) -> ComponentData: + """Configure the component with specific mock data.""" + controller_url = "http://127.0.0.1:123" + + hass_config = { + DOMAIN: {CONF_CONTROLLER: controller_url}, + } + + controller = MagicMock(spec=VeraController) # type: VeraController + controller.base_url = controller_url + controller.register = MagicMock() + controller.get_devices = MagicMock(return_value=devices or ()) + controller.get_scenes = MagicMock(return_value=scenes or ()) + + for vera_obj in controller.get_devices() + controller.get_scenes(): + vera_obj.vera_controller = controller + + controller.get_devices.reset_mock() + controller.get_scenes.reset_mock() + + if setup_callback: + setup_callback(controller, hass_config) + + def init_controller(base_url: str) -> list: + nonlocal controller + return [controller, True] + + self.init_controller_mock.side_effect = init_controller + + # Setup home assistant. + assert await async_setup_component(hass, DOMAIN, hass_config) + await hass.async_block_till_done() + + return ComponentData(controller=controller) diff --git a/tests/components/vera/conftest.py b/tests/components/vera/conftest.py new file mode 100644 index 00000000000..b94a40135d8 --- /dev/null +++ b/tests/components/vera/conftest.py @@ -0,0 +1,13 @@ +"""Fixtures for tests.""" + +from mock import patch +import pytest + +from .common import ComponentFactory + + +@pytest.fixture() +def vera_component_factory(): + """Return a factory for initializing the vera component.""" + with patch("pyvera.init_controller") as init_controller_mock: + yield ComponentFactory(init_controller_mock) diff --git a/tests/components/vera/test_binary_sensor.py b/tests/components/vera/test_binary_sensor.py new file mode 100644 index 00000000000..2c2e2b86388 --- /dev/null +++ b/tests/components/vera/test_binary_sensor.py @@ -0,0 +1,38 @@ +"""Vera tests.""" +from unittest.mock import MagicMock + +from pyvera import VeraBinarySensor + +from homeassistant.core import HomeAssistant + +from .common import ComponentFactory + + +async def test_binary_sensor( + hass: HomeAssistant, vera_component_factory: ComponentFactory +) -> None: + """Test function.""" + vera_device = MagicMock(spec=VeraBinarySensor) # type: VeraBinarySensor + vera_device.device_id = 1 + vera_device.name = "dev1" + vera_device.is_tripped = False + entity_id = "binary_sensor.dev1_1" + + component_data = await vera_component_factory.configure_component( + hass=hass, devices=(vera_device,) + ) + controller = component_data.controller + + update_callback = controller.register.call_args_list[0][0][1] + + vera_device.is_tripped = False + update_callback(vera_device) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == "off" + controller.register.reset_mock() + + vera_device.is_tripped = True + update_callback(vera_device) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == "on" + controller.register.reset_mock() diff --git a/tests/components/vera/test_climate.py b/tests/components/vera/test_climate.py new file mode 100644 index 00000000000..c27a72865fd --- /dev/null +++ b/tests/components/vera/test_climate.py @@ -0,0 +1,155 @@ +"""Vera tests.""" +from unittest.mock import MagicMock + +from pyvera import CATEGORY_THERMOSTAT, VeraController, VeraThermostat + +from homeassistant.components.climate.const import ( + FAN_AUTO, + FAN_ON, + HVAC_MODE_COOL, + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, +) +from homeassistant.core import HomeAssistant + +from .common import ComponentFactory + + +async def test_climate( + hass: HomeAssistant, vera_component_factory: ComponentFactory +) -> None: + """Test function.""" + vera_device = MagicMock(spec=VeraThermostat) # type: VeraThermostat + vera_device.device_id = 1 + vera_device.name = "dev1" + vera_device.category = CATEGORY_THERMOSTAT + vera_device.power = 10 + vera_device.get_current_temperature.return_value = 71 + vera_device.get_hvac_mode.return_value = "Off" + vera_device.get_current_goal_temperature.return_value = 72 + entity_id = "climate.dev1_1" + + component_data = await vera_component_factory.configure_component( + hass=hass, devices=(vera_device,), + ) + controller = component_data.controller + update_callback = controller.register.call_args_list[0][0][1] + + assert hass.states.get(entity_id).state == HVAC_MODE_OFF + + await hass.services.async_call( + "climate", + "set_hvac_mode", + {"entity_id": entity_id, "hvac_mode": HVAC_MODE_COOL}, + ) + await hass.async_block_till_done() + vera_device.turn_cool_on.assert_called() + vera_device.get_hvac_mode.return_value = "CoolOn" + update_callback(vera_device) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == HVAC_MODE_COOL + + await hass.services.async_call( + "climate", + "set_hvac_mode", + {"entity_id": entity_id, "hvac_mode": HVAC_MODE_HEAT}, + ) + await hass.async_block_till_done() + vera_device.turn_heat_on.assert_called() + vera_device.get_hvac_mode.return_value = "HeatOn" + update_callback(vera_device) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == HVAC_MODE_HEAT + + await hass.services.async_call( + "climate", + "set_hvac_mode", + {"entity_id": entity_id, "hvac_mode": HVAC_MODE_HEAT_COOL}, + ) + await hass.async_block_till_done() + vera_device.turn_auto_on.assert_called() + vera_device.get_hvac_mode.return_value = "AutoChangeOver" + update_callback(vera_device) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == HVAC_MODE_HEAT_COOL + + await hass.services.async_call( + "climate", + "set_hvac_mode", + {"entity_id": entity_id, "hvac_mode": HVAC_MODE_OFF}, + ) + await hass.async_block_till_done() + vera_device.turn_auto_on.assert_called() + vera_device.get_hvac_mode.return_value = "Off" + update_callback(vera_device) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == HVAC_MODE_OFF + + await hass.services.async_call( + "climate", "set_fan_mode", {"entity_id": entity_id, "fan_mode": "on"}, + ) + await hass.async_block_till_done() + vera_device.turn_auto_on.assert_called() + vera_device.get_fan_mode.return_value = "ContinuousOn" + update_callback(vera_device) + await hass.async_block_till_done() + assert hass.states.get(entity_id).attributes["fan_mode"] == FAN_ON + + await hass.services.async_call( + "climate", "set_fan_mode", {"entity_id": entity_id, "fan_mode": "off"}, + ) + await hass.async_block_till_done() + vera_device.turn_auto_on.assert_called() + vera_device.get_fan_mode.return_value = "Auto" + update_callback(vera_device) + await hass.async_block_till_done() + assert hass.states.get(entity_id).attributes["fan_mode"] == FAN_AUTO + + await hass.services.async_call( + "climate", "set_temperature", {"entity_id": entity_id, "temperature": 30}, + ) + await hass.async_block_till_done() + vera_device.set_temperature.assert_called_with(30) + vera_device.get_current_goal_temperature.return_value = 30 + vera_device.get_current_temperature.return_value = 25 + update_callback(vera_device) + await hass.async_block_till_done() + assert hass.states.get(entity_id).attributes["current_temperature"] == 25 + assert hass.states.get(entity_id).attributes["temperature"] == 30 + + +async def test_climate_f( + hass: HomeAssistant, vera_component_factory: ComponentFactory +) -> None: + """Test function.""" + vera_device = MagicMock(spec=VeraThermostat) # type: VeraThermostat + vera_device.device_id = 1 + vera_device.name = "dev1" + vera_device.category = CATEGORY_THERMOSTAT + vera_device.power = 10 + vera_device.get_current_temperature.return_value = 71 + vera_device.get_hvac_mode.return_value = "Off" + vera_device.get_current_goal_temperature.return_value = 72 + entity_id = "climate.dev1_1" + + def setup_callback(controller: VeraController, hass_config: dict) -> None: + controller.temperature_units = "F" + + component_data = await vera_component_factory.configure_component( + hass=hass, devices=(vera_device,), setup_callback=setup_callback + ) + controller = component_data.controller + update_callback = controller.register.call_args_list[0][0][1] + + await hass.services.async_call( + "climate", "set_temperature", {"entity_id": entity_id, "temperature": 30}, + ) + await hass.async_block_till_done() + vera_device.set_temperature.assert_called_with(86) + vera_device.get_current_goal_temperature.return_value = 30 + vera_device.get_current_temperature.return_value = 25 + update_callback(vera_device) + await hass.async_block_till_done() + assert hass.states.get(entity_id).attributes["current_temperature"] == -3.9 + assert hass.states.get(entity_id).attributes["temperature"] == -1.1 diff --git a/tests/components/vera/test_cover.py b/tests/components/vera/test_cover.py new file mode 100644 index 00000000000..79cb4adedfb --- /dev/null +++ b/tests/components/vera/test_cover.py @@ -0,0 +1,76 @@ +"""Vera tests.""" +from unittest.mock import MagicMock + +from pyvera import CATEGORY_CURTAIN, VeraCurtain + +from homeassistant.core import HomeAssistant + +from .common import ComponentFactory + + +async def test_cover( + hass: HomeAssistant, vera_component_factory: ComponentFactory +) -> None: + """Test function.""" + vera_device = MagicMock(spec=VeraCurtain) # type: VeraCurtain + vera_device.device_id = 1 + vera_device.name = "dev1" + vera_device.category = CATEGORY_CURTAIN + vera_device.is_closed = False + vera_device.get_level.return_value = 0 + entity_id = "cover.dev1_1" + + component_data = await vera_component_factory.configure_component( + hass=hass, devices=(vera_device,), + ) + controller = component_data.controller + update_callback = controller.register.call_args_list[0][0][1] + + assert hass.states.get(entity_id).state == "closed" + assert hass.states.get(entity_id).attributes["current_position"] == 0 + + await hass.services.async_call( + "cover", "open_cover", {"entity_id": entity_id}, + ) + await hass.async_block_till_done() + vera_device.open.assert_called() + vera_device.is_open.return_value = True + vera_device.get_level.return_value = 100 + update_callback(vera_device) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == "open" + assert hass.states.get(entity_id).attributes["current_position"] == 100 + + await hass.services.async_call( + "cover", "set_cover_position", {"entity_id": entity_id, "position": 50}, + ) + await hass.async_block_till_done() + vera_device.set_level.assert_called_with(50) + vera_device.is_open.return_value = True + vera_device.get_level.return_value = 50 + update_callback(vera_device) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == "open" + assert hass.states.get(entity_id).attributes["current_position"] == 50 + + await hass.services.async_call( + "cover", "stop_cover", {"entity_id": entity_id}, + ) + await hass.async_block_till_done() + vera_device.stop.assert_called() + update_callback(vera_device) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == "open" + assert hass.states.get(entity_id).attributes["current_position"] == 50 + + await hass.services.async_call( + "cover", "close_cover", {"entity_id": entity_id}, + ) + await hass.async_block_till_done() + vera_device.close.assert_called() + vera_device.is_open.return_value = False + vera_device.get_level.return_value = 00 + update_callback(vera_device) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == "closed" + assert hass.states.get(entity_id).attributes["current_position"] == 00 diff --git a/tests/components/vera/test_init.py b/tests/components/vera/test_init.py new file mode 100644 index 00000000000..9ff6cb4058b --- /dev/null +++ b/tests/components/vera/test_init.py @@ -0,0 +1,78 @@ +"""Vera tests.""" +from unittest.mock import MagicMock + +from pyvera import ( + VeraArmableDevice, + VeraBinarySensor, + VeraController, + VeraCurtain, + VeraDevice, + VeraDimmer, + VeraLock, + VeraScene, + VeraSceneController, + VeraSensor, + VeraSwitch, + VeraThermostat, +) + +from homeassistant.components.vera import ( + CONF_EXCLUDE, + CONF_LIGHTS, + DOMAIN, + VERA_DEVICES, +) +from homeassistant.core import HomeAssistant + +from .common import ComponentFactory + + +def new_vera_device(cls, device_id: int) -> VeraDevice: + """Create new mocked vera device..""" + vera_device = MagicMock(spec=cls) # type: VeraDevice + vera_device.device_id = device_id + vera_device.name = f"dev${device_id}" + return vera_device + + +def assert_hass_vera_devices(hass: HomeAssistant, platform: str, arr_len: int) -> None: + """Assert vera devices are present..""" + assert hass.data[VERA_DEVICES][platform] + assert len(hass.data[VERA_DEVICES][platform]) == arr_len + + +async def test_init( + hass: HomeAssistant, vera_component_factory: ComponentFactory +) -> None: + """Test function.""" + + def setup_callback(controller: VeraController, hass_config: dict) -> None: + hass_config[DOMAIN][CONF_EXCLUDE] = [11] + hass_config[DOMAIN][CONF_LIGHTS] = [10] + + await vera_component_factory.configure_component( + hass=hass, + devices=( + new_vera_device(VeraDimmer, 1), + new_vera_device(VeraBinarySensor, 2), + new_vera_device(VeraSensor, 3), + new_vera_device(VeraArmableDevice, 4), + new_vera_device(VeraLock, 5), + new_vera_device(VeraThermostat, 6), + new_vera_device(VeraCurtain, 7), + new_vera_device(VeraSceneController, 8), + new_vera_device(VeraSwitch, 9), + new_vera_device(VeraSwitch, 10), + new_vera_device(VeraSwitch, 11), + ), + scenes=(MagicMock(spec=VeraScene),), + setup_callback=setup_callback, + ) + + assert_hass_vera_devices(hass, "light", 2) + assert_hass_vera_devices(hass, "binary_sensor", 1) + assert_hass_vera_devices(hass, "sensor", 2) + assert_hass_vera_devices(hass, "switch", 2) + assert_hass_vera_devices(hass, "lock", 1) + assert_hass_vera_devices(hass, "climate", 1) + assert_hass_vera_devices(hass, "cover", 1) diff --git a/tests/components/vera/test_light.py b/tests/components/vera/test_light.py new file mode 100644 index 00000000000..fa63ce63454 --- /dev/null +++ b/tests/components/vera/test_light.py @@ -0,0 +1,76 @@ +"""Vera tests.""" +from unittest.mock import MagicMock + +from pyvera import CATEGORY_DIMMER, VeraDimmer + +from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_HS_COLOR +from homeassistant.core import HomeAssistant + +from .common import ComponentFactory + + +async def test_light( + hass: HomeAssistant, vera_component_factory: ComponentFactory +) -> None: + """Test function.""" + vera_device = MagicMock(spec=VeraDimmer) # type: VeraDimmer + vera_device.device_id = 1 + vera_device.name = "dev1" + vera_device.category = CATEGORY_DIMMER + vera_device.is_switched_on = MagicMock(return_value=False) + vera_device.get_brightness = MagicMock(return_value=0) + vera_device.get_color = MagicMock(return_value=[0, 0, 0]) + vera_device.is_dimmable = True + entity_id = "light.dev1_1" + + component_data = await vera_component_factory.configure_component( + hass=hass, devices=(vera_device,), + ) + controller = component_data.controller + update_callback = controller.register.call_args_list[0][0][1] + + assert hass.states.get(entity_id).state == "off" + + await hass.services.async_call( + "light", "turn_on", {"entity_id": entity_id}, + ) + await hass.async_block_till_done() + vera_device.switch_on.assert_called() + vera_device.is_switched_on.return_value = True + update_callback(vera_device) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == "on" + + await hass.services.async_call( + "light", "turn_on", {"entity_id": entity_id, ATTR_HS_COLOR: [300, 70]}, + ) + await hass.async_block_till_done() + vera_device.set_color.assert_called_with((255, 76, 255)) + vera_device.is_switched_on.return_value = True + vera_device.get_color.return_value = (255, 76, 255) + update_callback(vera_device) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == "on" + assert hass.states.get(entity_id).attributes["hs_color"] == (300.0, 70.196) + + await hass.services.async_call( + "light", "turn_on", {"entity_id": entity_id, ATTR_BRIGHTNESS: 55}, + ) + await hass.async_block_till_done() + vera_device.set_brightness.assert_called_with(55) + vera_device.is_switched_on.return_value = True + vera_device.get_brightness.return_value = 55 + update_callback(vera_device) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == "on" + assert hass.states.get(entity_id).attributes["brightness"] == 55 + + await hass.services.async_call( + "light", "turn_off", {"entity_id": entity_id}, + ) + await hass.async_block_till_done() + vera_device.switch_off.assert_called() + vera_device.is_switched_on.return_value = False + update_callback(vera_device) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == "off" diff --git a/tests/components/vera/test_lock.py b/tests/components/vera/test_lock.py new file mode 100644 index 00000000000..362bdbeddc0 --- /dev/null +++ b/tests/components/vera/test_lock.py @@ -0,0 +1,49 @@ +"""Vera tests.""" +from unittest.mock import MagicMock + +from pyvera import CATEGORY_LOCK, VeraLock + +from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED +from homeassistant.core import HomeAssistant + +from .common import ComponentFactory + + +async def test_lock( + hass: HomeAssistant, vera_component_factory: ComponentFactory +) -> None: + """Test function.""" + vera_device = MagicMock(spec=VeraLock) # type: VeraLock + vera_device.device_id = 1 + vera_device.name = "dev1" + vera_device.category = CATEGORY_LOCK + vera_device.is_locked = MagicMock(return_value=False) + entity_id = "lock.dev1_1" + + component_data = await vera_component_factory.configure_component( + hass=hass, devices=(vera_device,), + ) + controller = component_data.controller + update_callback = controller.register.call_args_list[0][0][1] + + assert hass.states.get(entity_id).state == STATE_UNLOCKED + + await hass.services.async_call( + "lock", "lock", {"entity_id": entity_id}, + ) + await hass.async_block_till_done() + vera_device.lock.assert_called() + vera_device.is_locked.return_value = True + update_callback(vera_device) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_LOCKED + + await hass.services.async_call( + "lock", "unlock", {"entity_id": entity_id}, + ) + await hass.async_block_till_done() + vera_device.unlock.assert_called() + vera_device.is_locked.return_value = False + update_callback(vera_device) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_UNLOCKED diff --git a/tests/components/vera/test_scene.py b/tests/components/vera/test_scene.py new file mode 100644 index 00000000000..136227ffa71 --- /dev/null +++ b/tests/components/vera/test_scene.py @@ -0,0 +1,27 @@ +"""Vera tests.""" +from unittest.mock import MagicMock + +from pyvera import VeraScene + +from homeassistant.core import HomeAssistant + +from .common import ComponentFactory + + +async def test_scene( + hass: HomeAssistant, vera_component_factory: ComponentFactory +) -> None: + """Test function.""" + vera_scene = MagicMock(spec=VeraScene) # type: VeraScene + vera_scene.scene_id = 1 + vera_scene.name = "dev1" + entity_id = "scene.dev1_1" + + await vera_component_factory.configure_component( + hass=hass, scenes=(vera_scene,), + ) + + await hass.services.async_call( + "scene", "turn_on", {"entity_id": entity_id}, + ) + await hass.async_block_till_done() diff --git a/tests/components/vera/test_sensor.py b/tests/components/vera/test_sensor.py new file mode 100644 index 00000000000..da28fc225e0 --- /dev/null +++ b/tests/components/vera/test_sensor.py @@ -0,0 +1,198 @@ +"""Vera tests.""" +from typing import Any, Callable, Tuple +from unittest.mock import MagicMock + +from pyvera import ( + CATEGORY_HUMIDITY_SENSOR, + CATEGORY_LIGHT_SENSOR, + CATEGORY_POWER_METER, + CATEGORY_SCENE_CONTROLLER, + CATEGORY_TEMPERATURE_SENSOR, + CATEGORY_UV_SENSOR, + VeraController, + VeraSensor, +) + +from homeassistant.core import HomeAssistant + +from .common import ComponentFactory + + +async def run_sensor_test( + hass: HomeAssistant, + vera_component_factory: ComponentFactory, + category: int, + class_property: str, + assert_states: Tuple[Tuple[Any, Any]], + assert_unit_of_measurement: str = None, + setup_callback: Callable[[VeraController], None] = None, +) -> None: + """Test generic sensor.""" + vera_device = MagicMock(spec=VeraSensor) # type: VeraSensor + vera_device.device_id = 1 + vera_device.name = "dev1" + vera_device.category = category + setattr(vera_device, class_property, "33") + entity_id = "sensor.dev1_1" + + component_data = await vera_component_factory.configure_component( + hass=hass, devices=(vera_device,), setup_callback=setup_callback + ) + controller = component_data.controller + update_callback = controller.register.call_args_list[0][0][1] + + for (initial_value, state_value) in assert_states: + setattr(vera_device, class_property, initial_value) + update_callback(vera_device) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == state_value + if assert_unit_of_measurement: + assert state.attributes["unit_of_measurement"] == assert_unit_of_measurement + + +async def test_temperature_sensor_f( + hass: HomeAssistant, vera_component_factory: ComponentFactory +) -> None: + """Test function.""" + + def setup_callback(controller: VeraController, hass_config: dict) -> None: + controller.temperature_units = "F" + + await run_sensor_test( + hass=hass, + vera_component_factory=vera_component_factory, + category=CATEGORY_TEMPERATURE_SENSOR, + class_property="temperature", + assert_states=(("33", "1"), ("44", "7"),), + setup_callback=setup_callback, + ) + + +async def test_temperature_sensor_c( + hass: HomeAssistant, vera_component_factory: ComponentFactory +) -> None: + """Test function.""" + await run_sensor_test( + hass=hass, + vera_component_factory=vera_component_factory, + category=CATEGORY_TEMPERATURE_SENSOR, + class_property="temperature", + assert_states=(("33", "33"), ("44", "44"),), + ) + + +async def test_light_sensor( + hass: HomeAssistant, vera_component_factory: ComponentFactory +) -> None: + """Test function.""" + await run_sensor_test( + hass=hass, + vera_component_factory=vera_component_factory, + category=CATEGORY_LIGHT_SENSOR, + class_property="light", + assert_states=(("12", "12"), ("13", "13"),), + assert_unit_of_measurement="lx", + ) + + +async def test_uv_sensor( + hass: HomeAssistant, vera_component_factory: ComponentFactory +) -> None: + """Test function.""" + await run_sensor_test( + hass=hass, + vera_component_factory=vera_component_factory, + category=CATEGORY_UV_SENSOR, + class_property="light", + assert_states=(("12", "12"), ("13", "13"),), + assert_unit_of_measurement="level", + ) + + +async def test_humidity_sensor( + hass: HomeAssistant, vera_component_factory: ComponentFactory +) -> None: + """Test function.""" + await run_sensor_test( + hass=hass, + vera_component_factory=vera_component_factory, + category=CATEGORY_HUMIDITY_SENSOR, + class_property="humidity", + assert_states=(("12", "12"), ("13", "13"),), + assert_unit_of_measurement="%", + ) + + +async def test_power_meter_sensor( + hass: HomeAssistant, vera_component_factory: ComponentFactory +) -> None: + """Test function.""" + await run_sensor_test( + hass=hass, + vera_component_factory=vera_component_factory, + category=CATEGORY_POWER_METER, + class_property="power", + assert_states=(("12", "12"), ("13", "13"),), + assert_unit_of_measurement="watts", + ) + + +async def test_trippable_sensor( + hass: HomeAssistant, vera_component_factory: ComponentFactory +) -> None: + """Test function.""" + + def setup_callback(controller: VeraController, hass_config: dict) -> None: + controller.get_devices()[0].is_trippable = True + + await run_sensor_test( + hass=hass, + vera_component_factory=vera_component_factory, + category=999, + class_property="is_tripped", + assert_states=((True, "Tripped"), (False, "Not Tripped"), (True, "Tripped"),), + setup_callback=setup_callback, + ) + + +async def test_unknown_sensor( + hass: HomeAssistant, vera_component_factory: ComponentFactory +) -> None: + """Test function.""" + + def setup_callback(controller: VeraController, hass_config: dict) -> None: + controller.get_devices()[0].is_trippable = False + + await run_sensor_test( + hass=hass, + vera_component_factory=vera_component_factory, + category=999, + class_property="is_tripped", + assert_states=((True, "Unknown"), (False, "Unknown"), (True, "Unknown"),), + setup_callback=setup_callback, + ) + + +async def test_scene_controller_sensor( + hass: HomeAssistant, vera_component_factory: ComponentFactory +) -> None: + """Test function.""" + vera_device = MagicMock(spec=VeraSensor) # type: VeraSensor + vera_device.device_id = 1 + vera_device.name = "dev1" + vera_device.category = CATEGORY_SCENE_CONTROLLER + vera_device.get_last_scene_id = MagicMock(return_value="id0") + vera_device.get_last_scene_time = MagicMock(return_value="0000") + entity_id = "sensor.dev1_1" + + component_data = await vera_component_factory.configure_component( + hass=hass, devices=(vera_device,), + ) + controller = component_data.controller + update_callback = controller.register.call_args_list[0][0][1] + + vera_device.get_last_scene_time = "1111" + update_callback(vera_device) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == "id0" diff --git a/tests/components/vera/test_switch.py b/tests/components/vera/test_switch.py new file mode 100644 index 00000000000..ba09068e7e6 --- /dev/null +++ b/tests/components/vera/test_switch.py @@ -0,0 +1,48 @@ +"""Vera tests.""" +from unittest.mock import MagicMock + +from pyvera import CATEGORY_SWITCH, VeraSwitch + +from homeassistant.core import HomeAssistant + +from .common import ComponentFactory + + +async def test_switch( + hass: HomeAssistant, vera_component_factory: ComponentFactory +) -> None: + """Test function.""" + vera_device = MagicMock(spec=VeraSwitch) # type: VeraSwitch + vera_device.device_id = 1 + vera_device.name = "dev1" + vera_device.category = CATEGORY_SWITCH + vera_device.is_switched_on = MagicMock(return_value=False) + entity_id = "switch.dev1_1" + + component_data = await vera_component_factory.configure_component( + hass=hass, devices=(vera_device,), + ) + controller = component_data.controller + update_callback = controller.register.call_args_list[0][0][1] + + assert hass.states.get(entity_id).state == "off" + + await hass.services.async_call( + "switch", "turn_on", {"entity_id": entity_id}, + ) + await hass.async_block_till_done() + vera_device.switch_on.assert_called() + vera_device.is_switched_on.return_value = True + update_callback(vera_device) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == "on" + + await hass.services.async_call( + "switch", "turn_off", {"entity_id": entity_id}, + ) + await hass.async_block_till_done() + vera_device.switch_off.assert_called() + vera_device.is_switched_on.return_value = False + update_callback(vera_device) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == "off" diff --git a/tests/components/verisure/test_ethernet_status.py b/tests/components/verisure/test_ethernet_status.py index 71c7df94ae5..611adde19d9 100644 --- a/tests/components/verisure/test_ethernet_status.py +++ b/tests/components/verisure/test_ethernet_status.py @@ -2,9 +2,9 @@ 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 -from homeassistant.components.verisure import DOMAIN as VERISURE_DOMAIN CONFIG = { "verisure": { diff --git a/tests/components/verisure/test_lock.py b/tests/components/verisure/test_lock.py index ac03e0d9fb6..2f69c183d7d 100644 --- a/tests/components/verisure/test_lock.py +++ b/tests/components/verisure/test_lock.py @@ -1,16 +1,16 @@ """Tests for the Verisure platform.""" from contextlib import contextmanager -from unittest.mock import patch, call -from homeassistant.const import STATE_UNLOCKED -from homeassistant.setup import async_setup_component +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": { diff --git a/tests/components/vesync/test_config_flow.py b/tests/components/vesync/test_config_flow.py index 205ce80b4b1..39b847effc5 100644 --- a/tests/components/vesync/test_config_flow.py +++ b/tests/components/vesync/test_config_flow.py @@ -1,8 +1,10 @@ """Test for vesync config flow.""" from unittest.mock import patch + from homeassistant import data_entry_flow -from homeassistant.components.vesync import config_flow, DOMAIN -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD +from homeassistant.components.vesync import DOMAIN, config_flow +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME + from tests.common import MockConfigEntry diff --git a/tests/components/voicerss/test_tts.py b/tests/components/voicerss/test_tts.py index 0f2a0618096..d2a7197fe1a 100644 --- a/tests/components/voicerss/test_tts.py +++ b/tests/components/voicerss/test_tts.py @@ -3,16 +3,15 @@ import asyncio import os import shutil -import homeassistant.components.tts as tts from homeassistant.components.media_player.const import ( - SERVICE_PLAY_MEDIA, ATTR_MEDIA_CONTENT_ID, DOMAIN as DOMAIN_MP, + SERVICE_PLAY_MEDIA, ) +import homeassistant.components.tts as tts from homeassistant.setup import setup_component -from tests.common import get_test_home_assistant, assert_setup_component, mock_service - +from tests.common import assert_setup_component, get_test_home_assistant, mock_service from tests.components.tts.test_init import mutagen_mock # noqa: F401 diff --git a/tests/components/vultr/test_binary_sensor.py b/tests/components/vultr/test_binary_sensor.py index e59c7b4e46c..f57926f30c8 100644 --- a/tests/components/vultr/test_binary_sensor.py +++ b/tests/components/vultr/test_binary_sensor.py @@ -3,25 +3,25 @@ import json import unittest from unittest.mock import patch -import requests_mock import pytest +import requests_mock import voluptuous as vol -from homeassistant.components.vultr import binary_sensor as vultr from homeassistant.components import vultr as base_vultr from homeassistant.components.vultr import ( ATTR_ALLOWED_BANDWIDTH, ATTR_AUTO_BACKUPS, - ATTR_IPV4_ADDRESS, ATTR_COST_PER_MONTH, ATTR_CREATED_AT, + ATTR_IPV4_ADDRESS, ATTR_SUBSCRIPTION_ID, CONF_SUBSCRIPTION, + binary_sensor as vultr, ) -from homeassistant.const import CONF_PLATFORM, CONF_NAME +from homeassistant.const import CONF_NAME, CONF_PLATFORM -from tests.components.vultr.test_init import VALID_CONFIG from tests.common import get_test_home_assistant, load_fixture +from tests.components.vultr.test_init import VALID_CONFIG class TestVultrBinarySensorSetup(unittest.TestCase): diff --git a/tests/components/vultr/test_sensor.py b/tests/components/vultr/test_sensor.py index 6e2969dd2e0..4da60783c44 100644 --- a/tests/components/vultr/test_sensor.py +++ b/tests/components/vultr/test_sensor.py @@ -7,13 +7,13 @@ import pytest import requests_mock import voluptuous as vol -import homeassistant.components.vultr.sensor as vultr from homeassistant.components import vultr as base_vultr from homeassistant.components.vultr import CONF_SUBSCRIPTION -from homeassistant.const import CONF_NAME, CONF_MONITORED_CONDITIONS, CONF_PLATFORM +import homeassistant.components.vultr.sensor as vultr +from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_PLATFORM -from tests.components.vultr.test_init import VALID_CONFIG from tests.common import get_test_home_assistant, load_fixture +from tests.components.vultr.test_init import VALID_CONFIG class TestVultrSensorSetup(unittest.TestCase): diff --git a/tests/components/vultr/test_switch.py b/tests/components/vultr/test_switch.py index 0d5055cd6a5..4f61291c4ad 100644 --- a/tests/components/vultr/test_switch.py +++ b/tests/components/vultr/test_switch.py @@ -3,25 +3,25 @@ import json import unittest from unittest.mock import patch -import requests_mock import pytest +import requests_mock import voluptuous as vol -from homeassistant.components.vultr import switch as vultr from homeassistant.components import vultr as base_vultr from homeassistant.components.vultr import ( ATTR_ALLOWED_BANDWIDTH, ATTR_AUTO_BACKUPS, - ATTR_IPV4_ADDRESS, ATTR_COST_PER_MONTH, ATTR_CREATED_AT, + ATTR_IPV4_ADDRESS, ATTR_SUBSCRIPTION_ID, CONF_SUBSCRIPTION, + switch as vultr, ) -from homeassistant.const import CONF_PLATFORM, CONF_NAME +from homeassistant.const import CONF_NAME, CONF_PLATFORM -from tests.components.vultr.test_init import VALID_CONFIG from tests.common import get_test_home_assistant, load_fixture +from tests.components.vultr.test_init import VALID_CONFIG class TestVultrSwitchSetup(unittest.TestCase): diff --git a/tests/components/wake_on_lan/test_switch.py b/tests/components/wake_on_lan/test_switch.py index 2d319c2b5e7..e0f12f9c7f8 100644 --- a/tests/components/wake_on_lan/test_switch.py +++ b/tests/components/wake_on_lan/test_switch.py @@ -2,14 +2,13 @@ import unittest from unittest.mock import patch -from homeassistant.setup import setup_component -from homeassistant.const import STATE_ON, STATE_OFF import homeassistant.components.switch as switch +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.setup import setup_component from tests.common import get_test_home_assistant, mock_service from tests.components.switch import common - TEST_STATE = None @@ -30,7 +29,7 @@ def system(): return "Windows" -class TestWOLSwitch(unittest.TestCase): +class TestWolSwitch(unittest.TestCase): """Test the wol switch.""" def setUp(self): @@ -53,7 +52,7 @@ class TestWOLSwitch(unittest.TestCase): { "switch": { "platform": "wake_on_lan", - "mac_address": "00-01-02-03-04-05", + "mac": "00-01-02-03-04-05", "host": "validhostname", } }, @@ -89,7 +88,7 @@ class TestWOLSwitch(unittest.TestCase): { "switch": { "platform": "wake_on_lan", - "mac_address": "00-01-02-03-04-05", + "mac": "00-01-02-03-04-05", "host": "validhostname", } }, @@ -113,7 +112,7 @@ class TestWOLSwitch(unittest.TestCase): assert setup_component( self.hass, switch.DOMAIN, - {"switch": {"platform": "wake_on_lan", "mac_address": "00-01-02-03-04-05"}}, + {"switch": {"platform": "wake_on_lan", "mac": "00-01-02-03-04-05"}}, ) @patch("wakeonlan.send_magic_packet", new=send_magic_packet) @@ -126,7 +125,7 @@ class TestWOLSwitch(unittest.TestCase): { "switch": { "platform": "wake_on_lan", - "mac_address": "00-01-02-03-04-05", + "mac": "00-01-02-03-04-05", "broadcast_address": "255.255.255.255", } }, @@ -150,7 +149,7 @@ class TestWOLSwitch(unittest.TestCase): { "switch": { "platform": "wake_on_lan", - "mac_address": "00-01-02-03-04-05", + "mac": "00-01-02-03-04-05", "host": "validhostname", "turn_off": {"service": "shell_command.turn_off_target"}, } @@ -192,7 +191,7 @@ class TestWOLSwitch(unittest.TestCase): { "switch": { "platform": "wake_on_lan", - "mac_address": "00-01-02-03-04-05", + "mac": "00-01-02-03-04-05", "host": "invalidhostname", } }, diff --git a/tests/components/water_heater/common.py b/tests/components/water_heater/common.py index 3fb010ab55c..04fd345577e 100644 --- a/tests/components/water_heater/common.py +++ b/tests/components/water_heater/common.py @@ -9,15 +9,15 @@ from homeassistant.components.water_heater import ( ATTR_OPERATION_MODE, DOMAIN, SERVICE_SET_AWAY_MODE, - SERVICE_SET_TEMPERATURE, SERVICE_SET_OPERATION_MODE, + SERVICE_SET_TEMPERATURE, ) -from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE +from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, ENTITY_MATCH_ALL from homeassistant.loader import bind_hass @bind_hass -def set_away_mode(hass, away_mode, entity_id=None): +def set_away_mode(hass, away_mode, entity_id=ENTITY_MATCH_ALL): """Turn all or specified water_heater devices away mode on.""" data = {ATTR_AWAY_MODE: away_mode} @@ -28,7 +28,9 @@ def set_away_mode(hass, away_mode, entity_id=None): @bind_hass -def set_temperature(hass, temperature=None, entity_id=None, operation_mode=None): +def set_temperature( + hass, temperature=None, entity_id=ENTITY_MATCH_ALL, operation_mode=None +): """Set new target temperature.""" kwargs = { key: value @@ -44,7 +46,7 @@ def set_temperature(hass, temperature=None, entity_id=None, operation_mode=None) @bind_hass -def set_operation_mode(hass, operation_mode, entity_id=None): +def set_operation_mode(hass, operation_mode, entity_id=ENTITY_MATCH_ALL): """Set new target operation mode.""" data = {ATTR_OPERATION_MODE: operation_mode} diff --git a/tests/components/weather/test_weather.py b/tests/components/weather/test_weather.py index bbfd07fb551..fd960b594a0 100644 --- a/tests/components/weather/test_weather.py +++ b/tests/components/weather/test_weather.py @@ -3,6 +3,11 @@ import unittest from homeassistant.components import weather from homeassistant.components.weather import ( + ATTR_FORECAST, + ATTR_FORECAST_CONDITION, + ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_TEMP, + ATTR_FORECAST_TEMP_LOW, ATTR_WEATHER_ATTRIBUTION, ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_OZONE, @@ -10,14 +15,9 @@ from homeassistant.components.weather import ( ATTR_WEATHER_TEMPERATURE, ATTR_WEATHER_WIND_BEARING, ATTR_WEATHER_WIND_SPEED, - ATTR_FORECAST, - ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, ) -from homeassistant.util.unit_system import METRIC_SYSTEM from homeassistant.setup import setup_component +from homeassistant.util.unit_system import METRIC_SYSTEM from tests.common import get_test_home_assistant diff --git a/tests/components/weblink/test_init.py b/tests/components/weblink/test_init.py index ae519b95ada..5f803107c46 100644 --- a/tests/components/weblink/test_init.py +++ b/tests/components/weblink/test_init.py @@ -1,8 +1,8 @@ """The tests for the weblink component.""" import unittest -from homeassistant.setup import setup_component from homeassistant.components import weblink +from homeassistant.setup import setup_component from tests.common import get_test_home_assistant diff --git a/tests/components/webostv/test_media_player.py b/tests/components/webostv/test_media_player.py index e5729a2d8d0..023e0e2dc07 100644 --- a/tests/components/webostv/test_media_player.py +++ b/tests/components/webostv/test_media_player.py @@ -1,56 +1,77 @@ """The tests for the LG webOS media player platform.""" -import unittest -from unittest import mock +import sys -from homeassistant.components.webostv import media_player as webostv +import pytest + +from homeassistant.components import media_player +from homeassistant.components.media_player.const import ( + ATTR_INPUT_SOURCE, + ATTR_MEDIA_VOLUME_MUTED, + SERVICE_SELECT_SOURCE, +) +from homeassistant.components.webostv import DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_HOST, + CONF_NAME, + SERVICE_VOLUME_MUTE, +) +from homeassistant.setup import async_setup_component + +if sys.version_info >= (3, 8, 0): + from unittest.mock import patch +else: + from asynctest import patch -class FakeLgWebOSDevice(webostv.LgWebOSDevice): - """A fake device without the client setup required for the real one.""" - - def __init__(self, *args, **kwargs): - """Initialise parameters needed for tests with fake values.""" - self._source_list = {} - self._client = mock.MagicMock() - self._name = "fake_device" - self._current_source = None +NAME = "fake" +ENTITY_ID = f"{media_player.DOMAIN}.{NAME}" -class TestLgWebOSDevice(unittest.TestCase): - """Test the LgWebOSDevice class.""" +@pytest.fixture(name="client") +def client_fixture(): + """Patch of client library for tests.""" + with patch( + "homeassistant.components.webostv.WebOsClient", autospec=True + ) as mock_client_class: + yield mock_client_class.return_value - def setUp(self): - """Configure a fake device for each test.""" - self.device = FakeLgWebOSDevice() - def test_select_source_with_empty_source_list(self): - """Ensure we don't call client methods when we don't have sources.""" - self.device.select_source("nonexistent") - assert 0 == self.device._client.launch_app.call_count - assert 0 == self.device._client.set_input.call_count +async def setup_webostv(hass): + """Initialize webostv and media_player for tests.""" + assert await async_setup_component( + hass, DOMAIN, {DOMAIN: {CONF_HOST: "fake", CONF_NAME: NAME}}, + ) + await hass.async_block_till_done() - def test_select_source_with_titled_entry(self): - """Test that a titled source is treated as an app.""" - self.device._source_list = { - "existent": {"id": "existent_id", "title": "existent_title"} - } - self.device.select_source("existent") +async def test_mute(hass, client): + """Test simple service call.""" - assert "existent_title" == self.device._current_source - assert [mock.call("existent_id")] == ( - self.device._client.launch_app.call_args_list - ) + await setup_webostv(hass) - def test_select_source_with_labelled_entry(self): - """Test that a labelled source is treated as an input source.""" - self.device._source_list = { - "existent": {"id": "existent_id", "label": "existent_label"} - } + data = { + ATTR_ENTITY_ID: ENTITY_ID, + ATTR_MEDIA_VOLUME_MUTED: True, + } + await hass.services.async_call(media_player.DOMAIN, SERVICE_VOLUME_MUTE, data) + await hass.async_block_till_done() - self.device.select_source("existent") + client.set_mute.assert_called_once() - assert "existent_label" == self.device._current_source - assert [mock.call("existent_id")] == ( - self.device._client.set_input.call_args_list - ) + +async def test_select_source_with_empty_source_list(hass, client): + """Ensure we don't call client methods when we don't have sources.""" + + await setup_webostv(hass) + + data = { + ATTR_ENTITY_ID: ENTITY_ID, + ATTR_INPUT_SOURCE: "nonexistent", + } + await hass.services.async_call(media_player.DOMAIN, SERVICE_SELECT_SOURCE, data) + await hass.async_block_till_done() + + assert hass.states.is_state(ENTITY_ID, "playing") + client.launch_app.assert_not_called() + client.set_input.assert_not_called() diff --git a/tests/components/websocket_api/conftest.py b/tests/components/websocket_api/conftest.py index 382de3142e8..65b9232821f 100644 --- a/tests/components/websocket_api/conftest.py +++ b/tests/components/websocket_api/conftest.py @@ -1,9 +1,9 @@ """Fixtures for websocket tests.""" import pytest -from homeassistant.setup import async_setup_component -from homeassistant.components.websocket_api.http import URL from homeassistant.components.websocket_api.auth import TYPE_AUTH_REQUIRED +from homeassistant.components.websocket_api.http import URL +from homeassistant.setup import async_setup_component @pytest.fixture diff --git a/tests/components/websocket_api/test_auth.py b/tests/components/websocket_api/test_auth.py index 00387506020..2a0bc9f8c5a 100644 --- a/tests/components/websocket_api/test_auth.py +++ b/tests/components/websocket_api/test_auth.py @@ -1,18 +1,17 @@ """Test auth of websocket API.""" from unittest.mock import patch -from homeassistant.components.websocket_api.const import ( - URL, - SIGNAL_WEBSOCKET_CONNECTED, - SIGNAL_WEBSOCKET_DISCONNECTED, -) from homeassistant.components.websocket_api.auth import ( TYPE_AUTH, TYPE_AUTH_INVALID, TYPE_AUTH_OK, TYPE_AUTH_REQUIRED, ) - +from homeassistant.components.websocket_api.const import ( + SIGNAL_WEBSOCKET_CONNECTED, + SIGNAL_WEBSOCKET_DISCONNECTED, + URL, +) from homeassistant.setup import async_setup_component from tests.common import mock_coro @@ -45,7 +44,7 @@ async def test_auth_events( async def test_auth_via_msg_incorrect_pass(no_auth_websocket_client): """Test authenticating.""" with patch( - "homeassistant.components.websocket_api.auth." "process_wrong_login", + "homeassistant.components.websocket_api.auth.process_wrong_login", return_value=mock_coro(), ) as mock_process_wrong_login: await no_auth_websocket_client.send_json( diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 1de5b8bb2c1..58d904c8f4b 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -1,14 +1,14 @@ """Tests for WebSocket API commands.""" from async_timeout import timeout -from homeassistant.core import callback -from homeassistant.components.websocket_api.const import URL +from homeassistant.components.websocket_api import const from homeassistant.components.websocket_api.auth import ( TYPE_AUTH, TYPE_AUTH_OK, TYPE_AUTH_REQUIRED, ) -from homeassistant.components.websocket_api import const +from homeassistant.components.websocket_api.const import URL +from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component diff --git a/tests/components/websocket_api/test_init.py b/tests/components/websocket_api/test_init.py index ed3319ed4f4..d32f55516aa 100644 --- a/tests/components/websocket_api/test_init.py +++ b/tests/components/websocket_api/test_init.py @@ -1,6 +1,5 @@ """Tests for the Home Assistant Websocket API.""" -import asyncio -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch from aiohttp import WSMsgType import pytest @@ -16,12 +15,11 @@ def mock_low_queue(): yield -@asyncio.coroutine -def test_invalid_message_format(websocket_client): +async def test_invalid_message_format(websocket_client): """Test sending invalid JSON.""" - yield from websocket_client.send_json({"type": 5}) + await websocket_client.send_json({"type": 5}) - msg = yield from websocket_client.receive_json() + msg = await websocket_client.receive_json() assert msg["type"] == const.TYPE_RESULT error = msg["error"] @@ -29,42 +27,38 @@ def test_invalid_message_format(websocket_client): assert error["message"].startswith("Message incorrectly formatted") -@asyncio.coroutine -def test_invalid_json(websocket_client): +async def test_invalid_json(websocket_client): """Test sending invalid JSON.""" - yield from websocket_client.send_str("this is not JSON") + await websocket_client.send_str("this is not JSON") - msg = yield from websocket_client.receive() + msg = await websocket_client.receive() assert msg.type == WSMsgType.close -@asyncio.coroutine -def test_quiting_hass(hass, websocket_client): +async def test_quiting_hass(hass, websocket_client): """Test sending invalid JSON.""" with patch.object(hass.loop, "stop"): - yield from hass.async_stop() + await hass.async_stop() - msg = yield from websocket_client.receive() + msg = await websocket_client.receive() assert msg.type == WSMsgType.CLOSE -@asyncio.coroutine -def test_pending_msg_overflow(hass, mock_low_queue, websocket_client): +async def test_pending_msg_overflow(hass, mock_low_queue, websocket_client): """Test get_panels command.""" for idx in range(10): - yield from websocket_client.send_json({"id": idx + 1, "type": "ping"}) - msg = yield from websocket_client.receive() + await websocket_client.send_json({"id": idx + 1, "type": "ping"}) + msg = await websocket_client.receive() assert msg.type == WSMsgType.close -@asyncio.coroutine -def test_unknown_command(websocket_client): +async def test_unknown_command(websocket_client): """Test get_panels command.""" - yield from websocket_client.send_json({"id": 5, "type": "unknown_command"}) + await websocket_client.send_json({"id": 5, "type": "unknown_command"}) - msg = yield from websocket_client.receive_json() + msg = await websocket_client.receive_json() assert not msg["success"] assert msg["error"]["code"] == const.ERR_UNKNOWN_COMMAND diff --git a/tests/components/websocket_api/test_sensor.py b/tests/components/websocket_api/test_sensor.py index 84b73060698..2c711737851 100644 --- a/tests/components/websocket_api/test_sensor.py +++ b/tests/components/websocket_api/test_sensor.py @@ -2,9 +2,10 @@ from homeassistant.bootstrap import async_setup_component -from tests.common import assert_setup_component from .test_auth import test_auth_active_with_token +from tests.common import assert_setup_component + async def test_websocket_api( hass, no_auth_websocket_client, hass_access_token, legacy_auth diff --git a/tests/components/withings/test_common.py b/tests/components/withings/test_common.py index f6075c0f734..acb69dddf4e 100644 --- a/tests/components/withings/test_common.py +++ b/tests/components/withings/test_common.py @@ -1,26 +1,43 @@ """Tests for the Withings component.""" -from asynctest import MagicMock +from datetime import timedelta +from unittest.mock import patch +from asynctest import MagicMock import pytest from withings_api import WithingsApi -from withings_api.common import UnauthorizedException, TimeoutException +from withings_api.common import TimeoutException, UnauthorizedException -from homeassistant.exceptions import PlatformNotReady from homeassistant.components.withings.common import ( NotAuthenticatedError, WithingsDataManager, ) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import PlatformNotReady +from homeassistant.util import dt @pytest.fixture(name="withings_api") def withings_api_fixture() -> WithingsApi: """Provide withings api.""" withings_api = WithingsApi.__new__(WithingsApi) - withings_api.get_measures = MagicMock() - withings_api.get_sleep = MagicMock() + withings_api.user_get_device = MagicMock() + withings_api.measure_get_meas = MagicMock() + withings_api.sleep_get = MagicMock() + withings_api.sleep_get_summary = MagicMock() return withings_api +@pytest.fixture +def mock_time_zone(): + """Provide an alternative time zone.""" + patch_time_zone = patch( + "homeassistant.util.dt.DEFAULT_TIME_ZONE", + new=dt.get_time_zone("America/Los_Angeles"), + ) + with patch_time_zone: + yield + + @pytest.fixture(name="data_manager") def data_manager_fixture(hass, withings_api: WithingsApi) -> WithingsDataManager: """Provide data manager.""" @@ -102,3 +119,23 @@ async def test_data_manager_call_throttle_disabled( assert result == "HELLO2" assert hello_func.call_count == 2 + + +async def test_data_manager_update_sleep_date_range( + hass: HomeAssistant, data_manager: WithingsDataManager, mock_time_zone +) -> None: + """Test method.""" + update_start_time = dt.now() + await data_manager.update_sleep() + + call_args = data_manager.api.sleep_get.call_args_list[0][1] + startdate = call_args.get("startdate") + enddate = call_args.get("enddate") + + assert startdate.tzname() == "PST" + + assert enddate.tzname() == "PST" + assert startdate.tzname() == "PST" + assert update_start_time < enddate + assert enddate < update_start_time + timedelta(seconds=1) + assert enddate > startdate diff --git a/tests/components/withings/test_init.py b/tests/components/withings/test_init.py index bd4940d9504..286b28b61ff 100644 --- a/tests/components/withings/test_init.py +++ b/tests/components/withings/test_init.py @@ -10,26 +10,26 @@ from withings_api.common import SleepModel, SleepState import homeassistant.components.http as http from homeassistant.components.withings import ( + CONFIG_SCHEMA, async_setup, async_setup_entry, const, - CONFIG_SCHEMA, ) from homeassistant.const import STATE_UNKNOWN from homeassistant.core import HomeAssistant from .common import ( - assert_state_equals, - configure_integration, - setup_hass, WITHINGS_GET_DEVICE_RESPONSE, WITHINGS_GET_DEVICE_RESPONSE_EMPTY, + WITHINGS_MEASURES_RESPONSE, + WITHINGS_MEASURES_RESPONSE_EMPTY, WITHINGS_SLEEP_RESPONSE, WITHINGS_SLEEP_RESPONSE_EMPTY, WITHINGS_SLEEP_SUMMARY_RESPONSE, WITHINGS_SLEEP_SUMMARY_RESPONSE_EMPTY, - WITHINGS_MEASURES_RESPONSE, - WITHINGS_MEASURES_RESPONSE_EMPTY, + assert_state_equals, + configure_integration, + setup_hass, ) @@ -308,8 +308,13 @@ async def test_full_setup(hass: HomeAssistant, aiohttp_client, aioclient_mock) - { "startdate": "2019-02-01 00:00:00", "enddate": "2019-02-01 01:00:00", + "state": SleepState.REM.real, + }, + { + "startdate": "2019-02-01 01:00:00", + "enddate": "2019-02-01 02:00:00", "state": SleepState.AWAKE.real, - } + }, ], }, }, @@ -329,11 +334,16 @@ async def test_full_setup(hass: HomeAssistant, aiohttp_client, aioclient_mock) - "body": { "model": SleepModel.TRACKER.real, "series": [ + { + "startdate": "2019-02-01 01:00:00", + "enddate": "2019-02-01 02:00:00", + "state": SleepState.LIGHT.real, + }, { "startdate": "2019-02-01 00:00:00", "enddate": "2019-02-01 01:00:00", - "state": SleepState.LIGHT.real, - } + "state": SleepState.REM.real, + }, ], }, }, @@ -356,8 +366,18 @@ async def test_full_setup(hass: HomeAssistant, aiohttp_client, aioclient_mock) - { "startdate": "2019-02-01 00:00:00", "enddate": "2019-02-01 01:00:00", + "state": SleepState.LIGHT.real, + }, + { + "startdate": "2019-02-01 02:00:00", + "enddate": "2019-02-01 03:00:00", "state": SleepState.REM.real, - } + }, + { + "startdate": "2019-02-01 01:00:00", + "enddate": "2019-02-01 02:00:00", + "state": SleepState.AWAKE.real, + }, ], }, }, diff --git a/tests/components/wled/test_light.py b/tests/components/wled/test_light.py index 185a25b0507..037081608af 100644 --- a/tests/components/wled/test_light.py +++ b/tests/components/wled/test_light.py @@ -8,6 +8,7 @@ from homeassistant.components.light import ( ATTR_HS_COLOR, ATTR_RGB_COLOR, ATTR_TRANSITION, + ATTR_WHITE_VALUE, DOMAIN as LIGHT_DOMAIN, ) from homeassistant.components.wled.const import ( @@ -164,7 +165,8 @@ async def test_rgbw_light( state = hass.states.get("light.wled_rgbw_light") assert state.state == STATE_ON - assert state.attributes.get(ATTR_HS_COLOR) == (0.0, 64.706) + assert state.attributes.get(ATTR_HS_COLOR) == (0.0, 100.0) + assert state.attributes.get(ATTR_WHITE_VALUE) == 139 await hass.services.async_call( LIGHT_DOMAIN, @@ -177,3 +179,17 @@ async def test_rgbw_light( state = hass.states.get("light.wled_rgbw_light") assert state.state == STATE_ON assert state.attributes.get(ATTR_HS_COLOR) == (28.874, 72.522) + assert state.attributes.get(ATTR_WHITE_VALUE) == 139 + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.wled_rgbw_light", ATTR_WHITE_VALUE: 100}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("light.wled_rgbw_light") + assert state.state == STATE_ON + assert state.attributes.get(ATTR_HS_COLOR) == (28.874, 72.522) + assert state.attributes.get(ATTR_WHITE_VALUE) == 100 diff --git a/tests/components/workday/test_binary_sensor.py b/tests/components/workday/test_binary_sensor.py index eeb0707ea50..81ae18bfd3b 100644 --- a/tests/components/workday/test_binary_sensor.py +++ b/tests/components/workday/test_binary_sensor.py @@ -2,11 +2,13 @@ from datetime import date from unittest.mock import patch -from homeassistant.components.workday.binary_sensor import day_to_string +import pytest +import voluptuous as vol + +import homeassistant.components.workday.binary_sensor as binary_sensor from homeassistant.setup import setup_component -from tests.common import get_test_home_assistant, assert_setup_component - +from tests.common import assert_setup_component, get_test_home_assistant FUNCTION_PATH = "homeassistant.components.workday.binary_sensor.get_date" @@ -69,6 +71,20 @@ class TestWorkdaySetup: """Stop everything that was started.""" self.hass.stop() + def test_valid_country(self): + """Test topic name/filter validation.""" + # Invalid UTF-8, must not contain U+D800 to U+DFFF + with pytest.raises(vol.Invalid): + binary_sensor.valid_country("\ud800") + with pytest.raises(vol.Invalid): + binary_sensor.valid_country("\udfff") + # Country MUST NOT be empty + with pytest.raises(vol.Invalid): + binary_sensor.valid_country("") + # Country must be supported by holidays + with pytest.raises(vol.Invalid): + binary_sensor.valid_country("HomeAssistantLand") + def test_setup_component_province(self): """Set up workday component.""" with assert_setup_component(1, "binary_sensor"): @@ -215,7 +231,7 @@ class TestWorkdaySetup: def test_day_to_string(self): """Test if day_to_string is behaving correctly.""" - assert day_to_string(0) == "mon" - assert day_to_string(1) == "tue" - assert day_to_string(7) == "holiday" - assert day_to_string(8) is None + assert binary_sensor.day_to_string(0) == "mon" + assert binary_sensor.day_to_string(1) == "tue" + assert binary_sensor.day_to_string(7) == "holiday" + assert binary_sensor.day_to_string(8) is None diff --git a/tests/components/worldclock/test_sensor.py b/tests/components/worldclock/test_sensor.py index 27cf574f7f8..b0e8119035d 100644 --- a/tests/components/worldclock/test_sensor.py +++ b/tests/components/worldclock/test_sensor.py @@ -2,9 +2,10 @@ import unittest from homeassistant.setup import setup_component -from tests.common import get_test_home_assistant import homeassistant.util.dt as dt_util +from tests.common import get_test_home_assistant + class TestWorldClockSensor(unittest.TestCase): """Test the World clock sensor.""" diff --git a/tests/components/wsdot/test_sensor.py b/tests/components/wsdot/test_sensor.py index 96e2c66a163..b548c099f40 100644 --- a/tests/components/wsdot/test_sensor.py +++ b/tests/components/wsdot/test_sensor.py @@ -4,7 +4,6 @@ import re import unittest import requests_mock -from tests.common import get_test_home_assistant, load_fixture import homeassistant.components.wsdot.sensor as wsdot from homeassistant.components.wsdot.sensor import ( @@ -19,6 +18,8 @@ from homeassistant.components.wsdot.sensor import ( ) from homeassistant.setup import setup_component +from tests.common import get_test_home_assistant, load_fixture + class TestWSDOT(unittest.TestCase): """Test the WSDOT platform.""" diff --git a/tests/components/wunderground/test_sensor.py b/tests/components/wunderground/test_sensor.py index 54072e04ab5..5f74a837cf3 100644 --- a/tests/components/wunderground/test_sensor.py +++ b/tests/components/wunderground/test_sensor.py @@ -1,14 +1,13 @@ """The tests for the WUnderground platform.""" -import asyncio import aiohttp - from pytest import raises import homeassistant.components.wunderground.sensor as wunderground -from homeassistant.const import TEMP_CELSIUS, LENGTH_INCHES, STATE_UNKNOWN +from homeassistant.const import LENGTH_INCHES, STATE_UNKNOWN, TEMP_CELSIUS from homeassistant.exceptions import PlatformNotReady from homeassistant.setup import async_setup_component -from tests.common import load_fixture, assert_setup_component + +from tests.common import assert_setup_component, load_fixture VALID_CONFIG_PWS = { "platform": "wunderground", @@ -50,47 +49,41 @@ URL = ( "http://api.wunderground.com/api/foo/alerts/conditions/forecast/lang" ":EN/q/32.87336,-117.22743.json" ) -PWS_URL = ( - "http://api.wunderground.com/api/foo/alerts/conditions/" "lang:EN/q/pws:bar.json" -) +PWS_URL = "http://api.wunderground.com/api/foo/alerts/conditions/lang:EN/q/pws:bar.json" INVALID_URL = ( - "http://api.wunderground.com/api/BOB/alerts/conditions/" "lang:foo/q/pws:bar.json" + "http://api.wunderground.com/api/BOB/alerts/conditions/lang:foo/q/pws:bar.json" ) -@asyncio.coroutine -def test_setup(hass, aioclient_mock): +async def test_setup(hass, aioclient_mock): """Test that the component is loaded.""" aioclient_mock.get(URL, text=load_fixture("wunderground-valid.json")) with assert_setup_component(1, "sensor"): - yield from async_setup_component(hass, "sensor", {"sensor": VALID_CONFIG}) + await async_setup_component(hass, "sensor", {"sensor": VALID_CONFIG}) -@asyncio.coroutine -def test_setup_pws(hass, aioclient_mock): +async def test_setup_pws(hass, aioclient_mock): """Test that the component is loaded with PWS id.""" aioclient_mock.get(PWS_URL, text=load_fixture("wunderground-valid.json")) with assert_setup_component(1, "sensor"): - yield from async_setup_component(hass, "sensor", {"sensor": VALID_CONFIG_PWS}) + await async_setup_component(hass, "sensor", {"sensor": VALID_CONFIG_PWS}) -@asyncio.coroutine -def test_setup_invalid(hass, aioclient_mock): +async def test_setup_invalid(hass, aioclient_mock): """Test that the component is not loaded with invalid config.""" aioclient_mock.get(INVALID_URL, text=load_fixture("wunderground-error.json")) with assert_setup_component(0, "sensor"): - yield from async_setup_component(hass, "sensor", {"sensor": INVALID_CONFIG}) + await async_setup_component(hass, "sensor", {"sensor": INVALID_CONFIG}) -@asyncio.coroutine -def test_sensor(hass, aioclient_mock): +async def test_sensor(hass, aioclient_mock): """Test the WUnderground sensor class and methods.""" aioclient_mock.get(URL, text=load_fixture("wunderground-valid.json")) - yield from async_setup_component(hass, "sensor", {"sensor": VALID_CONFIG}) + await async_setup_component(hass, "sensor", {"sensor": VALID_CONFIG}) state = hass.states.get("sensor.pws_weather") assert state.state == "Clear" @@ -131,20 +124,18 @@ def test_sensor(hass, aioclient_mock): assert state.attributes["unit_of_measurement"] == LENGTH_INCHES -@asyncio.coroutine -def test_connect_failed(hass, aioclient_mock): +async def test_connect_failed(hass, aioclient_mock): """Test the WUnderground connection error.""" aioclient_mock.get(URL, exc=aiohttp.ClientError()) with raises(PlatformNotReady): - yield from wunderground.async_setup_platform(hass, VALID_CONFIG, lambda _: None) + await wunderground.async_setup_platform(hass, VALID_CONFIG, lambda _: None) -@asyncio.coroutine -def test_invalid_data(hass, aioclient_mock): +async def test_invalid_data(hass, aioclient_mock): """Test the WUnderground invalid data.""" aioclient_mock.get(URL, text=load_fixture("wunderground-invalid.json")) - yield from async_setup_component(hass, "sensor", {"sensor": VALID_CONFIG}) + await async_setup_component(hass, "sensor", {"sensor": VALID_CONFIG}) for condition in VALID_CONFIG["monitored_conditions"]: state = hass.states.get("sensor.pws_" + condition) diff --git a/tests/components/xiaomi/test_device_tracker.py b/tests/components/xiaomi/test_device_tracker.py index 60e13ab3493..76788d0c3e8 100644 --- a/tests/components/xiaomi/test_device_tracker.py +++ b/tests/components/xiaomi/test_device_tracker.py @@ -1,13 +1,13 @@ """The tests for the Xiaomi router device tracker platform.""" import logging -from asynctest import mock, patch +from asynctest import mock, patch import requests from homeassistant.components.device_tracker import DOMAIN import homeassistant.components.xiaomi.device_tracker as xiaomi from homeassistant.components.xiaomi.device_tracker import get_scanner -from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD, CONF_PLATFORM +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PLATFORM, CONF_USERNAME _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/xiaomi_miio/test_vacuum.py b/tests/components/xiaomi_miio/test_vacuum.py index 18da270960c..7ebc59a964a 100644 --- a/tests/components/xiaomi_miio/test_vacuum.py +++ b/tests/components/xiaomi_miio/test_vacuum.py @@ -1,6 +1,5 @@ """The tests for the Xiaomi vacuum platform.""" -import asyncio -from datetime import timedelta, time +from datetime import time, timedelta from unittest import mock import pytest @@ -22,25 +21,26 @@ from homeassistant.components.vacuum import ( ) from homeassistant.components.xiaomi_miio.vacuum import ( ATTR_CLEANED_AREA, + ATTR_CLEANED_TOTAL_AREA, + ATTR_CLEANING_COUNT, ATTR_CLEANING_TIME, + ATTR_CLEANING_TOTAL_TIME, ATTR_DO_NOT_DISTURB, - ATTR_DO_NOT_DISTURB_START, ATTR_DO_NOT_DISTURB_END, + ATTR_DO_NOT_DISTURB_START, ATTR_ERROR, + ATTR_FILTER_LEFT, ATTR_MAIN_BRUSH_LEFT, ATTR_SIDE_BRUSH_LEFT, - ATTR_FILTER_LEFT, - ATTR_CLEANING_COUNT, - ATTR_CLEANED_TOTAL_AREA, - ATTR_CLEANING_TOTAL_TIME, CONF_HOST, CONF_NAME, CONF_TOKEN, + DOMAIN as XIAOMI_DOMAIN, + SERVICE_CLEAN_ZONE, SERVICE_MOVE_REMOTE_CONTROL, SERVICE_MOVE_REMOTE_CONTROL_STEP, SERVICE_START_REMOTE_CONTROL, SERVICE_STOP_REMOTE_CONTROL, - SERVICE_CLEAN_ZONE, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -148,11 +148,10 @@ def mirobo_errors_fixture(): yield mock_vacuum -@asyncio.coroutine -def test_xiaomi_exceptions(hass, caplog, mock_mirobo_errors): +async def test_xiaomi_exceptions(hass, caplog, mock_mirobo_errors): """Test vacuum supported features.""" entity_name = "test_vacuum_cleaner_error" - yield from async_setup_component( + await async_setup_component( hass, DOMAIN, { @@ -164,7 +163,7 @@ def test_xiaomi_exceptions(hass, caplog, mock_mirobo_errors): } }, ) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert "Initializing with host 127.0.0.1 (token 12345...)" in caplog.text assert mock_mirobo_errors.status.call_count == 1 @@ -172,13 +171,12 @@ def test_xiaomi_exceptions(hass, caplog, mock_mirobo_errors): assert "Got OSError while fetching the state" in caplog.text -@asyncio.coroutine -def test_xiaomi_vacuum_services(hass, caplog, mock_mirobo_is_got_error): +async def test_xiaomi_vacuum_services(hass, caplog, mock_mirobo_is_got_error): """Test vacuum supported features.""" entity_name = "test_vacuum_cleaner_1" entity_id = "{}.{}".format(DOMAIN, entity_name) - yield from async_setup_component( + await async_setup_component( hass, DOMAIN, { @@ -190,7 +188,7 @@ def test_xiaomi_vacuum_services(hass, caplog, mock_mirobo_is_got_error): } }, ) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert "Initializing with host 127.0.0.1 (token 12345...)" in caplog.text @@ -212,6 +210,7 @@ def test_xiaomi_vacuum_services(hass, caplog, mock_mirobo_is_got_error): "Balanced", "Turbo", "Max", + "Gentle", ] assert state.attributes.get(ATTR_MAIN_BRUSH_LEFT) == 12 assert state.attributes.get(ATTR_SIDE_BRUSH_LEFT) == 12 @@ -221,7 +220,7 @@ def test_xiaomi_vacuum_services(hass, caplog, mock_mirobo_is_got_error): assert state.attributes.get(ATTR_CLEANING_TOTAL_TIME) == 695 # Call services - yield from hass.services.async_call( + await hass.services.async_call( DOMAIN, SERVICE_START, {"entity_id": entity_id}, blocking=True ) mock_mirobo_is_got_error.assert_has_calls( @@ -230,28 +229,28 @@ def test_xiaomi_vacuum_services(hass, caplog, mock_mirobo_is_got_error): mock_mirobo_is_got_error.assert_has_calls(STATUS_CALLS, any_order=True) mock_mirobo_is_got_error.reset_mock() - yield from hass.services.async_call( + await hass.services.async_call( DOMAIN, SERVICE_STOP, {"entity_id": entity_id}, blocking=True ) mock_mirobo_is_got_error.assert_has_calls([mock.call.stop()], any_order=True) mock_mirobo_is_got_error.assert_has_calls(STATUS_CALLS, any_order=True) mock_mirobo_is_got_error.reset_mock() - yield from hass.services.async_call( + await hass.services.async_call( DOMAIN, SERVICE_RETURN_TO_BASE, {"entity_id": entity_id}, blocking=True ) mock_mirobo_is_got_error.assert_has_calls([mock.call.home()], any_order=True) mock_mirobo_is_got_error.assert_has_calls(STATUS_CALLS, any_order=True) mock_mirobo_is_got_error.reset_mock() - yield from hass.services.async_call( + await hass.services.async_call( DOMAIN, SERVICE_LOCATE, {"entity_id": entity_id}, blocking=True ) mock_mirobo_is_got_error.assert_has_calls([mock.call.find()], any_order=True) mock_mirobo_is_got_error.assert_has_calls(STATUS_CALLS, any_order=True) mock_mirobo_is_got_error.reset_mock() - yield from hass.services.async_call( + await hass.services.async_call( DOMAIN, SERVICE_CLEAN_SPOT, {"entity_id": entity_id}, blocking=True ) mock_mirobo_is_got_error.assert_has_calls([mock.call.spot()], any_order=True) @@ -259,7 +258,7 @@ def test_xiaomi_vacuum_services(hass, caplog, mock_mirobo_is_got_error): mock_mirobo_is_got_error.reset_mock() # Set speed service: - yield from hass.services.async_call( + await hass.services.async_call( DOMAIN, SERVICE_SET_FAN_SPEED, {"entity_id": entity_id, "fan_speed": 60}, @@ -271,7 +270,7 @@ def test_xiaomi_vacuum_services(hass, caplog, mock_mirobo_is_got_error): mock_mirobo_is_got_error.assert_has_calls(STATUS_CALLS, any_order=True) mock_mirobo_is_got_error.reset_mock() - yield from hass.services.async_call( + await hass.services.async_call( DOMAIN, SERVICE_SET_FAN_SPEED, {"entity_id": entity_id, "fan_speed": "turbo"}, @@ -284,7 +283,7 @@ def test_xiaomi_vacuum_services(hass, caplog, mock_mirobo_is_got_error): mock_mirobo_is_got_error.reset_mock() assert "ERROR" not in caplog.text - yield from hass.services.async_call( + await hass.services.async_call( DOMAIN, SERVICE_SET_FAN_SPEED, {"entity_id": entity_id, "fan_speed": "invent"}, @@ -292,7 +291,7 @@ def test_xiaomi_vacuum_services(hass, caplog, mock_mirobo_is_got_error): ) assert "ERROR" in caplog.text - yield from hass.services.async_call( + await hass.services.async_call( DOMAIN, SERVICE_SEND_COMMAND, {"entity_id": entity_id, "command": "raw"}, @@ -304,7 +303,7 @@ def test_xiaomi_vacuum_services(hass, caplog, mock_mirobo_is_got_error): mock_mirobo_is_got_error.assert_has_calls(STATUS_CALLS, any_order=True) mock_mirobo_is_got_error.reset_mock() - yield from hass.services.async_call( + await hass.services.async_call( DOMAIN, SERVICE_SEND_COMMAND, {"entity_id": entity_id, "command": "raw", "params": {"k1": 2}}, @@ -317,13 +316,12 @@ def test_xiaomi_vacuum_services(hass, caplog, mock_mirobo_is_got_error): mock_mirobo_is_got_error.reset_mock() -@asyncio.coroutine -def test_xiaomi_specific_services(hass, caplog, mock_mirobo_is_on): +async def test_xiaomi_specific_services(hass, caplog, mock_mirobo_is_on): """Test vacuum supported features.""" entity_name = "test_vacuum_cleaner_2" entity_id = "{}.{}".format(DOMAIN, entity_name) - yield from async_setup_component( + await async_setup_component( hass, DOMAIN, { @@ -335,7 +333,7 @@ def test_xiaomi_specific_services(hass, caplog, mock_mirobo_is_on): } }, ) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert "Initializing with host 192.168.1.100 (token 12345" in caplog.text @@ -354,6 +352,7 @@ def test_xiaomi_specific_services(hass, caplog, mock_mirobo_is_on): "Balanced", "Turbo", "Max", + "Gentle", ] assert state.attributes.get(ATTR_MAIN_BRUSH_LEFT) == 11 assert state.attributes.get(ATTR_SIDE_BRUSH_LEFT) == 11 @@ -363,8 +362,11 @@ def test_xiaomi_specific_services(hass, caplog, mock_mirobo_is_on): assert state.attributes.get(ATTR_CLEANING_TOTAL_TIME) == 675 # Xiaomi vacuum specific services: - yield from hass.services.async_call( - DOMAIN, SERVICE_START_REMOTE_CONTROL, {ATTR_ENTITY_ID: entity_id}, blocking=True + await hass.services.async_call( + XIAOMI_DOMAIN, + SERVICE_START_REMOTE_CONTROL, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, ) mock_mirobo_is_on.assert_has_calls([mock.call.manual_start()], any_order=True) @@ -372,8 +374,8 @@ def test_xiaomi_specific_services(hass, caplog, mock_mirobo_is_on): mock_mirobo_is_on.reset_mock() control = {"duration": 1000, "rotation": -40, "velocity": -0.1} - yield from hass.services.async_call( - DOMAIN, SERVICE_MOVE_REMOTE_CONTROL, control, blocking=True + await hass.services.async_call( + XIAOMI_DOMAIN, SERVICE_MOVE_REMOTE_CONTROL, control, blocking=True ) mock_mirobo_is_on.manual_control.assert_has_calls( [mock.call(**control)], any_order=True @@ -381,16 +383,16 @@ def test_xiaomi_specific_services(hass, caplog, mock_mirobo_is_on): mock_mirobo_is_on.assert_has_calls(STATUS_CALLS, any_order=True) mock_mirobo_is_on.reset_mock() - yield from hass.services.async_call( - DOMAIN, SERVICE_STOP_REMOTE_CONTROL, {}, blocking=True + await hass.services.async_call( + XIAOMI_DOMAIN, SERVICE_STOP_REMOTE_CONTROL, {}, blocking=True ) mock_mirobo_is_on.assert_has_calls([mock.call.manual_stop()], any_order=True) mock_mirobo_is_on.assert_has_calls(STATUS_CALLS, any_order=True) mock_mirobo_is_on.reset_mock() control_once = {"duration": 2000, "rotation": 120, "velocity": 0.1} - yield from hass.services.async_call( - DOMAIN, SERVICE_MOVE_REMOTE_CONTROL_STEP, control_once, blocking=True + await hass.services.async_call( + XIAOMI_DOMAIN, SERVICE_MOVE_REMOTE_CONTROL_STEP, control_once, blocking=True ) mock_mirobo_is_on.manual_control_once.assert_has_calls( [mock.call(**control_once)], any_order=True @@ -399,8 +401,8 @@ def test_xiaomi_specific_services(hass, caplog, mock_mirobo_is_on): mock_mirobo_is_on.reset_mock() control = {"zone": [[123, 123, 123, 123]], "repeats": 2} - yield from hass.services.async_call( - DOMAIN, SERVICE_CLEAN_ZONE, control, blocking=True + await hass.services.async_call( + XIAOMI_DOMAIN, SERVICE_CLEAN_ZONE, control, blocking=True ) mock_mirobo_is_on.zoned_clean.assert_has_calls( [mock.call([[123, 123, 123, 123, 2]])], any_order=True diff --git a/tests/components/yamaha/test_media_player.py b/tests/components/yamaha/test_media_player.py index 7e5b04f0269..6b101167c85 100644 --- a/tests/components/yamaha/test_media_player.py +++ b/tests/components/yamaha/test_media_player.py @@ -1,10 +1,11 @@ """The tests for the Yamaha Media player platform.""" import unittest -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch -from homeassistant.setup import setup_component import homeassistant.components.media_player as mp from homeassistant.components.yamaha import media_player as yamaha +from homeassistant.setup import setup_component + from tests.common import get_test_home_assistant diff --git a/tests/components/yandex_transport/test_yandex_transport_sensor.py b/tests/components/yandex_transport/test_yandex_transport_sensor.py index 7997d01bd13..a67108dc93b 100644 --- a/tests/components/yandex_transport/test_yandex_transport_sensor.py +++ b/tests/components/yandex_transport/test_yandex_transport_sensor.py @@ -1,15 +1,17 @@ """Tests for the yandex transport platform.""" import json + import pytest import homeassistant.components.sensor as sensor -import homeassistant.util.dt as dt_util from homeassistant.const import CONF_NAME +import homeassistant.util.dt as dt_util + from tests.common import ( + MockDependency, assert_setup_component, async_setup_component, - MockDependency, load_fixture, ) diff --git a/tests/components/yandextts/test_tts.py b/tests/components/yandextts/test_tts.py index c532732ccc5..edd5c058f12 100644 --- a/tests/components/yandextts/test_tts.py +++ b/tests/components/yandextts/test_tts.py @@ -3,14 +3,14 @@ import asyncio import os import shutil +from homeassistant.components.media_player.const import ( + DOMAIN as DOMAIN_MP, + SERVICE_PLAY_MEDIA, +) import homeassistant.components.tts as tts from homeassistant.setup import setup_component -from homeassistant.components.media_player.const import ( - SERVICE_PLAY_MEDIA, - DOMAIN as DOMAIN_MP, -) -from tests.common import get_test_home_assistant, assert_setup_component, mock_service +from tests.common import assert_setup_component, get_test_home_assistant, mock_service from tests.components.tts.test_init import mutagen_mock # noqa: F401 diff --git a/tests/components/yessssms/test_notify.py b/tests/components/yessssms/test_notify.py index dbc33b5a388..e5ef24ac150 100644 --- a/tests/components/yessssms/test_notify.py +++ b/tests/components/yessssms/test_notify.py @@ -1,16 +1,15 @@ """The tests for the notify yessssms platform.""" +import logging import unittest from unittest.mock import patch -import logging import pytest import requests_mock -from homeassistant.setup import async_setup_component -import homeassistant.components.yessssms.notify as yessssms from homeassistant.components.yessssms.const import CONF_PROVIDER - +import homeassistant.components.yessssms.notify as yessssms from homeassistant.const import CONF_PASSWORD, CONF_RECIPIENT, CONF_USERNAME +from homeassistant.setup import async_setup_component @pytest.fixture(name="config") diff --git a/tests/components/yr/test_sensor.py b/tests/components/yr/test_sensor.py index dce387b2c8c..161a7cef66b 100644 --- a/tests/components/yr/test_sensor.py +++ b/tests/components/yr/test_sensor.py @@ -1,29 +1,27 @@ """The tests for the Yr sensor platform.""" -import asyncio from datetime import datetime from unittest.mock import patch from homeassistant.bootstrap import async_setup_component import homeassistant.util.dt as dt_util -from tests.common import assert_setup_component, load_fixture +from tests.common import assert_setup_component, load_fixture NOW = datetime(2016, 6, 9, 1, tzinfo=dt_util.UTC) -@asyncio.coroutine -def test_default_setup(hass, aioclient_mock): +async def test_default_setup(hass, aioclient_mock): """Test the default setup.""" aioclient_mock.get( - "https://aa015h6buqvih86i1.api.met.no/" "weatherapi/locationforecast/1.9/", - text=load_fixture("yr.no.json"), + "https://aa015h6buqvih86i1.api.met.no/weatherapi/locationforecast/1.9/", + text=load_fixture("yr.no.xml"), ) config = {"platform": "yr", "elevation": 0} hass.allow_pool = True with patch( "homeassistant.components.yr.sensor.dt_util.utcnow", return_value=NOW ), assert_setup_component(1): - yield from async_setup_component(hass, "sensor", {"sensor": config}) + await async_setup_component(hass, "sensor", {"sensor": config}) state = hass.states.get("sensor.yr_symbol") @@ -31,12 +29,11 @@ def test_default_setup(hass, aioclient_mock): assert state.attributes.get("unit_of_measurement") is None -@asyncio.coroutine -def test_custom_setup(hass, aioclient_mock): +async def test_custom_setup(hass, aioclient_mock): """Test a custom setup.""" aioclient_mock.get( - "https://aa015h6buqvih86i1.api.met.no/" "weatherapi/locationforecast/1.9/", - text=load_fixture("yr.no.json"), + "https://aa015h6buqvih86i1.api.met.no/weatherapi/locationforecast/1.9/", + text=load_fixture("yr.no.xml"), ) config = { @@ -54,7 +51,7 @@ def test_custom_setup(hass, aioclient_mock): with patch( "homeassistant.components.yr.sensor.dt_util.utcnow", return_value=NOW ), assert_setup_component(1): - yield from async_setup_component(hass, "sensor", {"sensor": config}) + await async_setup_component(hass, "sensor", {"sensor": config}) state = hass.states.get("sensor.yr_pressure") assert state.attributes.get("unit_of_measurement") == "hPa" @@ -77,12 +74,11 @@ def test_custom_setup(hass, aioclient_mock): assert state.state == "3.5" -@asyncio.coroutine -def test_forecast_setup(hass, aioclient_mock): +async def test_forecast_setup(hass, aioclient_mock): """Test a custom setup with 24h forecast.""" aioclient_mock.get( - "https://aa015h6buqvih86i1.api.met.no/" "weatherapi/locationforecast/1.9/", - text=load_fixture("yr.no.json"), + "https://aa015h6buqvih86i1.api.met.no/weatherapi/locationforecast/1.9/", + text=load_fixture("yr.no.xml"), ) config = { @@ -101,7 +97,7 @@ def test_forecast_setup(hass, aioclient_mock): with patch( "homeassistant.components.yr.sensor.dt_util.utcnow", return_value=NOW ), assert_setup_component(1): - yield from async_setup_component(hass, "sensor", {"sensor": config}) + await async_setup_component(hass, "sensor", {"sensor": config}) state = hass.states.get("sensor.yr_pressure") assert state.attributes.get("unit_of_measurement") == "hPa" diff --git a/tests/components/yweather/test_sensor.py b/tests/components/yweather/test_sensor.py index f5f5852229b..25cb26d7c83 100644 --- a/tests/components/yweather/test_sensor.py +++ b/tests/components/yweather/test_sensor.py @@ -1,12 +1,11 @@ """The tests for the Yahoo weather sensor component.""" import json - import unittest from unittest.mock import patch from homeassistant.setup import setup_component -from tests.common import get_test_home_assistant, load_fixture, MockDependency +from tests.common import MockDependency, get_test_home_assistant, load_fixture VALID_CONFIG_MINIMAL = { "sensor": {"platform": "yweather", "monitored_conditions": ["weather"]} diff --git a/tests/components/yweather/test_weather.py b/tests/components/yweather/test_weather.py index 2c0435672e6..c9bf7bc89b6 100644 --- a/tests/components/yweather/test_weather.py +++ b/tests/components/yweather/test_weather.py @@ -1,6 +1,5 @@ """The tests for the Yahoo weather component.""" import json - import unittest from unittest.mock import patch @@ -11,10 +10,10 @@ from homeassistant.components.weather import ( ATTR_WEATHER_WIND_BEARING, ATTR_WEATHER_WIND_SPEED, ) -from homeassistant.util.unit_system import METRIC_SYSTEM from homeassistant.setup import setup_component +from homeassistant.util.unit_system import METRIC_SYSTEM -from tests.common import get_test_home_assistant, load_fixture, MockDependency +from tests.common import MockDependency, get_test_home_assistant, load_fixture def _yql_queryMock(yql): # pylint: disable=invalid-name diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index f60cf1d7bbb..00651bcfc5d 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -4,9 +4,9 @@ from unittest.mock import patch import pytest from zeroconf import ServiceInfo, ServiceStateChange +from homeassistant.components import zeroconf from homeassistant.generated import zeroconf as zc_gen from homeassistant.setup import async_setup_component -from homeassistant.components import zeroconf @pytest.fixture diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index 583b4e0738b..06712e638f6 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -36,10 +36,10 @@ APPLICATION = FakeApplication() class FakeEndpoint: """Fake endpoint for moking zigpy.""" - def __init__(self, manufacturer, model): + def __init__(self, manufacturer, model, epid=1): """Init fake endpoint.""" self.device = None - self.endpoint_id = 1 + self.endpoint_id = epid self.in_clusters = {} self.out_clusters = {} self._cluster_attr = {} @@ -93,23 +93,27 @@ class FakeDevice: self.manufacturer = manufacturer self.model = model self.node_desc = zigpy.zdo.types.NodeDescriptor() + self.add_to_group = CoroutineMock() + self.remove_from_group = CoroutineMock() -def make_device( - in_cluster_ids, out_cluster_ids, device_type, ieee, manufacturer, model -): +def make_device(endpoints, ieee, manufacturer, model): """Make a fake device using the specified cluster classes.""" device = FakeDevice(ieee, manufacturer, model) - endpoint = FakeEndpoint(manufacturer, model) - endpoint.device = device - device.endpoints[endpoint.endpoint_id] = endpoint - endpoint.device_type = device_type + for epid, ep in endpoints.items(): + endpoint = FakeEndpoint(manufacturer, model, epid) + endpoint.device = device + device.endpoints[epid] = endpoint + endpoint.device_type = ep["device_type"] + profile_id = ep.get("profile_id") + if profile_id: + endpoint.profile_id = profile_id - for cluster_id in in_cluster_ids: - endpoint.add_input_cluster(cluster_id) + for cluster_id in ep.get("in_clusters", []): + endpoint.add_input_cluster(cluster_id) - for cluster_id in out_cluster_ids: - endpoint.add_output_cluster(cluster_id) + for cluster_id in ep.get("out_clusters", []): + endpoint.add_output_cluster(cluster_id) return device @@ -134,7 +138,16 @@ async def async_init_zigpy_device( happens when the device is paired to the network for the first time. """ device = make_device( - in_cluster_ids, out_cluster_ids, device_type, ieee, manufacturer, model + { + 1: { + "in_clusters": in_cluster_ids, + "out_clusters": out_cluster_ids, + "device_type": device_type, + } + }, + ieee, + manufacturer, + model, ) if is_new_join: await gateway.async_device_initialized(device) diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index e34ad208744..cc8f9366ecb 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -1,7 +1,10 @@ """Test configuration for the ZHA component.""" +from unittest import mock from unittest.mock import patch import pytest +import zigpy +from zigpy.application import ControllerApplication from homeassistant import config_entries from homeassistant.components.zha.core.const import COMPONENTS, DATA_ZHA, DOMAIN @@ -12,6 +15,9 @@ from homeassistant.helpers.device_registry import async_get_registry as get_dev_ from .common import async_setup_entry +FIXTURE_GRP_ID = 0x1001 +FIXTURE_GRP_NAME = "fixture group" + @pytest.fixture(name="config_entry") def config_entry_fixture(hass): @@ -43,6 +49,11 @@ async def zha_gateway_fixture(hass, config_entry): gateway = ZHAGateway(hass, {}, config_entry) gateway.zha_storage = zha_storage gateway.ha_device_registry = dev_reg + gateway.application_controller = mock.MagicMock(spec_set=ControllerApplication) + groups = zigpy.group.Groups(gateway.application_controller) + groups.listener_event = mock.MagicMock() + groups.add_group(FIXTURE_GRP_ID, FIXTURE_GRP_NAME, suppress_event=True) + gateway.application_controller.groups = groups return gateway diff --git a/tests/components/zha/test_api.py b/tests/components/zha/test_api.py index 3fea9dfe088..f01d27eb167 100644 --- a/tests/components/zha/test_api.py +++ b/tests/components/zha/test_api.py @@ -1,7 +1,10 @@ """Test ZHA API.""" + import pytest +import zigpy import zigpy.zcl.clusters.general as general +from homeassistant.components.light import DOMAIN as light_domain from homeassistant.components.switch import DOMAIN from homeassistant.components.websocket_api import const from homeassistant.components.zha.api import ID, TYPE, async_load_api @@ -15,9 +18,13 @@ from homeassistant.components.zha.core.const import ( ATTR_NAME, ATTR_QUIRK_APPLIED, CLUSTER_TYPE_IN, + GROUP_ID, + GROUP_IDS, + GROUP_NAME, ) from .common import async_init_zigpy_device +from .conftest import FIXTURE_GRP_ID, FIXTURE_GRP_NAME @pytest.fixture @@ -36,9 +43,22 @@ async def zha_client(hass, config_entry, zha_gateway, hass_ws_client): zha_gateway, ) + await async_init_zigpy_device( + hass, + [general.OnOff.cluster_id, general.Basic.cluster_id, general.Groups.cluster_id], + [], + zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT, + zha_gateway, + manufacturer="FakeGroupManufacturer", + model="FakeGroupModel", + ieee="01:2d:6f:00:0a:90:69:e8", + ) + # load up switch domain await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) await hass.async_block_till_done() + await hass.config_entries.async_forward_entry_setup(config_entry, light_domain) + await hass.async_block_till_done() return await hass_ws_client(hass) @@ -114,15 +134,17 @@ async def test_device_cluster_commands(hass, config_entry, zha_gateway, zha_clie async def test_list_devices(hass, config_entry, zha_gateway, zha_client): - """Test getting entity cluster commands.""" + """Test getting zha devices.""" await zha_client.send_json({ID: 5, TYPE: "zha/devices"}) msg = await zha_client.receive_json() devices = msg["result"] - assert len(devices) == 1 + assert len(devices) == 2 + msg_id = 100 for device in devices: + msg_id += 1 assert device[ATTR_IEEE] is not None assert device[ATTR_MANUFACTURER] is not None assert device[ATTR_MODEL] is not None @@ -135,7 +157,7 @@ async def test_list_devices(hass, config_entry, zha_gateway, zha_client): assert entity_reference["entity_id"] is not None await zha_client.send_json( - {ID: 6, TYPE: "zha/device", ATTR_IEEE: device[ATTR_IEEE]} + {ID: msg_id, TYPE: "zha/device", ATTR_IEEE: device[ATTR_IEEE]} ) msg = await zha_client.receive_json() device2 = msg["result"] @@ -152,3 +174,151 @@ async def test_device_not_found(hass, config_entry, zha_gateway, zha_client): assert msg["type"] == const.TYPE_RESULT assert not msg["success"] assert msg["error"]["code"] == const.ERR_NOT_FOUND + + +async def test_list_groups(hass, config_entry, zha_gateway, zha_client): + """Test getting zha zigbee groups.""" + await zha_client.send_json({ID: 7, TYPE: "zha/groups"}) + + msg = await zha_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == const.TYPE_RESULT + + groups = msg["result"] + assert len(groups) == 1 + + for group in groups: + assert group["group_id"] == FIXTURE_GRP_ID + assert group["name"] == FIXTURE_GRP_NAME + assert group["members"] == [] + + +async def test_get_group(hass, config_entry, zha_gateway, zha_client): + """Test getting a specific zha zigbee group.""" + await zha_client.send_json({ID: 8, TYPE: "zha/group", GROUP_ID: FIXTURE_GRP_ID}) + + msg = await zha_client.receive_json() + assert msg["id"] == 8 + assert msg["type"] == const.TYPE_RESULT + + group = msg["result"] + assert group is not None + assert group["group_id"] == FIXTURE_GRP_ID + assert group["name"] == FIXTURE_GRP_NAME + assert group["members"] == [] + + +async def test_get_group_not_found(hass, config_entry, zha_gateway, zha_client): + """Test not found response from get group API.""" + await zha_client.send_json({ID: 9, TYPE: "zha/group", GROUP_ID: 1234567}) + + msg = await zha_client.receive_json() + + assert msg["id"] == 9 + assert msg["type"] == const.TYPE_RESULT + assert not msg["success"] + assert msg["error"]["code"] == const.ERR_NOT_FOUND + + +async def test_list_groupable_devices(hass, config_entry, zha_gateway, zha_client): + """Test getting zha devices that have a group cluster.""" + + # Make device available + zha_gateway.devices[ + zigpy.types.EUI64.convert("01:2d:6f:00:0a:90:69:e8") + ].set_available(True) + + await zha_client.send_json({ID: 10, TYPE: "zha/devices/groupable"}) + + msg = await zha_client.receive_json() + assert msg["id"] == 10 + assert msg["type"] == const.TYPE_RESULT + + devices = msg["result"] + assert len(devices) == 1 + + for device in devices: + assert device[ATTR_IEEE] == "01:2d:6f:00:0a:90:69:e8" + assert device[ATTR_MANUFACTURER] is not None + assert device[ATTR_MODEL] is not None + assert device[ATTR_NAME] is not None + assert device[ATTR_QUIRK_APPLIED] is not None + assert device["entities"] is not None + + for entity_reference in device["entities"]: + assert entity_reference[ATTR_NAME] is not None + assert entity_reference["entity_id"] is not None + + # Make sure there are no groupable devices when the device is unavailable + # Make device unavailable + zha_gateway.devices[ + zigpy.types.EUI64.convert("01:2d:6f:00:0a:90:69:e8") + ].set_available(False) + + await zha_client.send_json({ID: 11, TYPE: "zha/devices/groupable"}) + + msg = await zha_client.receive_json() + assert msg["id"] == 11 + assert msg["type"] == const.TYPE_RESULT + + devices = msg["result"] + assert len(devices) == 0 + + +async def test_add_group(hass, config_entry, zha_gateway, zha_client): + """Test adding and getting a new zha zigbee group.""" + await zha_client.send_json({ID: 12, TYPE: "zha/group/add", GROUP_NAME: "new_group"}) + + msg = await zha_client.receive_json() + assert msg["id"] == 12 + assert msg["type"] == const.TYPE_RESULT + + added_group = msg["result"] + + assert added_group["name"] == "new_group" + assert added_group["members"] == [] + + await zha_client.send_json({ID: 13, TYPE: "zha/groups"}) + + msg = await zha_client.receive_json() + assert msg["id"] == 13 + assert msg["type"] == const.TYPE_RESULT + + groups = msg["result"] + assert len(groups) == 2 + + for group in groups: + assert group["name"] == FIXTURE_GRP_NAME or group["name"] == "new_group" + + +async def test_remove_group(hass, config_entry, zha_gateway, zha_client): + """Test removing a new zha zigbee group.""" + + await zha_client.send_json({ID: 14, TYPE: "zha/groups"}) + + msg = await zha_client.receive_json() + assert msg["id"] == 14 + assert msg["type"] == const.TYPE_RESULT + + groups = msg["result"] + assert len(groups) == 1 + + await zha_client.send_json( + {ID: 15, TYPE: "zha/group/remove", GROUP_IDS: [FIXTURE_GRP_ID]} + ) + + msg = await zha_client.receive_json() + assert msg["id"] == 15 + assert msg["type"] == const.TYPE_RESULT + + groups_remaining = msg["result"] + assert len(groups_remaining) == 0 + + await zha_client.send_json({ID: 16, TYPE: "zha/groups"}) + + msg = await zha_client.receive_json() + assert msg["id"] == 16 + assert msg["type"] == const.TYPE_RESULT + + groups = msg["result"] + assert len(groups) == 0 diff --git a/tests/components/zha/test_channels.py b/tests/components/zha/test_channels.py index 3be3aaf0930..557cc0f2c5c 100644 --- a/tests/components/zha/test_channels.py +++ b/tests/components/zha/test_channels.py @@ -67,9 +67,7 @@ def nwk(): async def test_in_channel_config(cluster_id, bind_count, attrs, zha_gateway, hass): """Test ZHA core channel configuration for input clusters.""" zigpy_dev = make_device( - [cluster_id], - [], - 0x1234, + {1: {"in_clusters": [cluster_id], "out_clusters": [], "device_type": 0x1234}}, "00:11:22:33:44:55:66:77", "test manufacturer", "test model", @@ -125,9 +123,7 @@ async def test_in_channel_config(cluster_id, bind_count, attrs, zha_gateway, has async def test_out_channel_config(cluster_id, bind_count, zha_gateway, hass): """Test ZHA core channel configuration for output clusters.""" zigpy_dev = make_device( - [], - [cluster_id], - 0x1234, + {1: {"out_clusters": [cluster_id], "in_clusters": [], "device_type": 0x1234}}, "00:11:22:33:44:55:66:77", "test manufacturer", "test model", diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index 5e6bf51afd6..7e0b89f8d70 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -1,8 +1,11 @@ """Tests for ZHA config flow.""" -from asynctest import patch +from unittest import mock + +import asynctest from homeassistant.components.zha import config_flow -from homeassistant.components.zha.core.const import DOMAIN +from homeassistant.components.zha.core.const import CONTROLLER, DOMAIN, ZHA_GW_RADIO +import homeassistant.components.zha.core.registries from tests.common import MockConfigEntry @@ -12,8 +15,8 @@ async def test_user_flow(hass): flow = config_flow.ZhaFlowHandler() flow.hass = hass - with patch( - "homeassistant.components.zha.config_flow" ".check_zigpy_connection", + with asynctest.patch( + "homeassistant.components.zha.config_flow.check_zigpy_connection", return_value=False, ): result = await flow.async_step_user( @@ -22,8 +25,8 @@ async def test_user_flow(hass): assert result["errors"] == {"base": "cannot_connect"} - with patch( - "homeassistant.components.zha.config_flow" ".check_zigpy_connection", + with asynctest.patch( + "homeassistant.components.zha.config_flow.check_zigpy_connection", return_value=True, ): result = await flow.async_step_user( @@ -71,3 +74,53 @@ async def test_import_flow_existing_config_entry(hass): ) assert result["type"] == "abort" + + +async def test_check_zigpy_connection(): + """Test config flow validator.""" + + mock_radio = asynctest.MagicMock() + mock_radio.connect = asynctest.CoroutineMock() + radio_cls = asynctest.MagicMock(return_value=mock_radio) + + bad_radio = asynctest.MagicMock() + bad_radio.connect = asynctest.CoroutineMock(side_effect=Exception) + bad_radio_cls = asynctest.MagicMock(return_value=bad_radio) + + mock_ctrl = asynctest.MagicMock() + mock_ctrl.startup = asynctest.CoroutineMock() + mock_ctrl.shutdown = asynctest.CoroutineMock() + ctrl_cls = asynctest.MagicMock(return_value=mock_ctrl) + new_radios = { + mock.sentinel.radio: {ZHA_GW_RADIO: radio_cls, CONTROLLER: ctrl_cls}, + mock.sentinel.bad_radio: {ZHA_GW_RADIO: bad_radio_cls, CONTROLLER: ctrl_cls}, + } + + with mock.patch.dict( + homeassistant.components.zha.core.registries.RADIO_TYPES, new_radios, clear=True + ): + assert not await config_flow.check_zigpy_connection( + mock.sentinel.usb_path, mock.sentinel.unk_radio, mock.sentinel.zigbee_db + ) + assert mock_radio.connect.call_count == 0 + assert bad_radio.connect.call_count == 0 + assert mock_ctrl.startup.call_count == 0 + assert mock_ctrl.shutdown.call_count == 0 + + # unsuccessful radio connect + assert not await config_flow.check_zigpy_connection( + mock.sentinel.usb_path, mock.sentinel.bad_radio, mock.sentinel.zigbee_db + ) + assert mock_radio.connect.call_count == 0 + assert bad_radio.connect.call_count == 1 + assert mock_ctrl.startup.call_count == 0 + assert mock_ctrl.shutdown.call_count == 0 + + # successful radio connect + assert await config_flow.check_zigpy_connection( + mock.sentinel.usb_path, mock.sentinel.radio, mock.sentinel.zigbee_db + ) + assert mock_radio.connect.call_count == 1 + assert bad_radio.connect.call_count == 1 + assert mock_ctrl.startup.call_count == 1 + assert mock_ctrl.shutdown.call_count == 1 diff --git a/tests/components/zha/test_discover.py b/tests/components/zha/test_discover.py new file mode 100644 index 00000000000..91805acc448 --- /dev/null +++ b/tests/components/zha/test_discover.py @@ -0,0 +1,55 @@ +"""Test zha device discovery.""" + +import asyncio +from unittest import mock + +import pytest + +from homeassistant.components.zha.core.channels import EventRelayChannel +import homeassistant.components.zha.core.const as zha_const +import homeassistant.components.zha.core.discovery as disc +import homeassistant.components.zha.core.gateway as core_zha_gw + +from .common import make_device +from .zha_devices_list import DEVICES + + +@pytest.mark.parametrize("device", DEVICES) +async def test_devices(device, zha_gateway: core_zha_gw.ZHAGateway, hass, config_entry): + """Test device discovery.""" + + zigpy_device = make_device( + device["endpoints"], + "00:11:22:33:44:55:66:77", + device["manufacturer"], + device["model"], + ) + + with mock.patch( + "homeassistant.components.zha.core.discovery._async_create_cluster_channel", + wraps=disc._async_create_cluster_channel, + ) as cr_ch: + await zha_gateway.async_device_restored(zigpy_device) + await hass.async_block_till_done() + tasks = [ + hass.config_entries.async_forward_entry_setup(config_entry, component) + for component in zha_const.COMPONENTS + ] + await asyncio.gather(*tasks) + + await hass.async_block_till_done() + + entity_ids = hass.states.async_entity_ids() + await hass.async_block_till_done() + zha_entities = { + ent for ent in entity_ids if ent.split(".")[0] in zha_const.COMPONENTS + } + + event_channels = { + arg[0].cluster_id + for arg, kwarg in cr_ch.call_args_list + if kwarg.get("channel_class") == EventRelayChannel + } + + assert zha_entities == set(device["entities"]) + assert event_channels == set(device["event_channels"]) diff --git a/tests/components/zha/test_registries.py b/tests/components/zha/test_registries.py new file mode 100644 index 00000000000..9f77330dd55 --- /dev/null +++ b/tests/components/zha/test_registries.py @@ -0,0 +1,209 @@ +"""Test ZHA registries.""" +from unittest import mock + +import pytest + +import homeassistant.components.zha.core.registries as registries + +MANUFACTURER = "mock manufacturer" +MODEL = "mock model" + + +@pytest.fixture +def zha_device(): + """Return a mock of ZHA device.""" + dev = mock.MagicMock() + dev.manufacturer = MANUFACTURER + dev.model = MODEL + return dev + + +@pytest.fixture +def channels(): + """Return a mock of channels.""" + + def channel(name, chan_id): + ch = mock.MagicMock() + ch.name = name + ch.generic_id = chan_id + return ch + + return [channel("level", "channel_0x0008"), channel("on_off", "channel_0x0006")] + + +@pytest.mark.parametrize( + "rule, matched", + [ + (registries.MatchRule(), False), + (registries.MatchRule(channel_names={"level"}), True), + (registries.MatchRule(channel_names={"level", "no match"}), False), + (registries.MatchRule(channel_names={"on_off"}), True), + (registries.MatchRule(channel_names={"on_off", "no match"}), False), + (registries.MatchRule(channel_names={"on_off", "level"}), True), + (registries.MatchRule(channel_names={"on_off", "level", "no match"}), False), + # test generic_id matching + (registries.MatchRule(generic_ids={"channel_0x0006"}), True), + (registries.MatchRule(generic_ids={"channel_0x0008"}), True), + (registries.MatchRule(generic_ids={"channel_0x0006", "channel_0x0008"}), True), + ( + registries.MatchRule( + generic_ids={"channel_0x0006", "channel_0x0008", "channel_0x0009"} + ), + False, + ), + ( + registries.MatchRule( + generic_ids={"channel_0x0006", "channel_0x0008"}, + channel_names={"on_off", "level"}, + ), + True, + ), + # manufacturer matching + (registries.MatchRule(manufacturers="no match"), False), + (registries.MatchRule(manufacturers=MANUFACTURER), True), + (registries.MatchRule(models=MODEL), True), + (registries.MatchRule(models="no match"), False), + # match everything + ( + registries.MatchRule( + generic_ids={"channel_0x0006", "channel_0x0008"}, + channel_names={"on_off", "level"}, + manufacturers=MANUFACTURER, + models=MODEL, + ), + True, + ), + ( + registries.MatchRule( + channel_names="on_off", manufacturers={"random manuf", MANUFACTURER} + ), + True, + ), + ( + registries.MatchRule( + channel_names="on_off", manufacturers={"random manuf", "Another manuf"} + ), + False, + ), + ( + registries.MatchRule( + channel_names="on_off", manufacturers=lambda x: x == MANUFACTURER + ), + True, + ), + ( + registries.MatchRule( + channel_names="on_off", manufacturers=lambda x: x != MANUFACTURER + ), + False, + ), + ( + registries.MatchRule( + channel_names="on_off", models={"random model", MODEL} + ), + True, + ), + ( + registries.MatchRule( + channel_names="on_off", models={"random model", "Another model"} + ), + False, + ), + ( + registries.MatchRule(channel_names="on_off", models=lambda x: x == MODEL), + True, + ), + ( + registries.MatchRule(channel_names="on_off", models=lambda x: x != MODEL), + False, + ), + ], +) +def test_registry_matching(rule, matched, zha_device, channels): + """Test strict rule matching.""" + reg = registries.ZHAEntityRegistry() + assert reg._strict_matched(zha_device, channels, rule) is matched + + +@pytest.mark.parametrize( + "rule, matched", + [ + (registries.MatchRule(), False), + (registries.MatchRule(channel_names={"level"}), True), + (registries.MatchRule(channel_names={"level", "no match"}), False), + (registries.MatchRule(channel_names={"on_off"}), True), + (registries.MatchRule(channel_names={"on_off", "no match"}), False), + (registries.MatchRule(channel_names={"on_off", "level"}), True), + (registries.MatchRule(channel_names={"on_off", "level", "no match"}), False), + ( + registries.MatchRule(channel_names={"on_off", "level"}, models="no match"), + True, + ), + ( + registries.MatchRule( + channel_names={"on_off", "level"}, + models="no match", + manufacturers="no match", + ), + True, + ), + ( + registries.MatchRule( + channel_names={"on_off", "level"}, + models="no match", + manufacturers=MANUFACTURER, + ), + True, + ), + # test generic_id matching + (registries.MatchRule(generic_ids={"channel_0x0006"}), True), + (registries.MatchRule(generic_ids={"channel_0x0008"}), True), + (registries.MatchRule(generic_ids={"channel_0x0006", "channel_0x0008"}), True), + ( + registries.MatchRule( + generic_ids={"channel_0x0006", "channel_0x0008", "channel_0x0009"} + ), + False, + ), + ( + registries.MatchRule( + generic_ids={"channel_0x0006", "channel_0x0008", "channel_0x0009"}, + models="mo match", + ), + False, + ), + ( + registries.MatchRule( + generic_ids={"channel_0x0006", "channel_0x0008", "channel_0x0009"}, + models=MODEL, + ), + True, + ), + ( + registries.MatchRule( + generic_ids={"channel_0x0006", "channel_0x0008"}, + channel_names={"on_off", "level"}, + ), + True, + ), + # manufacturer matching + (registries.MatchRule(manufacturers="no match"), False), + (registries.MatchRule(manufacturers=MANUFACTURER), True), + (registries.MatchRule(models=MODEL), True), + (registries.MatchRule(models="no match"), False), + # match everything + ( + registries.MatchRule( + generic_ids={"channel_0x0006", "channel_0x0008"}, + channel_names={"on_off", "level"}, + manufacturers=MANUFACTURER, + models=MODEL, + ), + True, + ), + ], +) +def test_registry_loose_matching(rule, matched, zha_device, channels): + """Test loose rule matching.""" + reg = registries.ZHAEntityRegistry() + assert reg._loose_matched(zha_device, channels, rule) is matched diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index 7746c5d422e..4fa16f06b04 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -166,7 +166,7 @@ async def async_test_metering(hass, device_info): async def async_test_electrical_measurement(hass, device_info): """Test electrical measurement sensor.""" await send_attribute_report(hass, device_info["cluster"], 1291, 100) - assert_state(hass, device_info, "10.0", "W") + assert_state(hass, device_info, "100", "W") async def send_attribute_report(hass, cluster, attrid, value): diff --git a/tests/components/zha/zha_devices_list.py b/tests/components/zha/zha_devices_list.py new file mode 100644 index 00000000000..d5875edc9e2 --- /dev/null +++ b/tests/components/zha/zha_devices_list.py @@ -0,0 +1,1874 @@ +"""Example Zigbee Devices.""" + +DEVICES = [ + { + "endpoints": { + "1": { + "device_type": 2080, + "endpoint_id": 1, + "in_clusters": [0, 3, 4096, 64716], + "out_clusters": [3, 4, 6, 8, 4096, 64716], + "profile_id": 260, + } + }, + "entities": [], + "event_channels": [6, 8], + "manufacturer": "ADUROLIGHT", + "model": "Adurolight_NCC", + }, + { + "endpoints": { + "5": { + "device_type": 1026, + "endpoint_id": 5, + "in_clusters": [0, 1, 3, 32, 1026, 1280, 2821], + "out_clusters": [25], + "profile_id": 260, + } + }, + "entities": [ + "binary_sensor.bosch_isw_zpr1_wp13_77665544_ias_zone", + "sensor.bosch_isw_zpr1_wp13_77665544_power", + "sensor.bosch_isw_zpr1_wp13_77665544_temperature", + ], + "event_channels": [], + "manufacturer": "Bosch", + "model": "ISW-ZPR1-WP13", + }, + { + "endpoints": { + "1": { + "device_type": 1, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 32, 2821], + "out_clusters": [3, 6, 8, 25], + "profile_id": 260, + } + }, + "entities": [ + "binary_sensor.centralite_3130_77665544_on_off", + "sensor.centralite_3130_77665544_power", + ], + "event_channels": [6, 8], + "manufacturer": "CentraLite", + "model": "3130", + }, + { + "endpoints": { + "1": { + "device_type": 81, + "endpoint_id": 1, + "in_clusters": [0, 3, 4, 5, 6, 1794, 2820, 2821, 64515], + "out_clusters": [25], + "profile_id": 260, + } + }, + "entities": [ + "sensor.centralite_3210_l_77665544_smartenergy_metering", + "sensor.centralite_3210_l_77665544_electrical_measurement", + "switch.centralite_3210_l_77665544_on_off", + ], + "event_channels": [], + "manufacturer": "CentraLite", + "model": "3210-L", + }, + { + "endpoints": { + "1": { + "device_type": 770, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 32, 1026, 2821, 64581], + "out_clusters": [3, 25], + "profile_id": 260, + } + }, + "entities": [ + "sensor.centralite_3310_s_77665544_power", + "sensor.centralite_3310_s_77665544_temperature", + "sensor.centralite_3310_s_77665544_manufacturer_specific", + ], + "event_channels": [], + "manufacturer": "CentraLite", + "model": "3310-S", + }, + { + "endpoints": { + "1": { + "device_type": 1026, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 32, 1026, 1280, 2821], + "out_clusters": [25], + "profile_id": 260, + }, + "2": { + "device_type": 12, + "endpoint_id": 2, + "in_clusters": [0, 3, 2821, 64527], + "out_clusters": [3], + "profile_id": 49887, + }, + }, + "entities": [ + "binary_sensor.centralite_3315_s_77665544_ias_zone", + "sensor.centralite_3315_s_77665544_temperature", + "sensor.centralite_3315_s_77665544_power", + ], + "event_channels": [], + "manufacturer": "CentraLite", + "model": "3315-S", + }, + { + "endpoints": { + "1": { + "device_type": 1026, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 32, 1026, 1280, 2821], + "out_clusters": [25], + "profile_id": 260, + }, + "2": { + "device_type": 12, + "endpoint_id": 2, + "in_clusters": [0, 3, 2821, 64527], + "out_clusters": [3], + "profile_id": 49887, + }, + }, + "entities": [ + "binary_sensor.centralite_3320_l_77665544_ias_zone", + "sensor.centralite_3320_l_77665544_temperature", + "sensor.centralite_3320_l_77665544_power", + ], + "event_channels": [], + "manufacturer": "CentraLite", + "model": "3320-L", + }, + { + "endpoints": { + "1": { + "device_type": 1026, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 32, 1026, 1280, 2821], + "out_clusters": [25], + "profile_id": 260, + }, + "2": { + "device_type": 263, + "endpoint_id": 2, + "in_clusters": [0, 3, 2821, 64582], + "out_clusters": [3], + "profile_id": 49887, + }, + }, + "entities": [ + "binary_sensor.centralite_3326_l_77665544_ias_zone", + "sensor.centralite_3326_l_77665544_temperature", + "sensor.centralite_3326_l_77665544_power", + ], + "event_channels": [], + "manufacturer": "CentraLite", + "model": "3326-L", + }, + { + "endpoints": { + "1": { + "device_type": 1026, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 32, 1026, 1280, 2821], + "out_clusters": [25], + "profile_id": 260, + }, + "2": { + "device_type": 263, + "endpoint_id": 2, + "in_clusters": [0, 3, 1030, 2821], + "out_clusters": [3], + "profile_id": 260, + }, + }, + "entities": [ + "binary_sensor.centralite_motion_sensor_a_77665544_occupancy", + "binary_sensor.centralite_motion_sensor_a_77665544_ias_zone", + "sensor.centralite_motion_sensor_a_77665544_temperature", + "sensor.centralite_motion_sensor_a_77665544_power", + ], + "event_channels": [], + "manufacturer": "CentraLite", + "model": "Motion Sensor-A", + }, + { + "endpoints": { + "1": { + "device_type": 81, + "endpoint_id": 1, + "in_clusters": [0, 3, 4, 5, 6, 1794], + "out_clusters": [0], + "profile_id": 260, + }, + "4": { + "device_type": 9, + "endpoint_id": 4, + "in_clusters": [], + "out_clusters": [25], + "profile_id": 260, + }, + }, + "entities": [ + "sensor.climaxtechnology_psmp5_00_00_02_02tc_77665544_smartenergy_metering", + "switch.climaxtechnology_psmp5_00_00_02_02tc_77665544_on_off", + ], + "event_channels": [], + "manufacturer": "ClimaxTechnology", + "model": "PSMP5_00.00.02.02TC", + }, + { + "endpoints": { + "1": { + "device_type": 1026, + "endpoint_id": 1, + "in_clusters": [0, 3, 1280, 1282], + "out_clusters": [0], + "profile_id": 260, + } + }, + "entities": [ + "binary_sensor.climaxtechnology_sd8sc_00_00_03_12tc_77665544_ias_zone" + ], + "event_channels": [], + "manufacturer": "ClimaxTechnology", + "model": "SD8SC_00.00.03.12TC", + }, + { + "endpoints": { + "1": { + "device_type": 1026, + "endpoint_id": 1, + "in_clusters": [0, 3, 1280], + "out_clusters": [0], + "profile_id": 260, + } + }, + "entities": [ + "binary_sensor.climaxtechnology_ws15_00_00_03_03tc_77665544_ias_zone" + ], + "event_channels": [], + "manufacturer": "ClimaxTechnology", + "model": "WS15_00.00.03.03TC", + }, + { + "endpoints": { + "11": { + "device_type": 528, + "endpoint_id": 11, + "in_clusters": [0, 3, 4, 5, 6, 8, 768], + "out_clusters": [], + "profile_id": 49246, + }, + "13": { + "device_type": 57694, + "endpoint_id": 13, + "in_clusters": [4096], + "out_clusters": [4096], + "profile_id": 49246, + }, + }, + "entities": [ + "light.feibit_inc_co_fb56_zcw08ku1_1_77665544_level_light_color_on_off" + ], + "event_channels": [], + "manufacturer": "Feibit Inc co.", + "model": "FB56-ZCW08KU1.1", + }, + { + "endpoints": { + "1": { + "device_type": 1027, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 4, 9, 1280, 1282], + "out_clusters": [3, 25], + "profile_id": 260, + } + }, + "entities": [ + "binary_sensor.heiman_warningdevice_77665544_ias_zone", + "sensor.heiman_warningdevice_77665544_power", + ], + "event_channels": [], + "manufacturer": "Heiman", + "model": "WarningDevice", + }, + { + "endpoints": { + "6": { + "device_type": 1026, + "endpoint_id": 6, + "in_clusters": [0, 1, 3, 32, 1024, 1026, 1280], + "out_clusters": [25], + "profile_id": 260, + } + }, + "entities": [ + "sensor.hivehome_com_mot003_77665544_temperature", + "sensor.hivehome_com_mot003_77665544_power", + "sensor.hivehome_com_mot003_77665544_illuminance", + "binary_sensor.hivehome_com_mot003_77665544_ias_zone", + ], + "event_channels": [], + "manufacturer": "HiveHome.com", + "model": "MOT003", + }, + { + "endpoints": { + "1": { + "device_type": 268, + "endpoint_id": 1, + "in_clusters": [0, 3, 4, 5, 6, 8, 768, 4096, 64636], + "out_clusters": [5, 25, 32, 4096], + "profile_id": 260, + }, + "242": { + "device_type": 97, + "endpoint_id": 242, + "in_clusters": [33], + "out_clusters": [33], + "profile_id": 41440, + }, + }, + "entities": [ + "light.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_77665544_level_light_color_on_off" + ], + "event_channels": [], + "manufacturer": "IKEA of Sweden", + "model": "TRADFRI bulb E12 WS opal 600lm", + }, + { + "endpoints": { + "1": { + "device_type": 512, + "endpoint_id": 1, + "in_clusters": [0, 3, 4, 5, 6, 8, 768, 2821, 4096], + "out_clusters": [5, 25, 32, 4096], + "profile_id": 49246, + } + }, + "entities": [ + "light.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_77665544_level_light_color_on_off" + ], + "event_channels": [], + "manufacturer": "IKEA of Sweden", + "model": "TRADFRI bulb E26 CWS opal 600lm", + }, + { + "endpoints": { + "1": { + "device_type": 256, + "endpoint_id": 1, + "in_clusters": [0, 3, 4, 5, 6, 8, 2821, 4096], + "out_clusters": [5, 25, 32, 4096], + "profile_id": 49246, + } + }, + "entities": [ + "light.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_77665544_level_on_off" + ], + "event_channels": [], + "manufacturer": "IKEA of Sweden", + "model": "TRADFRI bulb E26 W opal 1000lm", + }, + { + "endpoints": { + "1": { + "device_type": 544, + "endpoint_id": 1, + "in_clusters": [0, 3, 4, 5, 6, 8, 768, 2821, 4096], + "out_clusters": [5, 25, 32, 4096], + "profile_id": 49246, + } + }, + "entities": [ + "light.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_77665544_level_light_color_on_off" + ], + "event_channels": [], + "manufacturer": "IKEA of Sweden", + "model": "TRADFRI bulb E26 WS opal 980lm", + }, + { + "endpoints": { + "1": { + "device_type": 256, + "endpoint_id": 1, + "in_clusters": [0, 3, 4, 5, 6, 8, 2821, 4096], + "out_clusters": [5, 25, 32, 4096], + "profile_id": 260, + } + }, + "entities": [ + "light.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_77665544_level_on_off" + ], + "event_channels": [], + "manufacturer": "IKEA of Sweden", + "model": "TRADFRI bulb E26 opal 1000lm", + }, + { + "endpoints": { + "1": { + "device_type": 266, + "endpoint_id": 1, + "in_clusters": [0, 3, 4, 5, 6, 64636], + "out_clusters": [5, 25, 32], + "profile_id": 260, + } + }, + "entities": ["switch.ikea_of_sweden_tradfri_control_outlet_77665544_on_off"], + "event_channels": [], + "manufacturer": "IKEA of Sweden", + "model": "TRADFRI control outlet", + }, + { + "endpoints": { + "1": { + "device_type": 2128, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 9, 2821, 4096], + "out_clusters": [3, 4, 6, 25, 4096], + "profile_id": 49246, + } + }, + "entities": [ + "binary_sensor.ikea_of_sweden_tradfri_motion_sensor_77665544_on_off", + "sensor.ikea_of_sweden_tradfri_motion_sensor_77665544_power", + ], + "event_channels": [6], + "manufacturer": "IKEA of Sweden", + "model": "TRADFRI motion sensor", + }, + { + "endpoints": { + "1": { + "device_type": 2080, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 9, 32, 4096, 64636], + "out_clusters": [3, 4, 6, 8, 25, 258, 4096], + "profile_id": 260, + } + }, + "entities": ["sensor.ikea_of_sweden_tradfri_on_off_switch_77665544_power"], + "event_channels": [6, 8], + "manufacturer": "IKEA of Sweden", + "model": "TRADFRI on/off switch", + }, + { + "endpoints": { + "1": { + "device_type": 2096, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 9, 2821, 4096], + "out_clusters": [3, 4, 5, 6, 8, 25, 4096], + "profile_id": 49246, + } + }, + "entities": ["sensor.ikea_of_sweden_tradfri_remote_control_77665544_power"], + "event_channels": [6, 8], + "manufacturer": "IKEA of Sweden", + "model": "TRADFRI remote control", + }, + { + "endpoints": { + "1": { + "device_type": 8, + "endpoint_id": 1, + "in_clusters": [0, 3, 9, 2821, 4096, 64636], + "out_clusters": [25, 32, 4096], + "profile_id": 260, + }, + "242": { + "device_type": 97, + "endpoint_id": 242, + "in_clusters": [33], + "out_clusters": [33], + "profile_id": 41440, + }, + }, + "entities": [], + "event_channels": [], + "manufacturer": "IKEA of Sweden", + "model": "TRADFRI signal repeater", + }, + { + "endpoints": { + "1": { + "device_type": 2064, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 9, 2821, 4096], + "out_clusters": [3, 4, 6, 8, 25, 4096], + "profile_id": 260, + } + }, + "entities": ["sensor.ikea_of_sweden_tradfri_wireless_dimmer_77665544_power"], + "event_channels": [6, 8], + "manufacturer": "IKEA of Sweden", + "model": "TRADFRI wireless dimmer", + }, + { + "endpoints": { + "1": { + "device_type": 257, + "endpoint_id": 1, + "in_clusters": [0, 3, 4, 5, 6, 8, 1794, 2821], + "out_clusters": [10, 25], + "profile_id": 260, + }, + "2": { + "device_type": 260, + "endpoint_id": 2, + "in_clusters": [0, 3, 2821], + "out_clusters": [3, 6, 8], + "profile_id": 260, + }, + }, + "entities": [ + "sensor.jasco_products_45852_77665544_smartenergy_metering", + "light.jasco_products_45852_77665544_level_on_off", + ], + "event_channels": [6, 8], + "manufacturer": "Jasco Products", + "model": "45852", + }, + { + "endpoints": { + "1": { + "device_type": 256, + "endpoint_id": 1, + "in_clusters": [0, 3, 4, 5, 6, 1794, 2821], + "out_clusters": [10, 25], + "profile_id": 260, + }, + "2": { + "device_type": 259, + "endpoint_id": 2, + "in_clusters": [0, 3, 2821], + "out_clusters": [3, 6], + "profile_id": 260, + }, + }, + "entities": [ + "sensor.jasco_products_45856_77665544_smartenergy_metering", + "switch.jasco_products_45856_77665544_on_off", + "light.jasco_products_45856_77665544_on_off", + ], + "event_channels": [6], + "manufacturer": "Jasco Products", + "model": "45856", + }, + { + "endpoints": { + "1": { + "device_type": 257, + "endpoint_id": 1, + "in_clusters": [0, 3, 4, 5, 6, 8, 1794, 2821], + "out_clusters": [10, 25], + "profile_id": 260, + }, + "2": { + "device_type": 260, + "endpoint_id": 2, + "in_clusters": [0, 3, 2821], + "out_clusters": [3, 6, 8], + "profile_id": 260, + }, + }, + "entities": [ + "sensor.jasco_products_45857_77665544_smartenergy_metering", + "light.jasco_products_45857_77665544_level_on_off", + ], + "event_channels": [6, 8], + "manufacturer": "Jasco Products", + "model": "45857", + }, + { + "endpoints": { + "1": { + "device_type": 3, + "endpoint_id": 1, + "in_clusters": [ + 0, + 1, + 3, + 4, + 5, + 6, + 8, + 32, + 1026, + 1027, + 2821, + 64513, + 64514, + ], + "out_clusters": [25], + "profile_id": 260, + } + }, + "entities": [ + "binary_sensor.keen_home_inc_sv02_610_mp_1_3_77665544_manufacturer_specific", + "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_pressure", + "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_temperature", + "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_power", + "light.keen_home_inc_sv02_610_mp_1_3_77665544_level_on_off", + ], + "event_channels": [], + "manufacturer": "Keen Home Inc", + "model": "SV02-610-MP-1.3", + }, + { + "endpoints": { + "1": { + "device_type": 3, + "endpoint_id": 1, + "in_clusters": [ + 0, + 1, + 3, + 4, + 5, + 6, + 8, + 32, + 1026, + 1027, + 2821, + 64513, + 64514, + ], + "out_clusters": [25], + "profile_id": 260, + } + }, + "entities": [ + "binary_sensor.keen_home_inc_sv02_612_mp_1_2_77665544_manufacturer_specific", + "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_temperature", + "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_power", + "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_pressure", + "light.keen_home_inc_sv02_612_mp_1_2_77665544_level_on_off", + ], + "event_channels": [], + "manufacturer": "Keen Home Inc", + "model": "SV02-612-MP-1.2", + }, + { + "endpoints": { + "1": { + "device_type": 3, + "endpoint_id": 1, + "in_clusters": [ + 0, + 1, + 3, + 4, + 5, + 6, + 8, + 32, + 1026, + 1027, + 2821, + 64513, + 64514, + ], + "out_clusters": [25], + "profile_id": 260, + } + }, + "entities": [ + "binary_sensor.keen_home_inc_sv02_612_mp_1_3_77665544_manufacturer_specific", + "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_pressure", + "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_power", + "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_temperature", + "light.keen_home_inc_sv02_612_mp_1_3_77665544_level_on_off", + ], + "event_channels": [], + "manufacturer": "Keen Home Inc", + "model": "SV02-612-MP-1.3", + }, + { + "endpoints": { + "1": { + "device_type": 14, + "endpoint_id": 1, + "in_clusters": [0, 3, 4, 5, 6, 8, 514], + "out_clusters": [3, 25], + "profile_id": 260, + } + }, + "entities": [ + "fan.king_of_fans_inc_hbuniversalcfremote_77665544_fan", + "switch.king_of_fans_inc_hbuniversalcfremote_77665544_on_off", + ], + "event_channels": [], + "manufacturer": "King Of Fans, Inc.", + "model": "HBUniversalCFRemote", + }, + { + "endpoints": { + "1": { + "device_type": 258, + "endpoint_id": 1, + "in_clusters": [0, 3, 4, 5, 6, 8, 768, 2821, 64513], + "out_clusters": [25], + "profile_id": 260, + } + }, + "entities": ["light.ledvance_a19_rgbw_77665544_level_light_color_on_off"], + "event_channels": [], + "manufacturer": "LEDVANCE", + "model": "A19 RGBW", + }, + { + "endpoints": { + "1": { + "device_type": 258, + "endpoint_id": 1, + "in_clusters": [0, 3, 4, 5, 6, 8, 768, 2821, 64513], + "out_clusters": [25], + "profile_id": 260, + } + }, + "entities": ["light.ledvance_flex_rgbw_77665544_level_light_color_on_off"], + "event_channels": [], + "manufacturer": "LEDVANCE", + "model": "FLEX RGBW", + }, + { + "endpoints": { + "1": { + "device_type": 81, + "endpoint_id": 1, + "in_clusters": [0, 3, 4, 5, 6, 2821, 64513, 64520], + "out_clusters": [3, 25], + "profile_id": 260, + } + }, + "entities": ["switch.ledvance_plug_77665544_on_off"], + "event_channels": [], + "manufacturer": "LEDVANCE", + "model": "PLUG", + }, + { + "endpoints": { + "1": { + "device_type": 258, + "endpoint_id": 1, + "in_clusters": [0, 3, 4, 5, 6, 8, 768, 2821, 64513], + "out_clusters": [25], + "profile_id": 260, + } + }, + "entities": ["light.ledvance_rt_rgbw_77665544_level_light_color_on_off"], + "event_channels": [], + "manufacturer": "LEDVANCE", + "model": "RT RGBW", + }, + { + "endpoints": { + "1": { + "device_type": 81, + "endpoint_id": 1, + "in_clusters": [0, 1, 2, 3, 4, 5, 6, 10, 16, 2820], + "out_clusters": [10, 25], + "profile_id": 260, + }, + "100": { + "device_type": 263, + "endpoint_id": 100, + "in_clusters": [15], + "out_clusters": [4, 15], + "profile_id": 260, + }, + "2": { + "device_type": 9, + "endpoint_id": 2, + "in_clusters": [12], + "out_clusters": [4, 12], + "profile_id": 260, + }, + "3": { + "device_type": 83, + "endpoint_id": 3, + "in_clusters": [12], + "out_clusters": [12], + "profile_id": 260, + }, + }, + "entities": [ + "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement", + "sensor.lumi_lumi_plug_maus01_77665544_analog_input", + "sensor.lumi_lumi_plug_maus01_77665544_analog_input_2", + "sensor.lumi_lumi_plug_maus01_77665544_power", + "switch.lumi_lumi_plug_maus01_77665544_on_off", + ], + "event_channels": [], + "manufacturer": "LUMI", + "model": "lumi.plug.maus01", + }, + { + "endpoints": { + "1": { + "device_type": 257, + "endpoint_id": 1, + "in_clusters": [0, 1, 2, 3, 4, 5, 6, 10, 12, 16, 2820], + "out_clusters": [10, 25], + "profile_id": 260, + }, + "2": { + "device_type": 257, + "endpoint_id": 2, + "in_clusters": [4, 5, 6, 16], + "out_clusters": [], + "profile_id": 260, + }, + }, + "entities": [ + "sensor.lumi_lumi_relay_c2acn01_77665544_analog_input", + "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement", + "sensor.lumi_lumi_relay_c2acn01_77665544_power", + "light.lumi_lumi_relay_c2acn01_77665544_on_off", + "light.lumi_lumi_relay_c2acn01_77665544_on_off_2", + ], + "event_channels": [], + "manufacturer": "LUMI", + "model": "lumi.relay.c2acn01", + }, + { + "endpoints": { + "1": { + "device_type": 24321, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 18, 25, 65535], + "out_clusters": [0, 3, 4, 5, 18, 25, 65535], + "profile_id": 260, + }, + "2": { + "device_type": 24322, + "endpoint_id": 2, + "in_clusters": [3, 18], + "out_clusters": [3, 4, 5, 18], + "profile_id": 260, + }, + "3": { + "device_type": 24323, + "endpoint_id": 3, + "in_clusters": [3, 18], + "out_clusters": [3, 4, 5, 12, 18], + "profile_id": 260, + }, + }, + "entities": [ + "sensor.lumi_lumi_remote_b186acn01_77665544_multistate_input", + "sensor.lumi_lumi_remote_b186acn01_77665544_power", + "sensor.lumi_lumi_remote_b186acn01_77665544_multistate_input_2", + "sensor.lumi_lumi_remote_b186acn01_77665544_multistate_input_3", + ], + "event_channels": [], + "manufacturer": "LUMI", + "model": "lumi.remote.b186acn01", + }, + { + "endpoints": { + "1": { + "device_type": 24321, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 18, 25, 65535], + "out_clusters": [0, 3, 4, 5, 18, 25, 65535], + "profile_id": 260, + }, + "2": { + "device_type": 24322, + "endpoint_id": 2, + "in_clusters": [3, 18], + "out_clusters": [3, 4, 5, 18], + "profile_id": 260, + }, + "3": { + "device_type": 24323, + "endpoint_id": 3, + "in_clusters": [3, 18], + "out_clusters": [3, 4, 5, 12, 18], + "profile_id": 260, + }, + }, + "entities": [ + "sensor.lumi_lumi_remote_b286acn01_77665544_multistate_input", + "sensor.lumi_lumi_remote_b286acn01_77665544_power", + "sensor.lumi_lumi_remote_b286acn01_77665544_multistate_input_2", + "sensor.lumi_lumi_remote_b286acn01_77665544_multistate_input_3", + ], + "event_channels": [], + "manufacturer": "LUMI", + "model": "lumi.remote.b286acn01", + }, + { + "endpoints": { + "1": { + "device_type": 261, + "endpoint_id": 1, + "in_clusters": [0, 1, 3], + "out_clusters": [3, 6, 8, 768], + "profile_id": 260, + }, + "2": { + "device_type": -1, + "endpoint_id": 2, + "in_clusters": [], + "out_clusters": [], + "profile_id": -1, + }, + "3": { + "device_type": -1, + "endpoint_id": 3, + "in_clusters": [], + "out_clusters": [], + "profile_id": -1, + }, + "4": { + "device_type": -1, + "endpoint_id": 4, + "in_clusters": [], + "out_clusters": [], + "profile_id": -1, + }, + "5": { + "device_type": -1, + "endpoint_id": 5, + "in_clusters": [], + "out_clusters": [], + "profile_id": -1, + }, + "6": { + "device_type": -1, + "endpoint_id": 6, + "in_clusters": [], + "out_clusters": [], + "profile_id": -1, + }, + }, + "entities": ["sensor.lumi_lumi_remote_b286opcn01_77665544_power"], + "event_channels": [6, 8, 768], + "manufacturer": "LUMI", + "model": "lumi.remote.b286opcn01", + }, + { + "endpoints": { + "1": { + "device_type": 261, + "endpoint_id": 1, + "in_clusters": [0, 1, 3], + "out_clusters": [3, 6, 8, 768], + "profile_id": 260, + }, + "2": { + "device_type": 259, + "endpoint_id": 2, + "in_clusters": [3], + "out_clusters": [3, 6], + "profile_id": 260, + }, + "3": { + "device_type": -1, + "endpoint_id": 3, + "in_clusters": [], + "out_clusters": [], + "profile_id": -1, + }, + "4": { + "device_type": -1, + "endpoint_id": 4, + "in_clusters": [], + "out_clusters": [], + "profile_id": -1, + }, + "5": { + "device_type": -1, + "endpoint_id": 5, + "in_clusters": [], + "out_clusters": [], + "profile_id": -1, + }, + "6": { + "device_type": -1, + "endpoint_id": 6, + "in_clusters": [], + "out_clusters": [], + "profile_id": -1, + }, + }, + "entities": [ + "sensor.lumi_lumi_remote_b486opcn01_77665544_power", + "switch.lumi_lumi_remote_b486opcn01_77665544_on_off", + ], + "event_channels": [6, 8, 768, 6], + "manufacturer": "LUMI", + "model": "lumi.remote.b486opcn01", + }, + { + "endpoints": { + "1": { + "device_type": 261, + "endpoint_id": 1, + "in_clusters": [0, 1, 3], + "out_clusters": [3, 6, 8, 768], + "profile_id": 260, + }, + "2": { + "device_type": 259, + "endpoint_id": 2, + "in_clusters": [3], + "out_clusters": [3, 6], + "profile_id": 260, + }, + "3": { + "device_type": None, + "endpoint_id": 3, + "in_clusters": [], + "out_clusters": [], + "profile_id": None, + }, + "4": { + "device_type": None, + "endpoint_id": 4, + "in_clusters": [], + "out_clusters": [], + "profile_id": None, + }, + "5": { + "device_type": None, + "endpoint_id": 5, + "in_clusters": [], + "out_clusters": [], + "profile_id": None, + }, + "6": { + "device_type": None, + "endpoint_id": 6, + "in_clusters": [], + "out_clusters": [], + "profile_id": None, + }, + }, + "entities": [ + "sensor.lumi_lumi_remote_b686opcn01_77665544_power", + "switch.lumi_lumi_remote_b686opcn01_77665544_on_off", + ], + "event_channels": [6, 8, 768, 6], + "manufacturer": "LUMI", + "model": "lumi.remote.b686opcn01", + }, + { + "endpoints": { + "8": { + "device_type": 256, + "endpoint_id": 8, + "in_clusters": [0, 6, 11, 17], + "out_clusters": [0, 6], + "profile_id": 260, + } + }, + "entities": ["light.lumi_lumi_router_77665544_on_off_on_off"], + "event_channels": [6], + "manufacturer": "LUMI", + "model": "lumi.router", + }, + { + "endpoints": { + "1": { + "device_type": 28417, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 25], + "out_clusters": [0, 3, 4, 5, 18, 25], + "profile_id": 260, + }, + "2": { + "device_type": 28418, + "endpoint_id": 2, + "in_clusters": [3, 18], + "out_clusters": [3, 4, 5, 18], + "profile_id": 260, + }, + "3": { + "device_type": 28419, + "endpoint_id": 3, + "in_clusters": [3, 12], + "out_clusters": [3, 4, 5, 12], + "profile_id": 260, + }, + }, + "entities": [ + "sensor.lumi_lumi_sensor_cube_aqgl01_77665544_analog_input", + "sensor.lumi_lumi_sensor_cube_aqgl01_77665544_multistate_input", + "sensor.lumi_lumi_sensor_cube_aqgl01_77665544_power", + ], + "event_channels": [], + "manufacturer": "LUMI", + "model": "lumi.sensor_cube.aqgl01", + }, + { + "endpoints": { + "1": { + "device_type": 24322, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 25, 1026, 1029, 65535], + "out_clusters": [0, 3, 4, 5, 18, 25, 65535], + "profile_id": 260, + }, + "2": { + "device_type": 24322, + "endpoint_id": 2, + "in_clusters": [3], + "out_clusters": [3, 4, 5, 18], + "profile_id": 260, + }, + "3": { + "device_type": 24323, + "endpoint_id": 3, + "in_clusters": [3], + "out_clusters": [3, 4, 5, 12], + "profile_id": 260, + }, + }, + "entities": [ + "sensor.lumi_lumi_sensor_ht_77665544_power", + "sensor.lumi_lumi_sensor_ht_77665544_temperature", + "sensor.lumi_lumi_sensor_ht_77665544_humidity", + ], + "event_channels": [], + "manufacturer": "LUMI", + "model": "lumi.sensor_ht", + }, + { + "endpoints": { + "1": { + "device_type": 2128, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 25, 65535], + "out_clusters": [0, 3, 4, 5, 6, 8, 25], + "profile_id": 260, + } + }, + "entities": [ + "sensor.lumi_lumi_sensor_magnet_77665544_power", + "binary_sensor.lumi_lumi_sensor_magnet_77665544_on_off", + ], + "event_channels": [6, 8], + "manufacturer": "LUMI", + "model": "lumi.sensor_magnet", + }, + { + "endpoints": { + "1": { + "device_type": 24321, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 65535], + "out_clusters": [0, 4, 6, 65535], + "profile_id": 260, + } + }, + "entities": [ + "binary_sensor.lumi_lumi_sensor_magnet_aq2_77665544_on_off", + "sensor.lumi_lumi_sensor_magnet_aq2_77665544_power", + ], + "event_channels": [6], + "manufacturer": "LUMI", + "model": "lumi.sensor_magnet.aq2", + }, + { + "endpoints": { + "1": { + "device_type": 263, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 1024, 1030, 1280, 65535], + "out_clusters": [0, 25], + "profile_id": 260, + } + }, + "entities": [ + "binary_sensor.lumi_lumi_sensor_motion_aq2_77665544_occupancy", + "binary_sensor.lumi_lumi_sensor_motion_aq2_77665544_ias_zone", + "sensor.lumi_lumi_sensor_motion_aq2_77665544_illuminance", + "sensor.lumi_lumi_sensor_motion_aq2_77665544_power", + ], + "event_channels": [], + "manufacturer": "LUMI", + "model": "lumi.sensor_motion.aq2", + }, + { + "endpoints": { + "1": { + "device_type": 6, + "endpoint_id": 1, + "in_clusters": [0, 1, 3], + "out_clusters": [0, 4, 5, 6, 8, 25], + "profile_id": 260, + } + }, + "entities": ["sensor.lumi_lumi_sensor_switch_77665544_power"], + "event_channels": [6, 8], + "manufacturer": "LUMI", + "model": "lumi.sensor_switch", + }, + { + "endpoints": { + "1": { + "device_type": 6, + "endpoint_id": 1, + "in_clusters": [0, 1, 65535], + "out_clusters": [0, 4, 6, 65535], + "profile_id": 260, + } + }, + "entities": ["sensor.lumi_lumi_sensor_switch_aq2_77665544_power"], + "event_channels": [6], + "manufacturer": "LUMI", + "model": "lumi.sensor_switch.aq2", + }, + { + "endpoints": { + "1": { + "device_type": 6, + "endpoint_id": 1, + "in_clusters": [0, 1, 18], + "out_clusters": [0, 6], + "profile_id": 260, + } + }, + "entities": [ + "sensor.lumi_lumi_sensor_switch_aq3_77665544_multistate_input", + "sensor.lumi_lumi_sensor_switch_aq3_77665544_power", + ], + "event_channels": [6], + "manufacturer": "LUMI", + "model": "lumi.sensor_switch.aq3", + }, + { + "endpoints": { + "1": { + "device_type": 1026, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 1280], + "out_clusters": [25], + "profile_id": 260, + } + }, + "entities": [ + "binary_sensor.lumi_lumi_sensor_wleak_aq1_77665544_ias_zone", + "sensor.lumi_lumi_sensor_wleak_aq1_77665544_power", + ], + "event_channels": [], + "manufacturer": "LUMI", + "model": "lumi.sensor_wleak.aq1", + }, + { + "endpoints": { + "1": { + "device_type": 10, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 25, 257, 1280], + "out_clusters": [0, 3, 4, 5, 25], + "profile_id": 260, + }, + "2": { + "device_type": 24322, + "endpoint_id": 2, + "in_clusters": [3], + "out_clusters": [3, 4, 5, 18], + "profile_id": 260, + }, + }, + "entities": [ + "binary_sensor.lumi_lumi_vibration_aq1_77665544_ias_zone", + "sensor.lumi_lumi_vibration_aq1_77665544_power", + "lock.lumi_lumi_vibration_aq1_77665544_door_lock", + ], + "event_channels": [], + "manufacturer": "LUMI", + "model": "lumi.vibration.aq1", + }, + { + "endpoints": { + "1": { + "device_type": 24321, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 1026, 1027, 1029, 65535], + "out_clusters": [0, 4, 65535], + "profile_id": 260, + } + }, + "entities": [ + "sensor.lumi_lumi_weather_77665544_temperature", + "sensor.lumi_lumi_weather_77665544_power", + "sensor.lumi_lumi_weather_77665544_humidity", + "sensor.lumi_lumi_weather_77665544_pressure", + ], + "event_channels": [], + "manufacturer": "LUMI", + "model": "lumi.weather", + }, + { + "endpoints": { + "1": { + "device_type": 1026, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 32, 1280], + "out_clusters": [], + "profile_id": 260, + } + }, + "entities": [ + "binary_sensor.nyce_3010_77665544_ias_zone", + "sensor.nyce_3010_77665544_power", + ], + "event_channels": [], + "manufacturer": "NYCE", + "model": "3010", + }, + { + "endpoints": { + "1": { + "device_type": 1026, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 32, 1280], + "out_clusters": [], + "profile_id": 260, + } + }, + "entities": [ + "binary_sensor.nyce_3014_77665544_ias_zone", + "sensor.nyce_3014_77665544_power", + ], + "event_channels": [], + "manufacturer": "NYCE", + "model": "3014", + }, + { + "endpoints": { + "3": { + "device_type": 258, + "endpoint_id": 3, + "in_clusters": [0, 3, 4, 5, 6, 8, 768, 64527], + "out_clusters": [25], + "profile_id": 260, + } + }, + "entities": ["light.osram_lightify_a19_rgbw_77665544_level_light_color_on_off"], + "event_channels": [], + "manufacturer": "OSRAM", + "model": "LIGHTIFY A19 RGBW", + }, + { + "endpoints": { + "1": { + "device_type": 1, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 32, 2821], + "out_clusters": [3, 6, 8, 25], + "profile_id": 260, + } + }, + "entities": [ + "binary_sensor.osram_lightify_dimming_switch_77665544_on_off", + "sensor.osram_lightify_dimming_switch_77665544_power", + ], + "event_channels": [6, 8], + "manufacturer": "OSRAM", + "model": "LIGHTIFY Dimming Switch", + }, + { + "endpoints": { + "3": { + "device_type": 258, + "endpoint_id": 3, + "in_clusters": [0, 3, 4, 5, 6, 8, 768, 64527], + "out_clusters": [25], + "profile_id": 260, + } + }, + "entities": [ + "light.osram_lightify_flex_rgbw_77665544_level_light_color_on_off" + ], + "event_channels": [], + "manufacturer": "OSRAM", + "model": "LIGHTIFY Flex RGBW", + }, + { + "endpoints": { + "3": { + "device_type": 258, + "endpoint_id": 3, + "in_clusters": [0, 3, 4, 5, 6, 8, 768, 2820, 64527], + "out_clusters": [25], + "profile_id": 260, + } + }, + "entities": [ + "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement", + "light.osram_lightify_rt_tunable_white_77665544_level_light_color_on_off", + ], + "event_channels": [], + "manufacturer": "OSRAM", + "model": "LIGHTIFY RT Tunable White", + }, + { + "endpoints": { + "3": { + "device_type": 16, + "endpoint_id": 3, + "in_clusters": [0, 3, 4, 5, 6, 2820, 4096, 64527], + "out_clusters": [25], + "profile_id": 49246, + } + }, + "entities": [ + "sensor.osram_plug_01_77665544_electrical_measurement", + "switch.osram_plug_01_77665544_on_off", + ], + "event_channels": [], + "manufacturer": "OSRAM", + "model": "Plug 01", + }, + { + "endpoints": { + "1": { + "device_type": 2064, + "endpoint_id": 1, + "in_clusters": [0, 1, 32, 4096, 64768], + "out_clusters": [3, 4, 5, 6, 8, 25, 768, 4096], + "profile_id": 260, + }, + "2": { + "device_type": 2064, + "endpoint_id": 2, + "in_clusters": [0, 4096, 64768], + "out_clusters": [3, 4, 5, 6, 8, 768, 4096], + "profile_id": 260, + }, + "3": { + "device_type": 2064, + "endpoint_id": 3, + "in_clusters": [0, 4096, 64768], + "out_clusters": [3, 4, 5, 6, 8, 768, 4096], + "profile_id": 260, + }, + "4": { + "device_type": 2064, + "endpoint_id": 4, + "in_clusters": [0, 4096, 64768], + "out_clusters": [3, 4, 5, 6, 8, 768, 4096], + "profile_id": 260, + }, + "5": { + "device_type": 2064, + "endpoint_id": 5, + "in_clusters": [0, 4096, 64768], + "out_clusters": [3, 4, 5, 6, 8, 768, 4096], + "profile_id": 260, + }, + "6": { + "device_type": 2064, + "endpoint_id": 6, + "in_clusters": [0, 4096, 64768], + "out_clusters": [3, 4, 5, 6, 8, 768, 4096], + "profile_id": 260, + }, + }, + "entities": ["sensor.osram_switch_4x_lightify_77665544_power"], + "event_channels": [ + 6, + 8, + 768, + 6, + 8, + 768, + 6, + 8, + 768, + 6, + 8, + 768, + 6, + 8, + 768, + 6, + 8, + 768, + ], + "manufacturer": "OSRAM", + "model": "Switch 4x-LIGHTIFY", + }, + { + "endpoints": { + "1": { + "device_type": 2096, + "endpoint_id": 1, + "in_clusters": [0], + "out_clusters": [0, 3, 4, 5, 6, 8], + "profile_id": 49246, + }, + "2": { + "device_type": 12, + "endpoint_id": 2, + "in_clusters": [0, 1, 3, 15, 64512], + "out_clusters": [25], + "profile_id": 260, + }, + }, + "entities": ["sensor.philips_rwl020_77665544_power"], + "event_channels": [6, 8], + "manufacturer": "Philips", + "model": "RWL020", + }, + { + "endpoints": { + "1": { + "device_type": 1026, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 32, 1026, 1280, 2821], + "out_clusters": [3, 25], + "profile_id": 260, + } + }, + "entities": [ + "binary_sensor.samjin_button_77665544_ias_zone", + "sensor.samjin_button_77665544_temperature", + "sensor.samjin_button_77665544_power", + ], + "event_channels": [], + "manufacturer": "Samjin", + "model": "button", + }, + { + "endpoints": { + "1": { + "device_type": 1026, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 32, 1026, 1280, 64514], + "out_clusters": [3, 25], + "profile_id": 260, + } + }, + "entities": [ + "sensor.samjin_multi_77665544_power", + "sensor.samjin_multi_77665544_temperature", + "binary_sensor.samjin_multi_77665544_ias_zone", + "binary_sensor.samjin_multi_77665544_manufacturer_specific", + ], + "event_channels": [], + "manufacturer": "Samjin", + "model": "multi", + }, + { + "endpoints": { + "1": { + "device_type": 1026, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 32, 1026, 1280], + "out_clusters": [3, 25], + "profile_id": 260, + } + }, + "entities": [ + "binary_sensor.samjin_water_77665544_ias_zone", + "sensor.samjin_water_77665544_power", + "sensor.samjin_water_77665544_temperature", + ], + "event_channels": [], + "manufacturer": "Samjin", + "model": "water", + }, + { + "endpoints": { + "1": { + "device_type": 0, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 4, 5, 6, 2820, 2821], + "out_clusters": [0, 1, 3, 4, 5, 6, 25, 2820, 2821], + "profile_id": 260, + } + }, + "entities": [ + "binary_sensor.securifi_ltd_unk_model_77665544_on_off", + "sensor.securifi_ltd_unk_model_77665544_electrical_measurement", + "sensor.securifi_ltd_unk_model_77665544_power", + "switch.securifi_ltd_unk_model_77665544_on_off", + ], + "event_channels": [6], + "manufacturer": "Securifi Ltd.", + "model": None, + }, + { + "endpoints": { + "1": { + "device_type": 1026, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 32, 1026, 1280, 2821], + "out_clusters": [3, 25], + "profile_id": 260, + } + }, + "entities": [ + "binary_sensor.sercomm_corp_sz_dws04n_sf_77665544_ias_zone", + "sensor.sercomm_corp_sz_dws04n_sf_77665544_power", + "sensor.sercomm_corp_sz_dws04n_sf_77665544_temperature", + ], + "event_channels": [], + "manufacturer": "Sercomm Corp.", + "model": "SZ-DWS04N_SF", + }, + { + "endpoints": { + "1": { + "device_type": 256, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 4, 5, 6, 1794, 2820, 2821], + "out_clusters": [3, 10, 25, 2821], + "profile_id": 260, + }, + "2": { + "device_type": 259, + "endpoint_id": 2, + "in_clusters": [0, 1, 3], + "out_clusters": [3, 6], + "profile_id": 260, + }, + }, + "entities": [ + "sensor.sercomm_corp_sz_esw01_77665544_smartenergy_metering", + "sensor.sercomm_corp_sz_esw01_77665544_power", + "sensor.sercomm_corp_sz_esw01_77665544_power_2", + "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement", + "switch.sercomm_corp_sz_esw01_77665544_on_off", + "light.sercomm_corp_sz_esw01_77665544_on_off", + ], + "event_channels": [6], + "manufacturer": "Sercomm Corp.", + "model": "SZ-ESW01", + }, + { + "endpoints": { + "1": { + "device_type": 1026, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 32, 1024, 1026, 1280, 2821], + "out_clusters": [25], + "profile_id": 260, + } + }, + "entities": [ + "binary_sensor.sercomm_corp_sz_pir04_77665544_ias_zone", + "sensor.sercomm_corp_sz_pir04_77665544_temperature", + "sensor.sercomm_corp_sz_pir04_77665544_illuminance", + "sensor.sercomm_corp_sz_pir04_77665544_power", + ], + "event_channels": [], + "manufacturer": "Sercomm Corp.", + "model": "SZ-PIR04", + }, + { + "endpoints": { + "1": { + "device_type": 2, + "endpoint_id": 1, + "in_clusters": [0, 3, 4, 5, 6, 2820, 2821, 65281], + "out_clusters": [3, 4, 25], + "profile_id": 260, + } + }, + "entities": [ + "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement", + "switch.sinope_technologies_rm3250zb_77665544_on_off", + ], + "event_channels": [], + "manufacturer": "Sinope Technologies", + "model": "RM3250ZB", + }, + { + "endpoints": { + "1": { + "device_type": 769, + "endpoint_id": 1, + "in_clusters": [0, 3, 4, 5, 513, 516, 1026, 2820, 2821, 65281], + "out_clusters": [25, 65281], + "profile_id": 260, + }, + "196": { + "device_type": 769, + "endpoint_id": 196, + "in_clusters": [1], + "out_clusters": [], + "profile_id": 49757, + }, + }, + "entities": [ + "sensor.sinope_technologies_th1124zb_77665544_temperature", + "sensor.sinope_technologies_th1124zb_77665544_power", + "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement", + ], + "event_channels": [], + "manufacturer": "Sinope Technologies", + "model": "TH1124ZB", + }, + { + "endpoints": { + "1": { + "device_type": 2, + "endpoint_id": 1, + "in_clusters": [0, 3, 4, 5, 6, 9, 15, 2820], + "out_clusters": [25], + "profile_id": 260, + } + }, + "entities": [ + "sensor.smartthings_outletv4_77665544_electrical_measurement", + "switch.smartthings_outletv4_77665544_on_off", + ], + "event_channels": [], + "manufacturer": "SmartThings", + "model": "outletv4", + }, + { + "endpoints": { + "1": { + "device_type": 32768, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 15, 32], + "out_clusters": [3, 25], + "profile_id": 260, + } + }, + "entities": ["device_tracker.smartthings_tagv4_77665544_power"], + "event_channels": [], + "manufacturer": "SmartThings", + "model": "tagv4", + }, + { + "endpoints": { + "1": { + "device_type": 2, + "endpoint_id": 1, + "in_clusters": [0, 3, 4, 5, 6, 25], + "out_clusters": [], + "profile_id": 260, + } + }, + "entities": ["switch.third_reality_inc_3rss007z_77665544_on_off"], + "event_channels": [], + "manufacturer": "Third Reality, Inc", + "model": "3RSS007Z", + }, + { + "endpoints": { + "1": { + "device_type": 2, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 4, 5, 6, 25], + "out_clusters": [1], + "profile_id": 260, + } + }, + "entities": [ + "sensor.third_reality_inc_3rss008z_77665544_power", + "switch.third_reality_inc_3rss008z_77665544_on_off", + ], + "event_channels": [], + "manufacturer": "Third Reality, Inc", + "model": "3RSS008Z", + }, + { + "endpoints": { + "1": { + "device_type": 1026, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 32, 1026, 1280, 2821], + "out_clusters": [25], + "profile_id": 260, + } + }, + "entities": [ + "binary_sensor.visonic_mct_340_e_77665544_ias_zone", + "sensor.visonic_mct_340_e_77665544_temperature", + "sensor.visonic_mct_340_e_77665544_power", + ], + "event_channels": [], + "manufacturer": "Visonic", + "model": "MCT-340 E", + }, + { + "endpoints": { + "1": { + "device_type": 1026, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 21, 32, 1280, 2821], + "out_clusters": [], + "profile_id": 260, + } + }, + "entities": [ + "binary_sensor.netvox_z308e3ed_77665544_ias_zone", + "sensor.netvox_z308e3ed_77665544_power", + ], + "event_channels": [], + "manufacturer": "netvox", + "model": "Z308E3ED", + }, + { + "endpoints": { + "1": { + "device_type": 257, + "endpoint_id": 1, + "in_clusters": [0, 3, 4, 5, 6, 8, 1794, 2821], + "out_clusters": [25], + "profile_id": 260, + } + }, + "entities": [ + "light.sengled_e11_g13_77665544_level_on_off", + "sensor.sengled_e11_g13_77665544_smartenergy_metering", + ], + "event_channels": [], + "manufacturer": "sengled", + "model": "E11-G13", + }, + { + "endpoints": { + "1": { + "device_type": 257, + "endpoint_id": 1, + "in_clusters": [0, 3, 4, 5, 6, 8, 1794, 2821], + "out_clusters": [25], + "profile_id": 260, + } + }, + "entities": [ + "sensor.sengled_e12_n14_77665544_smartenergy_metering", + "light.sengled_e12_n14_77665544_level_on_off", + ], + "event_channels": [], + "manufacturer": "sengled", + "model": "E12-N14", + }, + { + "endpoints": { + "1": { + "device_type": 257, + "endpoint_id": 1, + "in_clusters": [0, 3, 4, 5, 6, 8, 768, 1794, 2821], + "out_clusters": [25], + "profile_id": 260, + } + }, + "entities": [ + "sensor.sengled_z01_a19nae26_77665544_smartenergy_metering", + "light.sengled_z01_a19nae26_77665544_level_light_color_on_off", + ], + "event_channels": [], + "manufacturer": "sengled", + "model": "Z01-A19NAE26", + }, +] diff --git a/tests/components/zone/test_config_flow.py b/tests/components/zone/test_config_flow.py index 1e1bca9cdea..5f57e8b4064 100644 --- a/tests/components/zone/test_config_flow.py +++ b/tests/components/zone/test_config_flow.py @@ -3,10 +3,10 @@ from homeassistant.components.zone import config_flow from homeassistant.components.zone.const import CONF_PASSIVE, DOMAIN, HOME_ZONE from homeassistant.const import ( - CONF_NAME, + CONF_ICON, CONF_LATITUDE, CONF_LONGITUDE, - CONF_ICON, + CONF_NAME, CONF_RADIUS, ) diff --git a/tests/components/zone/test_init.py b/tests/components/zone/test_init.py index b5b975ae4bd..d4a76463c18 100644 --- a/tests/components/zone/test_init.py +++ b/tests/components/zone/test_init.py @@ -6,8 +6,7 @@ from unittest.mock import Mock from homeassistant import setup from homeassistant.components import zone -from tests.common import get_test_home_assistant -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, get_test_home_assistant async def test_setup_entry_successful(hass): diff --git a/tests/components/zwave/conftest.py b/tests/components/zwave/conftest.py index cc9dc5c72ba..f80c55f7767 100644 --- a/tests/components/zwave/conftest.py +++ b/tests/components/zwave/conftest.py @@ -1,5 +1,5 @@ """Fixtures for Z-Wave tests.""" -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch import pytest diff --git a/tests/components/zwave/test_binary_sensor.py b/tests/components/zwave/test_binary_sensor.py index b68f4208f7a..54270cdc3f4 100644 --- a/tests/components/zwave/test_binary_sensor.py +++ b/tests/components/zwave/test_binary_sensor.py @@ -1,11 +1,10 @@ """Test Z-Wave binary sensors.""" import datetime - from unittest.mock import patch -from homeassistant.components.zwave import const, binary_sensor +from homeassistant.components.zwave import binary_sensor, const -from tests.mock.zwave import MockNode, MockValue, MockEntityValues, value_changed +from tests.mock.zwave import MockEntityValues, MockNode, MockValue, value_changed def test_get_device_detects_none(mock_openzwave): diff --git a/tests/components/zwave/test_climate.py b/tests/components/zwave/test_climate.py index 2f13d95fb9f..631bf0a0ce8 100644 --- a/tests/components/zwave/test_climate.py +++ b/tests/components/zwave/test_climate.py @@ -2,23 +2,31 @@ import pytest from homeassistant.components.climate.const import ( - CURRENT_HVAC_HEAT, + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, CURRENT_HVAC_COOL, - HVAC_MODES, + CURRENT_HVAC_HEAT, HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, + HVAC_MODES, + PRESET_AWAY, PRESET_BOOST, PRESET_ECO, PRESET_NONE, + SUPPORT_AUX_HEAT, SUPPORT_FAN_MODE, SUPPORT_PRESET_MODE, SUPPORT_SWING_MODE, SUPPORT_TARGET_TEMPERATURE, + SUPPORT_TARGET_TEMPERATURE_RANGE, +) +from homeassistant.components.zwave import climate, const +from homeassistant.components.zwave.climate import ( + AUX_HEAT_ZWAVE_MODE, + DEFAULT_HVAC_MODES, ) -from homeassistant.components.zwave import climate -from homeassistant.components.zwave.climate import DEFAULT_HVAC_MODES from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from tests.mock.zwave import MockEntityValues, MockNode, MockValue, value_changed @@ -29,9 +37,8 @@ def device(hass, mock_openzwave): """Fixture to provide a precreated climate device.""" node = MockNode() values = MockEntityValues( - primary=MockValue(data=1, node=node), - temperature=MockValue(data=5, node=node, units=None), - mode=MockValue( + primary=MockValue( + command_class=const.COMMAND_CLASS_THERMOSTAT_MODE, data=HVAC_MODE_HEAT, data_items=[ HVAC_MODE_OFF, @@ -41,6 +48,9 @@ def device(hass, mock_openzwave): ], node=node, ), + setpoint_heating=MockValue(data=1, node=node), + setpoint_cooling=MockValue(data=10, node=node), + temperature=MockValue(data=5, node=node, units=None), fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), operating_state=MockValue(data=CURRENT_HVAC_HEAT, node=node), fan_action=MockValue(data=7, node=node), @@ -56,9 +66,8 @@ def device_zxt_120(hass, mock_openzwave): node = MockNode(manufacturer_id="5254", product_id="8377") values = MockEntityValues( - primary=MockValue(data=1, node=node), - temperature=MockValue(data=5, node=node, units=None), - mode=MockValue( + primary=MockValue( + command_class=const.COMMAND_CLASS_THERMOSTAT_MODE, data=HVAC_MODE_HEAT, data_items=[ HVAC_MODE_OFF, @@ -68,6 +77,9 @@ def device_zxt_120(hass, mock_openzwave): ], node=node, ), + setpoint_heating=MockValue(data=1, node=node), + setpoint_cooling=MockValue(data=10, node=node), + temperature=MockValue(data=5, node=node, units=None), fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), operating_state=MockValue(data=CURRENT_HVAC_HEAT, node=node), fan_action=MockValue(data=7, node=node), @@ -83,13 +95,15 @@ def device_mapping(hass, mock_openzwave): """Fixture to provide a precreated climate device. Test state mapping.""" node = MockNode() values = MockEntityValues( - primary=MockValue(data=1, node=node), - temperature=MockValue(data=5, node=node, units=None), - mode=MockValue( + primary=MockValue( + command_class=const.COMMAND_CLASS_THERMOSTAT_MODE, data="Heat", - data_items=["Off", "Cool", "Heat", "Full Power", "heat_cool"], + data_items=["Off", "Cool", "Heat", "Full Power", "Auto"], node=node, ), + setpoint_heating=MockValue(data=1, node=node), + setpoint_cooling=MockValue(data=10, node=node), + temperature=MockValue(data=5, node=node, units=None), fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), operating_state=MockValue(data="heating", node=node), fan_action=MockValue(data=7, node=node), @@ -104,13 +118,15 @@ def device_unknown(hass, mock_openzwave): """Fixture to provide a precreated climate device. Test state unknown.""" node = MockNode() values = MockEntityValues( - primary=MockValue(data=1, node=node), - temperature=MockValue(data=5, node=node, units=None), - mode=MockValue( + primary=MockValue( + command_class=const.COMMAND_CLASS_THERMOSTAT_MODE, data="Heat", data_items=["Off", "Cool", "Heat", "heat_cool", "Abcdefg"], node=node, ), + setpoint_heating=MockValue(data=1, node=node), + setpoint_cooling=MockValue(data=10, node=node), + temperature=MockValue(data=5, node=node, units=None), fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), operating_state=MockValue(data="test4", node=node), fan_action=MockValue(data=7, node=node), @@ -125,9 +141,8 @@ def device_heat_cool(hass, mock_openzwave): """Fixture to provide a precreated climate device. Test state heat only.""" node = MockNode() values = MockEntityValues( - primary=MockValue(data=1, node=node), - temperature=MockValue(data=5, node=node, units=None), - mode=MockValue( + primary=MockValue( + command_class=const.COMMAND_CLASS_THERMOSTAT_MODE, data=HVAC_MODE_HEAT, data_items=[ HVAC_MODE_OFF, @@ -138,6 +153,9 @@ def device_heat_cool(hass, mock_openzwave): ], node=node, ), + setpoint_heating=MockValue(data=1, node=node), + setpoint_cooling=MockValue(data=10, node=node), + temperature=MockValue(data=5, node=node, units=None), fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), operating_state=MockValue(data="test4", node=node), fan_action=MockValue(data=7, node=node), @@ -147,6 +165,182 @@ def device_heat_cool(hass, mock_openzwave): yield device +@pytest.fixture +def device_heat_cool_range(hass, mock_openzwave): + """Fixture to provide a precreated climate device. Target range mode.""" + node = MockNode() + values = MockEntityValues( + primary=MockValue( + command_class=const.COMMAND_CLASS_THERMOSTAT_MODE, + data=HVAC_MODE_HEAT_COOL, + data_items=[ + HVAC_MODE_OFF, + HVAC_MODE_HEAT, + HVAC_MODE_COOL, + HVAC_MODE_HEAT_COOL, + ], + node=node, + ), + setpoint_heating=MockValue(data=1, node=node), + setpoint_cooling=MockValue(data=10, node=node), + temperature=MockValue(data=5, node=node, units=None), + fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), + operating_state=MockValue(data="test4", node=node), + fan_action=MockValue(data=7, node=node), + ) + device = climate.get_device(hass, node=node, values=values, node_config={}) + + yield device + + +@pytest.fixture +def device_heat_cool_away(hass, mock_openzwave): + """Fixture to provide a precreated climate device. Target range mode.""" + node = MockNode() + values = MockEntityValues( + primary=MockValue( + command_class=const.COMMAND_CLASS_THERMOSTAT_MODE, + data=HVAC_MODE_HEAT_COOL, + data_items=[ + HVAC_MODE_OFF, + HVAC_MODE_HEAT, + HVAC_MODE_COOL, + HVAC_MODE_HEAT_COOL, + PRESET_AWAY, + ], + node=node, + ), + setpoint_heating=MockValue(data=2, node=node), + setpoint_cooling=MockValue(data=9, node=node), + setpoint_away_heating=MockValue(data=1, node=node), + setpoint_away_cooling=MockValue(data=10, node=node), + temperature=MockValue(data=5, node=node, units=None), + fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), + operating_state=MockValue(data="test4", node=node), + fan_action=MockValue(data=7, node=node), + ) + device = climate.get_device(hass, node=node, values=values, node_config={}) + + yield device + + +@pytest.fixture +def device_heat_eco(hass, mock_openzwave): + """Fixture to provide a precreated climate device. heat/heat eco.""" + node = MockNode() + values = MockEntityValues( + primary=MockValue( + command_class=const.COMMAND_CLASS_THERMOSTAT_MODE, + data=HVAC_MODE_HEAT, + data_items=[HVAC_MODE_OFF, HVAC_MODE_HEAT, "heat econ"], + node=node, + ), + setpoint_heating=MockValue(data=2, node=node), + setpoint_eco_heating=MockValue(data=1, node=node), + temperature=MockValue(data=5, node=node, units=None), + fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), + operating_state=MockValue(data="test4", node=node), + fan_action=MockValue(data=7, node=node), + ) + device = climate.get_device(hass, node=node, values=values, node_config={}) + + yield device + + +@pytest.fixture +def device_aux_heat(hass, mock_openzwave): + """Fixture to provide a precreated climate device. aux heat.""" + node = MockNode() + values = MockEntityValues( + primary=MockValue( + command_class=const.COMMAND_CLASS_THERMOSTAT_MODE, + data=HVAC_MODE_HEAT, + data_items=[HVAC_MODE_OFF, HVAC_MODE_HEAT, "Aux Heat"], + node=node, + ), + setpoint_heating=MockValue(data=2, node=node), + setpoint_eco_heating=MockValue(data=1, node=node), + temperature=MockValue(data=5, node=node, units=None), + fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), + operating_state=MockValue(data="test4", node=node), + fan_action=MockValue(data=7, node=node), + ) + device = climate.get_device(hass, node=node, values=values, node_config={}) + + yield device + + +@pytest.fixture +def device_single_setpoint(hass, mock_openzwave): + """Fixture to provide a precreated climate device. + + SETPOINT_THERMOSTAT device class. + """ + + node = MockNode() + values = MockEntityValues( + primary=MockValue( + command_class=const.COMMAND_CLASS_THERMOSTAT_SETPOINT, data=1, node=node + ), + mode=None, + temperature=MockValue(data=5, node=node, units=None), + fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), + operating_state=MockValue(data=CURRENT_HVAC_HEAT, node=node), + fan_action=MockValue(data=7, node=node), + ) + device = climate.get_device(hass, node=node, values=values, node_config={}) + + yield device + + +@pytest.fixture +def device_single_setpoint_with_mode(hass, mock_openzwave): + """Fixture to provide a precreated climate device. + + SETPOINT_THERMOSTAT device class with COMMAND_CLASS_THERMOSTAT_MODE command class + """ + + node = MockNode() + values = MockEntityValues( + primary=MockValue( + command_class=const.COMMAND_CLASS_THERMOSTAT_SETPOINT, data=1, node=node + ), + mode=MockValue( + command_class=const.COMMAND_CLASS_THERMOSTAT_MODE, + data=HVAC_MODE_HEAT, + data_items=[HVAC_MODE_OFF, HVAC_MODE_HEAT], + node=node, + ), + temperature=MockValue(data=5, node=node, units=None), + fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), + operating_state=MockValue(data=CURRENT_HVAC_HEAT, node=node), + fan_action=MockValue(data=7, node=node), + ) + device = climate.get_device(hass, node=node, values=values, node_config={}) + + yield device + + +def test_get_device_detects_none(hass, mock_openzwave): + """Test get_device returns None.""" + node = MockNode() + value = MockValue(data=0, node=node) + values = MockEntityValues(primary=value) + + device = climate.get_device(hass, node=node, values=values, node_config={}) + assert device is None + + +def test_get_device_detects_multiple_setpoint_device(device): + """Test get_device returns a Z-Wave multiple setpoint device.""" + assert isinstance(device, climate.ZWaveClimateMultipleSetpoint) + + +def test_get_device_detects_single_setpoint_device(device_single_setpoint): + """Test get_device returns a Z-Wave single setpoint device.""" + assert isinstance(device_single_setpoint, climate.ZWaveClimateSingleSetpoint) + + def test_default_hvac_modes(): """Test wether all hvac modes are included in default_hvac_modes.""" for hvac_mode in HVAC_MODES: @@ -155,7 +349,23 @@ def test_default_hvac_modes(): def test_supported_features(device): """Test supported features flags.""" - assert device.supported_features == SUPPORT_FAN_MODE + SUPPORT_TARGET_TEMPERATURE + assert ( + device.supported_features + == SUPPORT_FAN_MODE + + SUPPORT_TARGET_TEMPERATURE + + SUPPORT_TARGET_TEMPERATURE_RANGE + ) + + +def test_supported_features_temp_range(device_heat_cool_range): + """Test supported features flags with target temp range.""" + device = device_heat_cool_range + assert ( + device.supported_features + == SUPPORT_FAN_MODE + + SUPPORT_TARGET_TEMPERATURE + + SUPPORT_TARGET_TEMPERATURE_RANGE + ) def test_supported_features_preset_mode(device_mapping): @@ -163,7 +373,22 @@ def test_supported_features_preset_mode(device_mapping): device = device_mapping assert ( device.supported_features - == SUPPORT_FAN_MODE + SUPPORT_TARGET_TEMPERATURE + SUPPORT_PRESET_MODE + == SUPPORT_FAN_MODE + + SUPPORT_TARGET_TEMPERATURE + + SUPPORT_TARGET_TEMPERATURE_RANGE + + SUPPORT_PRESET_MODE + ) + + +def test_supported_features_preset_mode_away(device_heat_cool_away): + """Test supported features flags with swing mode.""" + device = device_heat_cool_away + assert ( + device.supported_features + == SUPPORT_FAN_MODE + + SUPPORT_TARGET_TEMPERATURE + + SUPPORT_TARGET_TEMPERATURE_RANGE + + SUPPORT_PRESET_MODE ) @@ -172,10 +397,34 @@ def test_supported_features_swing_mode(device_zxt_120): device = device_zxt_120 assert ( device.supported_features - == SUPPORT_FAN_MODE + SUPPORT_TARGET_TEMPERATURE + SUPPORT_SWING_MODE + == SUPPORT_FAN_MODE + + SUPPORT_TARGET_TEMPERATURE + + SUPPORT_TARGET_TEMPERATURE_RANGE + + SUPPORT_SWING_MODE ) +def test_supported_features_aux_heat(device_aux_heat): + """Test supported features flags with aux heat.""" + device = device_aux_heat + assert ( + device.supported_features + == SUPPORT_FAN_MODE + SUPPORT_TARGET_TEMPERATURE + SUPPORT_AUX_HEAT + ) + + +def test_supported_features_single_setpoint(device_single_setpoint): + """Test supported features flags for SETPOINT_THERMOSTAT.""" + device = device_single_setpoint + assert device.supported_features == SUPPORT_FAN_MODE + SUPPORT_TARGET_TEMPERATURE + + +def test_supported_features_single_setpoint_with_mode(device_single_setpoint_with_mode): + """Test supported features flags for SETPOINT_THERMOSTAT.""" + device = device_single_setpoint_with_mode + assert device.supported_features == SUPPORT_FAN_MODE + SUPPORT_TARGET_TEMPERATURE + + def test_zxt_120_swing_mode(device_zxt_120): """Test operation of the zxt 120 swing mode.""" device = device_zxt_120 @@ -207,14 +456,6 @@ def test_temperature_unit(device): assert device.temperature_unit == TEMP_CELSIUS -def test_default_target_temperature(device): - """Test default setting of target temperature.""" - assert device.target_temperature == 1 - device.values.primary.data = 0 - value_changed(device.values.primary) - assert device.target_temperature == 5 # Current Temperature - - def test_data_lists(device): """Test data lists from zwave value items.""" assert device.fan_modes == [3, 4, 5] @@ -225,7 +466,23 @@ def test_data_lists(device): HVAC_MODE_HEAT_COOL, ] assert device.preset_modes == [] - device.values.mode = None + device.values.primary = None + assert device.preset_modes == [] + + +def test_data_lists_single_setpoint(device_single_setpoint): + """Test data lists from zwave value items.""" + device = device_single_setpoint + assert device.fan_modes == [3, 4, 5] + assert device.hvac_modes == [] + assert device.preset_modes == [] + + +def test_data_lists_single_setpoint_with_mode(device_single_setpoint_with_mode): + """Test data lists from zwave value items.""" + device = device_single_setpoint_with_mode + assert device.fan_modes == [3, 4, 5] + assert device.hvac_modes == [HVAC_MODE_OFF, HVAC_MODE_HEAT] assert device.preset_modes == [] @@ -234,14 +491,77 @@ def test_data_lists_mapping(device_mapping): device = device_mapping assert device.hvac_modes == ["off", "cool", "heat", "heat_cool"] assert device.preset_modes == ["boost", "none"] - device.values.mode = None + device.values.primary = None assert device.preset_modes == [] def test_target_value_set(device): """Test values changed for climate device.""" - assert device.values.primary.data == 1 + assert device.values.setpoint_heating.data == 1 + assert device.values.setpoint_cooling.data == 10 device.set_temperature() + assert device.values.setpoint_heating.data == 1 + assert device.values.setpoint_cooling.data == 10 + device.set_temperature(**{ATTR_TEMPERATURE: 2}) + assert device.values.setpoint_heating.data == 2 + assert device.values.setpoint_cooling.data == 10 + device.set_hvac_mode(HVAC_MODE_COOL) + value_changed(device.values.primary) + assert device.values.setpoint_heating.data == 2 + assert device.values.setpoint_cooling.data == 10 + device.set_temperature(**{ATTR_TEMPERATURE: 9}) + assert device.values.setpoint_heating.data == 2 + assert device.values.setpoint_cooling.data == 9 + + +def test_target_value_set_range(device_heat_cool_range): + """Test values changed for climate device.""" + device = device_heat_cool_range + assert device.values.setpoint_heating.data == 1 + assert device.values.setpoint_cooling.data == 10 + device.set_temperature() + assert device.values.setpoint_heating.data == 1 + assert device.values.setpoint_cooling.data == 10 + device.set_temperature(**{ATTR_TARGET_TEMP_LOW: 2}) + assert device.values.setpoint_heating.data == 2 + assert device.values.setpoint_cooling.data == 10 + device.set_temperature(**{ATTR_TARGET_TEMP_HIGH: 9}) + assert device.values.setpoint_heating.data == 2 + assert device.values.setpoint_cooling.data == 9 + device.set_temperature(**{ATTR_TARGET_TEMP_LOW: 3, ATTR_TARGET_TEMP_HIGH: 8}) + assert device.values.setpoint_heating.data == 3 + assert device.values.setpoint_cooling.data == 8 + + +def test_target_value_set_range_away(device_heat_cool_away): + """Test values changed for climate device.""" + device = device_heat_cool_away + assert device.values.setpoint_heating.data == 2 + assert device.values.setpoint_cooling.data == 9 + assert device.values.setpoint_away_heating.data == 1 + assert device.values.setpoint_away_cooling.data == 10 + device.set_preset_mode(PRESET_AWAY) + device.set_temperature(**{ATTR_TARGET_TEMP_LOW: 0, ATTR_TARGET_TEMP_HIGH: 11}) + assert device.values.setpoint_heating.data == 2 + assert device.values.setpoint_cooling.data == 9 + assert device.values.setpoint_away_heating.data == 0 + assert device.values.setpoint_away_cooling.data == 11 + + +def test_target_value_set_eco(device_heat_eco): + """Test values changed for climate device.""" + device = device_heat_eco + assert device.values.setpoint_heating.data == 2 + assert device.values.setpoint_eco_heating.data == 1 + device.set_preset_mode("heat econ") + device.set_temperature(**{ATTR_TEMPERATURE: 0}) + assert device.values.setpoint_heating.data == 2 + assert device.values.setpoint_eco_heating.data == 0 + + +def test_target_value_set_single_setpoint(device_single_setpoint): + """Test values changed for climate device.""" + device = device_single_setpoint assert device.values.primary.data == 1 device.set_temperature(**{ATTR_TEMPERATURE: 2}) assert device.values.primary.data == 2 @@ -249,56 +569,56 @@ def test_target_value_set(device): def test_operation_value_set(device): """Test values changed for climate device.""" - assert device.values.mode.data == HVAC_MODE_HEAT + assert device.values.primary.data == HVAC_MODE_HEAT device.set_hvac_mode(HVAC_MODE_COOL) - assert device.values.mode.data == HVAC_MODE_COOL + assert device.values.primary.data == HVAC_MODE_COOL device.set_preset_mode(PRESET_ECO) - assert device.values.mode.data == PRESET_ECO + assert device.values.primary.data == PRESET_ECO device.set_preset_mode(PRESET_NONE) - assert device.values.mode.data == HVAC_MODE_HEAT_COOL - device.values.mode = None + assert device.values.primary.data == HVAC_MODE_HEAT_COOL + device.values.primary = None device.set_hvac_mode("test_set_failes") - assert device.values.mode is None + assert device.values.primary is None device.set_preset_mode("test_set_failes") - assert device.values.mode is None + assert device.values.primary is None def test_operation_value_set_mapping(device_mapping): """Test values changed for climate device. Mapping.""" device = device_mapping - assert device.values.mode.data == "Heat" + assert device.values.primary.data == "Heat" device.set_hvac_mode(HVAC_MODE_COOL) - assert device.values.mode.data == "Cool" + assert device.values.primary.data == "Cool" device.set_hvac_mode(HVAC_MODE_OFF) - assert device.values.mode.data == "Off" + assert device.values.primary.data == "Off" device.set_preset_mode(PRESET_BOOST) - assert device.values.mode.data == "Full Power" + assert device.values.primary.data == "Full Power" device.set_preset_mode(PRESET_ECO) - assert device.values.mode.data == "eco" + assert device.values.primary.data == "eco" def test_operation_value_set_unknown(device_unknown): """Test values changed for climate device. Unknown.""" device = device_unknown - assert device.values.mode.data == "Heat" + assert device.values.primary.data == "Heat" device.set_preset_mode("Abcdefg") - assert device.values.mode.data == "Abcdefg" + assert device.values.primary.data == "Abcdefg" device.set_preset_mode(PRESET_NONE) - assert device.values.mode.data == HVAC_MODE_HEAT_COOL + assert device.values.primary.data == HVAC_MODE_HEAT_COOL def test_operation_value_set_heat_cool(device_heat_cool): """Test values changed for climate device. Heat/Cool only.""" device = device_heat_cool - assert device.values.mode.data == HVAC_MODE_HEAT + assert device.values.primary.data == HVAC_MODE_HEAT device.set_preset_mode("Heat Eco") - assert device.values.mode.data == "Heat Eco" + assert device.values.primary.data == "Heat Eco" device.set_preset_mode(PRESET_NONE) - assert device.values.mode.data == HVAC_MODE_HEAT + assert device.values.primary.data == HVAC_MODE_HEAT device.set_preset_mode("Cool Eco") - assert device.values.mode.data == "Cool Eco" + assert device.values.primary.data == "Cool Eco" device.set_preset_mode(PRESET_NONE) - assert device.values.mode.data == HVAC_MODE_COOL + assert device.values.primary.data == HVAC_MODE_COOL def test_fan_mode_value_set(device): @@ -314,6 +634,85 @@ def test_fan_mode_value_set(device): def test_target_value_changed(device): """Test values changed for climate device.""" assert device.target_temperature == 1 + device.values.setpoint_heating.data = 2 + value_changed(device.values.setpoint_heating) + assert device.target_temperature == 2 + device.values.primary.data = HVAC_MODE_COOL + value_changed(device.values.primary) + assert device.target_temperature == 10 + device.values.setpoint_cooling.data = 9 + value_changed(device.values.setpoint_cooling) + assert device.target_temperature == 9 + + +def test_target_range_changed(device_heat_cool_range): + """Test values changed for climate device.""" + device = device_heat_cool_range + assert device.target_temperature_low == 1 + assert device.target_temperature_high == 10 + device.values.setpoint_heating.data = 2 + value_changed(device.values.setpoint_heating) + assert device.target_temperature_low == 2 + assert device.target_temperature_high == 10 + device.values.setpoint_cooling.data = 9 + value_changed(device.values.setpoint_cooling) + assert device.target_temperature_low == 2 + assert device.target_temperature_high == 9 + + +def test_target_changed_preset_range(device_heat_cool_away): + """Test values changed for climate device.""" + device = device_heat_cool_away + assert device.target_temperature_low == 2 + assert device.target_temperature_high == 9 + device.values.primary.data = PRESET_AWAY + value_changed(device.values.primary) + assert device.target_temperature_low == 1 + assert device.target_temperature_high == 10 + device.values.setpoint_away_heating.data = 0 + value_changed(device.values.setpoint_away_heating) + device.values.setpoint_away_cooling.data = 11 + value_changed(device.values.setpoint_away_cooling) + assert device.target_temperature_low == 0 + assert device.target_temperature_high == 11 + device.values.primary.data = HVAC_MODE_HEAT_COOL + value_changed(device.values.primary) + assert device.target_temperature_low == 2 + assert device.target_temperature_high == 9 + + +def test_target_changed_eco(device_heat_eco): + """Test values changed for climate device.""" + device = device_heat_eco + assert device.target_temperature == 2 + device.values.primary.data = "heat econ" + value_changed(device.values.primary) + assert device.target_temperature == 1 + device.values.setpoint_eco_heating.data = 0 + value_changed(device.values.setpoint_eco_heating) + assert device.target_temperature == 0 + device.values.primary.data = HVAC_MODE_HEAT + value_changed(device.values.primary) + assert device.target_temperature == 2 + + +def test_target_changed_with_mode(device): + """Test values changed for climate device.""" + assert device.hvac_mode == HVAC_MODE_HEAT + assert device.target_temperature == 1 + device.values.primary.data = HVAC_MODE_COOL + value_changed(device.values.primary) + assert device.target_temperature == 10 + device.values.primary.data = HVAC_MODE_HEAT_COOL + value_changed(device.values.primary) + assert device.target_temperature_low == 1 + assert device.target_temperature_high == 10 + + +def test_target_value_changed_single_setpoint(device_single_setpoint): + """Test values changed for climate device.""" + device = device_single_setpoint + assert device.target_temperature == 1 device.values.primary.data = 2 value_changed(device.values.primary) assert device.target_temperature == 2 @@ -331,15 +730,15 @@ def test_operation_value_changed(device): """Test values changed for climate device.""" assert device.hvac_mode == HVAC_MODE_HEAT assert device.preset_mode == PRESET_NONE - device.values.mode.data = HVAC_MODE_COOL - value_changed(device.values.mode) + device.values.primary.data = HVAC_MODE_COOL + value_changed(device.values.primary) assert device.hvac_mode == HVAC_MODE_COOL assert device.preset_mode == PRESET_NONE - device.values.mode.data = HVAC_MODE_OFF - value_changed(device.values.mode) + device.values.primary.data = HVAC_MODE_OFF + value_changed(device.values.primary) assert device.hvac_mode == HVAC_MODE_OFF assert device.preset_mode == PRESET_NONE - device.values.mode = None + device.values.primary = None assert device.hvac_mode == HVAC_MODE_HEAT_COOL assert device.preset_mode == PRESET_NONE @@ -349,8 +748,8 @@ def test_operation_value_changed_preset(device_mapping): device = device_mapping assert device.hvac_mode == HVAC_MODE_HEAT assert device.preset_mode == PRESET_NONE - device.values.mode.data = PRESET_ECO - value_changed(device.values.mode) + device.values.primary.data = PRESET_ECO + value_changed(device.values.primary) assert device.hvac_mode == HVAC_MODE_HEAT_COOL assert device.preset_mode == PRESET_ECO @@ -360,12 +759,12 @@ def test_operation_value_changed_mapping(device_mapping): device = device_mapping assert device.hvac_mode == HVAC_MODE_HEAT assert device.preset_mode == PRESET_NONE - device.values.mode.data = "Off" - value_changed(device.values.mode) + device.values.primary.data = "Off" + value_changed(device.values.primary) assert device.hvac_mode == HVAC_MODE_OFF assert device.preset_mode == PRESET_NONE - device.values.mode.data = "Cool" - value_changed(device.values.mode) + device.values.primary.data = "Cool" + value_changed(device.values.primary) assert device.hvac_mode == HVAC_MODE_COOL assert device.preset_mode == PRESET_NONE @@ -375,11 +774,11 @@ def test_operation_value_changed_mapping_preset(device_mapping): device = device_mapping assert device.hvac_mode == HVAC_MODE_HEAT assert device.preset_mode == PRESET_NONE - device.values.mode.data = "Full Power" - value_changed(device.values.mode) + device.values.primary.data = "Full Power" + value_changed(device.values.primary) assert device.hvac_mode == HVAC_MODE_HEAT_COOL assert device.preset_mode == PRESET_BOOST - device.values.mode = None + device.values.primary = None assert device.hvac_mode == HVAC_MODE_HEAT_COOL assert device.preset_mode == PRESET_NONE @@ -389,8 +788,8 @@ def test_operation_value_changed_unknown(device_unknown): device = device_unknown assert device.hvac_mode == HVAC_MODE_HEAT assert device.preset_mode == PRESET_NONE - device.values.mode.data = "Abcdefg" - value_changed(device.values.mode) + device.values.primary.data = "Abcdefg" + value_changed(device.values.primary) assert device.hvac_mode == HVAC_MODE_HEAT_COOL assert device.preset_mode == "Abcdefg" @@ -400,12 +799,12 @@ def test_operation_value_changed_heat_cool(device_heat_cool): device = device_heat_cool assert device.hvac_mode == HVAC_MODE_HEAT assert device.preset_mode == PRESET_NONE - device.values.mode.data = "Cool Eco" - value_changed(device.values.mode) + device.values.primary.data = "Cool Eco" + value_changed(device.values.primary) assert device.hvac_mode == HVAC_MODE_COOL assert device.preset_mode == "Cool Eco" - device.values.mode.data = "Heat Eco" - value_changed(device.values.mode) + device.values.primary.data = "Heat Eco" + value_changed(device.values.primary) assert device.hvac_mode == HVAC_MODE_HEAT assert device.preset_mode == "Heat Eco" @@ -450,3 +849,44 @@ def test_fan_action_value_changed(device): device.values.fan_action.data = 9 value_changed(device.values.fan_action) assert device.device_state_attributes[climate.ATTR_FAN_ACTION] == 9 + + +def test_aux_heat_unsupported_set(device): + """Test aux heat for climate device.""" + device = device + assert device.values.primary.data == HVAC_MODE_HEAT + device.turn_aux_heat_on() + assert device.values.primary.data == HVAC_MODE_HEAT + device.turn_aux_heat_off() + assert device.values.primary.data == HVAC_MODE_HEAT + + +def test_aux_heat_unsupported_value_changed(device): + """Test aux heat for climate device.""" + device = device + assert device.is_aux_heat is None + device.values.primary.data = HVAC_MODE_HEAT + value_changed(device.values.primary) + assert device.is_aux_heat is None + + +def test_aux_heat_set(device_aux_heat): + """Test aux heat for climate device.""" + device = device_aux_heat + assert device.values.primary.data == HVAC_MODE_HEAT + device.turn_aux_heat_on() + assert device.values.primary.data == AUX_HEAT_ZWAVE_MODE + device.turn_aux_heat_off() + assert device.values.primary.data == HVAC_MODE_HEAT + + +def test_aux_heat_value_changed(device_aux_heat): + """Test aux heat for climate device.""" + device = device_aux_heat + assert device.is_aux_heat is False + device.values.primary.data = AUX_HEAT_ZWAVE_MODE + value_changed(device.values.primary) + assert device.is_aux_heat is True + device.values.primary.data = HVAC_MODE_HEAT + value_changed(device.values.primary) + assert device.is_aux_heat is False diff --git a/tests/components/zwave/test_cover.py b/tests/components/zwave/test_cover.py index 848decc2fb5..e8b784feefe 100644 --- a/tests/components/zwave/test_cover.py +++ b/tests/components/zwave/test_cover.py @@ -1,15 +1,15 @@ """Test Z-Wave cover devices.""" from unittest.mock import MagicMock -from homeassistant.components.cover import SUPPORT_OPEN, SUPPORT_CLOSE +from homeassistant.components.cover import SUPPORT_CLOSE, SUPPORT_OPEN from homeassistant.components.zwave import ( - const, - cover, CONF_INVERT_OPENCLOSE_BUTTONS, CONF_INVERT_PERCENT, + const, + cover, ) -from tests.mock.zwave import MockNode, MockValue, MockEntityValues, value_changed +from tests.mock.zwave import MockEntityValues, MockNode, MockValue, value_changed def test_get_device_detects_none(hass, mock_openzwave): diff --git a/tests/components/zwave/test_fan.py b/tests/components/zwave/test_fan.py index 2252a42a064..e5dac881ba2 100644 --- a/tests/components/zwave/test_fan.py +++ b/tests/components/zwave/test_fan.py @@ -1,14 +1,14 @@ """Test Z-Wave fans.""" -from homeassistant.components.zwave import fan from homeassistant.components.fan import ( - SPEED_OFF, + SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, - SPEED_HIGH, + SPEED_OFF, SUPPORT_SET_SPEED, ) +from homeassistant.components.zwave import fan -from tests.mock.zwave import MockNode, MockValue, MockEntityValues, value_changed +from tests.mock.zwave import MockEntityValues, MockNode, MockValue, value_changed def test_get_device_detects_fan(mock_openzwave): diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index 1de69249bfe..b7ffaba7e42 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -19,8 +19,8 @@ from homeassistant.components.zwave import ( ) from homeassistant.components.zwave.binary_sensor import get_device from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START -from homeassistant.helpers.entity_registry import async_get_registry from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg +from homeassistant.helpers.entity_registry import async_get_registry from homeassistant.setup import setup_component from tests.common import ( @@ -130,6 +130,7 @@ async def test_auto_heal_midnight(hass, mock_openzwave): time = utc.localize(datetime(2017, 5, 6, 0, 0, 0)) async_fire_time_changed(hass, time) await hass.async_block_till_done() + await hass.async_block_till_done() assert network.heal.called assert len(network.heal.mock_calls) == 1 @@ -573,19 +574,38 @@ async def test_value_discovery_existing_entity(hass, mock_openzwave): assert len(mock_receivers) == 1 - node = MockNode(node_id=11, generic=const.GENERIC_TYPE_THERMOSTAT) - setpoint = MockValue( + node = MockNode( + node_id=11, + generic=const.GENERIC_TYPE_THERMOSTAT, + specific=const.SPECIFIC_TYPE_THERMOSTAT_GENERAL_V2, + ) + thermostat_mode = MockValue( + data="Heat", + data_items=["Off", "Heat"], + node=node, + command_class=const.COMMAND_CLASS_THERMOSTAT_MODE, + genre=const.GENRE_USER, + ) + setpoint_heating = MockValue( data=22.0, node=node, - index=12, - instance=13, command_class=const.COMMAND_CLASS_THERMOSTAT_SETPOINT, + index=1, genre=const.GENRE_USER, - units="C", ) - hass.async_add_job(mock_receivers[0], node, setpoint) + + hass.async_add_job(mock_receivers[0], node, thermostat_mode) await hass.async_block_till_done() + def mock_update(self): + self.hass.add_job(self.async_update_ha_state) + + with patch.object( + zwave.node_entity.ZWaveBaseEntity, "maybe_schedule_update", new=mock_update + ): + hass.async_add_job(mock_receivers[0], node, setpoint_heating) + await hass.async_block_till_done() + assert ( hass.states.get("climate.mock_node_mock_value").attributes["temperature"] == 22.0 @@ -597,9 +617,6 @@ async def test_value_discovery_existing_entity(hass, mock_openzwave): is None ) - def mock_update(self): - self.hass.add_job(self.async_update_ha_state) - with patch.object( zwave.node_entity.ZWaveBaseEntity, "maybe_schedule_update", new=mock_update ): @@ -607,7 +624,6 @@ async def test_value_discovery_existing_entity(hass, mock_openzwave): data=23.5, node=node, index=1, - instance=13, command_class=const.COMMAND_CLASS_SENSOR_MULTILEVEL, genre=const.GENRE_USER, units="C", @@ -627,6 +643,42 @@ async def test_value_discovery_existing_entity(hass, mock_openzwave): ) +async def test_value_discovery_legacy_thermostat(hass, mock_openzwave): + """Test discovery of a node. Special case for legacy thermostats.""" + mock_receivers = [] + + def mock_connect(receiver, signal, *args, **kwargs): + if signal == MockNetwork.SIGNAL_VALUE_ADDED: + mock_receivers.append(receiver) + + with patch("pydispatch.dispatcher.connect", new=mock_connect): + await async_setup_component(hass, "zwave", {"zwave": {}}) + await hass.async_block_till_done() + + assert len(mock_receivers) == 1 + + node = MockNode( + node_id=11, + generic=const.GENERIC_TYPE_THERMOSTAT, + specific=const.SPECIFIC_TYPE_SETPOINT_THERMOSTAT, + ) + setpoint_heating = MockValue( + data=22.0, + node=node, + command_class=const.COMMAND_CLASS_THERMOSTAT_SETPOINT, + index=1, + genre=const.GENRE_USER, + ) + + hass.async_add_job(mock_receivers[0], node, setpoint_heating) + await hass.async_block_till_done() + + assert ( + hass.states.get("climate.mock_node_mock_value").attributes["temperature"] + == 22.0 + ) + + async def test_power_schemes(hass, mock_openzwave): """Test power attribute.""" mock_receivers = [] diff --git a/tests/components/zwave/test_light.py b/tests/components/zwave/test_light.py index 55070b9081d..10efed24bf2 100644 --- a/tests/components/zwave/test_light.py +++ b/tests/components/zwave/test_light.py @@ -1,22 +1,22 @@ """Test Z-Wave lights.""" -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch from homeassistant.components import zwave -from homeassistant.components.zwave import const, light from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION, - SUPPORT_BRIGHTNESS, - SUPPORT_TRANSITION, - SUPPORT_COLOR, ATTR_WHITE_VALUE, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, SUPPORT_COLOR_TEMP, + SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE, ) +from homeassistant.components.zwave import const, light -from tests.mock.zwave import MockNode, MockValue, MockEntityValues, value_changed +from tests.mock.zwave import MockEntityValues, MockNode, MockValue, value_changed class MockLightValues(MockEntityValues): diff --git a/tests/components/zwave/test_lock.py b/tests/components/zwave/test_lock.py index 4a32c3fb07c..d5b6d0a0d27 100644 --- a/tests/components/zwave/test_lock.py +++ b/tests/components/zwave/test_lock.py @@ -1,10 +1,10 @@ """Test Z-Wave locks.""" -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch from homeassistant import config_entries from homeassistant.components.zwave import const, lock -from tests.mock.zwave import MockNode, MockValue, MockEntityValues, value_changed +from tests.mock.zwave import MockEntityValues, MockNode, MockValue, value_changed def test_get_device_detects_lock(mock_openzwave): diff --git a/tests/components/zwave/test_node_entity.py b/tests/components/zwave/test_node_entity.py index dba187d7b96..9136b53ff0b 100644 --- a/tests/components/zwave/test_node_entity.py +++ b/tests/components/zwave/test_node_entity.py @@ -1,11 +1,14 @@ """Test Z-Wave node entity.""" import unittest -from unittest.mock import patch, MagicMock -import tests.mock.zwave as mock_zwave +from unittest.mock import MagicMock, patch + import pytest -from homeassistant.components.zwave import node_entity, const + +from homeassistant.components.zwave import const, node_entity from homeassistant.const import ATTR_ENTITY_ID +import tests.mock.zwave as mock_zwave + async def test_maybe_schedule_update(hass, mock_openzwave): """Test maybe schedule update.""" diff --git a/tests/components/zwave/test_sensor.py b/tests/components/zwave/test_sensor.py index cec93f5af4a..74e1ef2cd03 100644 --- a/tests/components/zwave/test_sensor.py +++ b/tests/components/zwave/test_sensor.py @@ -2,7 +2,7 @@ from homeassistant.components.zwave import const, sensor import homeassistant.const -from tests.mock.zwave import MockNode, MockValue, MockEntityValues, value_changed +from tests.mock.zwave import MockEntityValues, MockNode, MockValue, value_changed def test_get_device_detects_none(mock_openzwave): diff --git a/tests/components/zwave/test_switch.py b/tests/components/zwave/test_switch.py index a275e4b9e01..4293a4a23fd 100644 --- a/tests/components/zwave/test_switch.py +++ b/tests/components/zwave/test_switch.py @@ -3,7 +3,7 @@ from unittest.mock import patch from homeassistant.components.zwave import switch -from tests.mock.zwave import MockNode, MockValue, MockEntityValues, value_changed +from tests.mock.zwave import MockEntityValues, MockNode, MockValue, value_changed def test_get_device_detects_switch(mock_openzwave): diff --git a/tests/components/zwave/test_websocket_api.py b/tests/components/zwave/test_websocket_api.py index b55024eb3f0..978fc09a10d 100644 --- a/tests/components/zwave/test_websocket_api.py +++ b/tests/components/zwave/test_websocket_api.py @@ -1,10 +1,9 @@ """Test Z-Wave Websocket API.""" from homeassistant.bootstrap import async_setup_component - from homeassistant.components.zwave.const import ( - CONF_USB_STICK_PATH, CONF_AUTOHEAL, CONF_POLLING_INTERVAL, + CONF_USB_STICK_PATH, ) from homeassistant.components.zwave.websocket_api import ID, TYPE diff --git a/tests/components/zwave/test_workaround.py b/tests/components/zwave/test_workaround.py index 0825b8f47f9..ec708d38e43 100644 --- a/tests/components/zwave/test_workaround.py +++ b/tests/components/zwave/test_workaround.py @@ -1,5 +1,6 @@ """Test Z-Wave workarounds.""" from homeassistant.components.zwave import const, workaround + from tests.mock.zwave import MockNode, MockValue diff --git a/tests/conftest.py b/tests/conftest.py index 5e1bbc76fb5..558da48a7c1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,35 +1,28 @@ """Set up some common test helper things.""" -import asyncio import functools import logging -import os from unittest.mock import patch import pytest import requests_mock as _requests_mock from homeassistant import util -from homeassistant.util import location from homeassistant.auth.const import GROUP_ID_ADMIN, GROUP_ID_READ_ONLY -from homeassistant.auth.providers import legacy_api_password, homeassistant +from homeassistant.auth.providers import homeassistant, legacy_api_password +from homeassistant.util import location pytest.register_assert_rewrite("tests.common") -from tests.common import ( # noqa: E402 module level import not at top of file - async_test_home_assistant, + +from tests.common import ( # noqa: E402, isort:skip + CLIENT_ID, INSTANCES, + MockUser, + async_test_home_assistant, mock_coro, mock_storage as mock_storage, - MockUser, - CLIENT_ID, ) -from tests.test_util.aiohttp import ( - mock_aiohttp_client, -) # noqa: E402 module level import not at top of file +from tests.test_util.aiohttp import mock_aiohttp_client # noqa: E402, isort:skip -if os.environ.get("UVLOOP") == "1": - import uvloop - - asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) logging.basicConfig(level=logging.DEBUG) logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO) @@ -67,7 +60,7 @@ def verify_cleanup(): for inst in INSTANCES: inst.stop() pytest.exit( - "Detected non stopped instances " "({}), aborting test run".format(count) + "Detected non stopped instances ({}), aborting test run".format(count) ) diff --git a/tests/fixtures/elgato/info.json b/tests/fixtures/elgato/info.json new file mode 100644 index 00000000000..e2a816df26e --- /dev/null +++ b/tests/fixtures/elgato/info.json @@ -0,0 +1,9 @@ +{ + "productName": "Elgato Key Light", + "hardwareBoardType": 53, + "firmwareBuildNumber": 192, + "firmwareVersion": "1.0.3", + "serialNumber": "CN11A1A00001", + "displayName": "Frenck", + "features": ["lights"] +} diff --git a/tests/fixtures/elgato/state.json b/tests/fixtures/elgato/state.json new file mode 100644 index 00000000000..f6180e14238 --- /dev/null +++ b/tests/fixtures/elgato/state.json @@ -0,0 +1,10 @@ +{ + "numberOfLights": 1, + "lights": [ + { + "on": 1, + "brightness": 21, + "temperature": 297 + } + ] +} diff --git a/tests/fixtures/homematicip_cloud.json b/tests/fixtures/homematicip_cloud.json index 8cec2462f32..01667f353d3 100644 --- a/tests/fixtures/homematicip_cloud.json +++ b/tests/fixtures/homematicip_cloud.json @@ -14,6 +14,68 @@ } }, "devices": { + "3014F7110000ABCDABCD0033": { + "availableFirmwareVersion": "1.0.6", + "firmwareVersion": "1.0.6", + "firmwareVersionInteger": 65542, + "functionalChannels": { + "0": { + "badBatteryHealth": true, + "coProFaulty": false, + "coProRestartNeeded": false, + "coProUpdateFailure": false, + "configPending": false, + "deviceId": "3014F7110000ABCDABCD0033", + "deviceOverheated": false, + "deviceOverloaded": false, + "deviceUndervoltage": false, + "dutyCycle": false, + "functionalChannelType": "DEVICE_RECHARGEABLE_WITH_SABOTAGE", + "groupIndex": 0, + "groups": [], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -51, + "rssiPeerValue": null, + "sabotage": false, + "supportedOptionalFeatures": { + "IFeatureDeviceCoProError": false, + "IFeatureDeviceCoProRestart": false, + "IFeatureDeviceCoProUpdate": false, + "IFeatureDeviceOverheated": false, + "IFeatureDeviceOverloaded": false, + "IFeatureDeviceTemperatureOutOfRange": false, + "IFeatureDeviceUndervoltage": false + }, + "temperatureOutOfRange": false, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000ABCDABCD0033", + "functionalChannelType": "ALARM_SIREN_CHANNEL", + "groupIndex": 1, + "groups": [], + "index": 1, + "label": "" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000ABCDABCD0033", + "label": "Alarmsirene \u2013 au\u00dfen", + "lastStatusUpdate": 1573078567665, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 385, + "modelType": "HmIP-ASIR-O", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000ABCDABCD0033", + "type": "ALARM_SIREN_OUTDOOR", + "updateState": "UP_TO_DATE" + }, "3014F7110000000000000031": { "availableFirmwareVersion": "1.2.1", "firmwareVersion": "1.2.1", @@ -685,14 +747,14 @@ "firmwareVersionInteger": 65542, "functionalChannels": { "0": { - "coProFaulty": false, - "coProRestartNeeded": false, - "coProUpdateFailure": false, + "coProFaulty": true, + "coProRestartNeeded": true, + "coProUpdateFailure": true, "configPending": false, "deviceId": "3014F7110000000000000064", "deviceOverheated": true, - "deviceOverloaded": false, - "deviceUndervoltage": false, + "deviceOverloaded": true, + "deviceUndervoltage": true, "dutyCycle": false, "functionalChannelType": "DEVICE_SABOTAGE", "groupIndex": 0, @@ -709,15 +771,15 @@ "rssiPeerValue": null, "sabotage": false, "supportedOptionalFeatures": { - "IFeatureDeviceCoProError": false, - "IFeatureDeviceCoProRestart": false, - "IFeatureDeviceCoProUpdate": false, + "IFeatureDeviceCoProError": true, + "IFeatureDeviceCoProRestart": true, + "IFeatureDeviceCoProUpdate": true, "IFeatureDeviceOverheated": true, - "IFeatureDeviceOverloaded": false, - "IFeatureDeviceTemperatureOutOfRange": false, - "IFeatureDeviceUndervoltage": false + "IFeatureDeviceOverloaded": true, + "IFeatureDeviceTemperatureOutOfRange": true, + "IFeatureDeviceUndervoltage": true }, - "temperatureOutOfRange": false, + "temperatureOutOfRange": true, "unreach": false }, "1": { @@ -4031,6 +4093,74 @@ "serializedGlobalTradeItemNumber": "3014F711BBBBBBBBBBBBB18", "type": "OPEN_COLLECTOR_8_MODULE", "updateState": "UP_TO_DATE" + }, + "3014F0000000000000FAF9B4": { + "availableFirmwareVersion": "1.0.0", + "firmwareVersion": "1.0.0", + "firmwareVersionInteger": 65536, + "functionalChannels": { + "0": { + "coProFaulty": false, + "coProRestartNeeded": false, + "coProUpdateFailure": false, + "configPending": false, + "deviceId": "3014F0000000000000FAF9B4", + "deviceOverheated": false, + "deviceOverloaded": false, + "deviceUndervoltage": false, + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000004" + ], + "index": 0, + "label": "", + "lowBat": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -52, + "rssiPeerValue": -54, + "supportedOptionalFeatures": { + "IFeatureDeviceCoProError": false, + "IFeatureDeviceCoProRestart": false, + "IFeatureDeviceCoProUpdate": false, + "IFeatureDeviceOverheated": false, + "IFeatureDeviceOverloaded": false, + "IFeatureDeviceTemperatureOutOfRange": false, + "IFeatureDeviceUndervoltage": false + }, + "temperatureOutOfRange": false, + "unreach": false + }, + "1": { + "deviceId": "3014F0000000000000FAF9B4", + "doorState": "CLOSED", + "functionalChannelType": "DOOR_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000005" + ], + "index": 1, + "label": "", + "on": false, + "processing": false, + "ventilationPositionSupported": true + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F0000000000000FAF9B4", + "label": "Garage Door Module", + "lastStatusUpdate": 1574878451970, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 376, + "modelType": "HmIP-MOD-TM", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F0000000000000FAF9B4", + "type": "TORMATIC_MODULE", + "updateState": "UP_TO_DATE" } }, "groups": { @@ -4406,7 +4536,11 @@ "lowBat": null, "metaGroupId": "00000000-0000-0000-0000-000000000017", "on": true, - "processing": null, + "primaryShadingLevel": 1.0, + "primaryShadingStateType": "POSITION_USED", + "processing": false, + "secondaryShadingLevel": null, + "secondaryShadingStateType": "NOT_EXISTENT", "shutterLevel": null, "slatsLevel": null, "type": "SWITCHING", @@ -5394,6 +5528,47 @@ "type": "HUMIDITY_WARNING_RULE_GROUP", "unreach": false, "ventilationRecommended": false + }, + "00000000-0000-0000-0000-000000000050": { + "bottomShutterLevel": 1.0, + "bottomSlatsLevel": 1.0, + "channels": [], + "dutyCycle": false, + "groupVisibility": "VISIBLE", + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000050", + "label": "Rollos", + "lastStatusUpdate": 1573078054795, + "lowBat": null, + "metaGroupId": null, + "primaryShadingLevel": 1.0, + "primaryShadingStateType": "POSITION_USED", + "processing": false, + "secondaryShadingLevel": null, + "secondaryShadingStateType": "NOT_EXISTENT", + "sensorSpecificParameters": {}, + "shutterLevel": 1.0, + "slatsLevel": null, + "topShutterLevel": 0.0, + "topSlatsLevel": 0.0, + "type": "EXTENDED_LINKED_SHUTTER", + "unreach": false + }, + "00000000-0000-0000-0000-000000000067": { + "channels": [], + "dutyCycle": null, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000067", + "label": "HOT_WATER", + "lastStatusUpdate": 0, + "lowBat": null, + "metaGroupId": null, + "on": null, + "onTime": 900.0, + "profileId": "00000000-0000-0000-0000-000000000068", + "profileMode": null, + "type": "HOT_WATER", + "unreach": null } }, "home": { diff --git a/tests/fixtures/yr.no.json b/tests/fixtures/yr.no.xml similarity index 100% rename from tests/fixtures/yr.no.json rename to tests/fixtures/yr.no.xml diff --git a/tests/hassfest/__init__.py b/tests/hassfest/__init__.py new file mode 100644 index 00000000000..1ec5a22a567 --- /dev/null +++ b/tests/hassfest/__init__.py @@ -0,0 +1 @@ +"""Tests for hassfest.""" diff --git a/tests/hassfest/test_dependencies.py b/tests/hassfest/test_dependencies.py new file mode 100644 index 00000000000..b9690107619 --- /dev/null +++ b/tests/hassfest/test_dependencies.py @@ -0,0 +1,121 @@ +"""Tests for hassfest dependency finder.""" +import ast + +import pytest +from script.hassfest.dependencies import ImportCollector + + +@pytest.fixture +def mock_collector(): + """Fixture with import collector that adds all referenced nodes.""" + collector = ImportCollector(None) + collector.unfiltered_referenced = set() + collector._add_reference = collector.unfiltered_referenced.add + return collector + + +def test_child_import(mock_collector): + """Test detecting a child_import reference.""" + mock_collector.visit( + ast.parse( + """ +from homeassistant.components import child_import +""" + ) + ) + assert mock_collector.unfiltered_referenced == {"child_import"} + + +def test_subimport(mock_collector): + """Test detecting a subimport reference.""" + mock_collector.visit( + ast.parse( + """ +from homeassistant.components.subimport.smart_home import EVENT_ALEXA_SMART_HOME +""" + ) + ) + assert mock_collector.unfiltered_referenced == {"subimport"} + + +def test_child_import_field(mock_collector): + """Test detecting a child_import_field reference.""" + mock_collector.visit( + ast.parse( + """ +from homeassistant.components.child_import_field import bla +""" + ) + ) + assert mock_collector.unfiltered_referenced == {"child_import_field"} + + +def test_renamed_absolute(mock_collector): + """Test detecting a renamed_absolute reference.""" + mock_collector.visit( + ast.parse( + """ +import homeassistant.components.renamed_absolute as hue +""" + ) + ) + assert mock_collector.unfiltered_referenced == {"renamed_absolute"} + + +def test_hass_components_var(mock_collector): + """Test detecting a hass_components_var reference.""" + mock_collector.visit( + ast.parse( + """ +def bla(hass): + hass.components.hass_components_var.async_do_something() +""" + ) + ) + assert mock_collector.unfiltered_referenced == {"hass_components_var"} + + +def test_hass_components_class(mock_collector): + """Test detecting a hass_components_class reference.""" + mock_collector.visit( + ast.parse( + """ +class Hello: + def something(self): + self.hass.components.hass_components_class.async_yo() +""" + ) + ) + assert mock_collector.unfiltered_referenced == {"hass_components_class"} + + +def test_all_imports(mock_collector): + """Test all imports together.""" + mock_collector.visit( + ast.parse( + """ +from homeassistant.components import child_import + +from homeassistant.components.subimport.smart_home import EVENT_ALEXA_SMART_HOME + +from homeassistant.components.child_import_field import bla + +import homeassistant.components.renamed_absolute as hue + +def bla(hass): + hass.components.hass_components_var.async_do_something() + +class Hello: + def something(self): + self.hass.components.hass_components_class.async_yo() +""" + ) + ) + assert mock_collector.unfiltered_referenced == { + "child_import", + "subimport", + "child_import_field", + "renamed_absolute", + "hass_components_var", + "hass_components_class", + } diff --git a/tests/helpers/test_aiohttp_client.py b/tests/helpers/test_aiohttp_client.py index 5494bc40a75..de7057ae9c7 100644 --- a/tests/helpers/test_aiohttp_client.py +++ b/tests/helpers/test_aiohttp_client.py @@ -6,8 +6,8 @@ import aiohttp import pytest from homeassistant.core import EVENT_HOMEASSISTANT_CLOSE -from homeassistant.setup import async_setup_component import homeassistant.helpers.aiohttp_client as client +from homeassistant.setup import async_setup_component from homeassistant.util.async_ import run_callback_threadsafe from tests.common import get_test_home_assistant diff --git a/tests/helpers/test_area_registry.py b/tests/helpers/test_area_registry.py index 48d32861a0a..87e4b4c4d03 100644 --- a/tests/helpers/test_area_registry.py +++ b/tests/helpers/test_area_registry.py @@ -6,7 +6,8 @@ import pytest from homeassistant.core import callback from homeassistant.helpers import area_registry -from tests.common import mock_area_registry, flush_store + +from tests.common import flush_store, mock_area_registry @pytest.fixture diff --git a/tests/helpers/test_check_config.py b/tests/helpers/test_check_config.py index 0b34b263caf..0182d830e4c 100644 --- a/tests/helpers/test_check_config.py +++ b/tests/helpers/test_check_config.py @@ -2,11 +2,12 @@ import logging from unittest.mock import patch -from homeassistant.helpers.check_config import ( - async_check_ha_config_file, - CheckConfigError, -) from homeassistant.config import YAML_CONFIG_FILE +from homeassistant.helpers.check_config import ( + CheckConfigError, + async_check_ha_config_file, +) + from tests.common import patch_yaml_files _LOGGER = logging.getLogger(__name__) @@ -22,7 +23,7 @@ BASE_CONFIG = ( "\n\n" ) -BAD_CORE_CONFIG = "homeassistant:\n" " unit_system: bad\n" "\n\n" +BAD_CORE_CONFIG = "homeassistant:\n unit_system: bad\n\n\n" def log_ha_config(conf): @@ -105,8 +106,7 @@ async def test_component_platform_not_found_2(hass, loop): async def test_package_invalid(hass, loop): """Test a valid platform setup.""" files = { - YAML_CONFIG_FILE: BASE_CONFIG - + (" packages:\n" " p1:\n" ' group: ["a"]') + YAML_CONFIG_FILE: BASE_CONFIG + (" packages:\n p1:\n" ' group: ["a"]') } with patch("os.path.isfile", return_value=True), patch_yaml_files(files): res = await async_check_ha_config_file(hass) diff --git a/tests/helpers/test_config_entry_flow.py b/tests/helpers/test_config_entry_flow.py index 3c3d1224e12..1c292e5ed48 100644 --- a/tests/helpers/test_config_entry_flow.py +++ b/tests/helpers/test_config_entry_flow.py @@ -1,16 +1,17 @@ """Tests for the Config Entry Flow helper.""" -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch import pytest from homeassistant import config_entries, data_entry_flow, setup from homeassistant.helpers import config_entry_flow + from tests.common import ( MockConfigEntry, MockModule, mock_coro, - mock_integration, mock_entity_platform, + mock_integration, ) diff --git a/tests/helpers/test_config_entry_oauth2_flow.py b/tests/helpers/test_config_entry_oauth2_flow.py index 773dfa09375..366c295874d 100644 --- a/tests/helpers/test_config_entry_oauth2_flow.py +++ b/tests/helpers/test_config_entry_oauth2_flow.py @@ -1,15 +1,15 @@ """Tests for the Somfy config flow.""" import asyncio import logging -from unittest.mock import patch import time +from unittest.mock import patch import pytest -from homeassistant import data_entry_flow, setup, config_entries +from homeassistant import config_entries, data_entry_flow, setup from homeassistant.helpers import config_entry_oauth2_flow -from tests.common import mock_platform, MockConfigEntry +from tests.common import MockConfigEntry, mock_platform TEST_DOMAIN = "oauth2_test" CLIENT_SECRET = "5678" diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index 1f5d6ddfc40..57554d37bb1 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -395,7 +395,7 @@ def test_template_complex(): """Test template_complex validator.""" schema = vol.Schema(cv.template_complex) - for value in (None, "{{ partial_print }", "{% if True %}Hello"): + for value in ("{{ partial_print }", "{% if True %}Hello"): with pytest.raises(vol.MultipleInvalid): schema(value) @@ -420,6 +420,10 @@ def test_template_complex(): ["{{ beer }}", 1], ) + # Ensure we don't mutate non-string types that cannot be templates. + for value in (1, True, None): + assert schema(value) == value + def test_time_zone(): """Test time zone validation.""" diff --git a/tests/helpers/test_deprecation.py b/tests/helpers/test_deprecation.py index d0cb0eca55a..38410c3bf0f 100644 --- a/tests/helpers/test_deprecation.py +++ b/tests/helpers/test_deprecation.py @@ -1,7 +1,7 @@ """Test deprecation helpers.""" -from homeassistant.helpers.deprecation import deprecated_substitute, get_deprecated +from unittest.mock import MagicMock, patch -from unittest.mock import patch, MagicMock +from homeassistant.helpers.deprecation import deprecated_substitute, get_deprecated class MockBaseClass: diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index 1b146e9cb12..7f31c32cde3 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -7,7 +7,8 @@ import pytest from homeassistant.core import callback from homeassistant.helpers import device_registry -from tests.common import mock_device_registry, flush_store + +from tests.common import flush_store, mock_device_registry @pytest.fixture @@ -339,7 +340,7 @@ async def test_no_unnecessary_changes(registry): identifiers={("hue", "456"), ("bla", "123")}, ) with patch( - "homeassistant.helpers.device_registry" ".DeviceRegistry.async_schedule_save" + "homeassistant.helpers.device_registry.DeviceRegistry.async_schedule_save" ) as mock_save: entry2 = registry.async_get_or_create( config_entry_id="1234", identifiers={("hue", "456")} diff --git a/tests/helpers/test_discovery.py b/tests/helpers/test_discovery.py index dbd837eb5c7..3b0996d676a 100644 --- a/tests/helpers/test_discovery.py +++ b/tests/helpers/test_discovery.py @@ -9,12 +9,12 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import discovery from tests.common import ( - get_test_home_assistant, MockModule, MockPlatform, + get_test_home_assistant, mock_coro, - mock_integration, mock_entity_platform, + mock_integration, ) diff --git a/tests/helpers/test_dispatcher.py b/tests/helpers/test_dispatcher.py index 789cb6c1dc8..4cf266e88a2 100644 --- a/tests/helpers/test_dispatcher.py +++ b/tests/helpers/test_dispatcher.py @@ -4,8 +4,8 @@ import asyncio from homeassistant.core import callback from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, - dispatcher_send, dispatcher_connect, + dispatcher_send, ) from tests.common import get_test_home_assistant diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 9d05920f78b..749c11ff1a5 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -1,16 +1,16 @@ """Test the entity helper.""" # pylint: disable=protected-access import asyncio -import threading from datetime import timedelta -from unittest.mock import MagicMock, patch, PropertyMock +import threading +from unittest.mock import MagicMock, PropertyMock, patch import pytest -from homeassistant.helpers import entity, entity_registry -from homeassistant.core import Context -from homeassistant.const import ATTR_HIDDEN, ATTR_DEVICE_CLASS from homeassistant.config import DATA_CUSTOMIZE +from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_HIDDEN, STATE_UNAVAILABLE +from homeassistant.core import Context +from homeassistant.helpers import entity, entity_registry from homeassistant.helpers.entity_values import EntityValues from tests.common import get_test_home_assistant, mock_registry @@ -641,3 +641,23 @@ async def test_disabled_in_entity_registry(hass): assert entry3 != entry2 assert ent.registry_entry == entry3 assert ent.enabled is False + + +async def test_capability_attrs(hass): + """Test we still include capabilities even when unavailable.""" + with patch.object( + entity.Entity, "available", PropertyMock(return_value=False) + ), patch.object( + entity.Entity, + "capability_attributes", + PropertyMock(return_value={"always": "there"}), + ): + ent = entity.Entity() + ent.hass = hass + ent.entity_id = "hello.world" + ent.async_write_ha_state() + + state = hass.states.get("hello.world") + assert state is not None + assert state.state == STATE_UNAVAILABLE + assert state.attributes["always"] == "there" diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index 0d52f430ff5..7284a5d9b67 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -1,29 +1,28 @@ """The tests for the Entity component helper.""" # pylint: disable=protected-access from collections import OrderedDict -import logging -from unittest.mock import patch, Mock from datetime import timedelta +import logging +from unittest.mock import Mock, patch import asynctest import pytest +from homeassistant.const import ENTITY_MATCH_ALL import homeassistant.core as ha from homeassistant.exceptions import PlatformNotReady -from homeassistant.components import group +from homeassistant.helpers import discovery from homeassistant.helpers.entity_component import EntityComponent from homeassistant.setup import async_setup_component - -from homeassistant.helpers import discovery import homeassistant.util.dt as dt_util from tests.common import ( - MockPlatform, - MockModule, - mock_coro, - async_fire_time_changed, - MockEntity, MockConfigEntry, + MockEntity, + MockModule, + MockPlatform, + async_fire_time_changed, + mock_coro, mock_entity_platform, mock_integration, ) @@ -116,7 +115,7 @@ async def test_setup_recovers_when_setup_raises(hass): @asynctest.patch( - "homeassistant.helpers.entity_component.EntityComponent" ".async_setup_platform", + "homeassistant.helpers.entity_component.EntityComponent.async_setup_platform", return_value=mock_coro(), ) @asynctest.patch( @@ -138,7 +137,7 @@ async def test_setup_does_discovery(mock_setup_component, mock_setup, hass): assert ("platform_test", {}, {"msg": "discovery_info"}) == mock_setup.call_args[0] -@asynctest.patch("homeassistant.helpers.entity_platform." "async_track_time_interval") +@asynctest.patch("homeassistant.helpers.entity_platform.async_track_time_interval") async def test_set_scan_interval_via_config(mock_track, hass): """Test the setting of the scan interval via configuration.""" @@ -194,7 +193,7 @@ async def test_extract_from_service_available_device(hass): ] ) - call_1 = ha.ServiceCall("test", "service") + call_1 = ha.ServiceCall("test", "service", data={"entity_id": ENTITY_MATCH_ALL}) assert ["test_domain.test_1", "test_domain.test_3"] == sorted( ent.entity_id for ent in (await component.async_extract_from_service(call_1)) @@ -250,7 +249,7 @@ async def test_platform_not_ready(hass): assert "test_domain.mod1" in hass.config.components -async def test_extract_from_service_returns_all_if_no_entity_id(hass): +async def test_extract_from_service_fails_if_no_entity_id(hass): """Test the extraction of everything from service.""" component = EntityComponent(_LOGGER, DOMAIN, hass) await component.async_add_entities( @@ -259,7 +258,7 @@ async def test_extract_from_service_returns_all_if_no_entity_id(hass): call = ha.ServiceCall("test", "service") - assert ["test_domain.test_1", "test_domain.test_2"] == sorted( + assert [] == sorted( ent.entity_id for ent in (await component.async_extract_from_service(call)) ) @@ -285,15 +284,13 @@ async def test_extract_from_service_filter_out_non_existing_entities(hass): async def test_extract_from_service_no_group_expand(hass): """Test not expanding a group.""" component = EntityComponent(_LOGGER, DOMAIN, hass) - test_group = await group.Group.async_create_group( - hass, "test_group", ["light.Ceiling", "light.Kitchen"] - ) - await component.async_add_entities([test_group]) + await component.async_add_entities([MockEntity(entity_id="group.test_group")]) call = ha.ServiceCall("test", "service", {"entity_id": ["group.test_group"]}) extracted = await component.async_extract_from_service(call, expand_group=False) - assert extracted == [test_group] + assert len(extracted) == 1 + assert extracted[0].entity_id == "group.test_group" async def test_setup_dependencies_platform(hass): @@ -445,12 +442,9 @@ async def test_extract_all_omit_entity_id(hass, caplog): call = ha.ServiceCall("test", "service") - assert ["test_domain.test_1", "test_domain.test_2"] == sorted( + assert [] == sorted( ent.entity_id for ent in await component.async_extract_from_service(call) ) - assert ( - "Not passing an entity ID to a service to target all entities is " "deprecated" - ) in caplog.text async def test_extract_all_use_match_all(hass, caplog): @@ -466,5 +460,5 @@ async def test_extract_all_use_match_all(hass, caplog): ent.entity_id for ent in await component.async_extract_from_service(call) ) assert ( - "Not passing an entity ID to a service to target all entities is " "deprecated" + "Not passing an entity ID to a service to target all entities is deprecated" ) not in caplog.text diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index caf8bb702af..592cc24df8e 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -1,30 +1,29 @@ """Tests for the EntityPlatform helper.""" import asyncio -import logging -from unittest.mock import patch, Mock, MagicMock from datetime import timedelta +import logging +from unittest.mock import MagicMock, Mock, patch import asynctest import pytest from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers import entity_platform, entity_registry from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.entity_component import ( - EntityComponent, DEFAULT_SCAN_INTERVAL, + EntityComponent, ) -from homeassistant.helpers import entity_platform, entity_registry - import homeassistant.util.dt as dt_util from tests.common import ( - MockPlatform, - async_fire_time_changed, - mock_registry, + MockConfigEntry, MockEntity, MockEntityPlatform, - MockConfigEntry, + MockPlatform, + async_fire_time_changed, mock_entity_platform, + mock_registry, ) _LOGGER = logging.getLogger(__name__) @@ -135,7 +134,7 @@ async def test_update_state_adds_entities_with_update_before_add_false(hass): assert not ent.update.called -@asynctest.patch("homeassistant.helpers.entity_platform." "async_track_time_interval") +@asynctest.patch("homeassistant.helpers.entity_platform.async_track_time_interval") async def test_set_scan_interval_via_platform(mock_track, hass): """Test the setting of the scan interval via platform.""" @@ -794,3 +793,44 @@ async def test_entity_disabled_by_integration(hass): assert entry_default.disabled_by is None entry_disabled = registry.async_get_or_create(DOMAIN, DOMAIN, "disabled") assert entry_disabled.disabled_by == "integration" + + +async def test_entity_info_added_to_entity_registry(hass): + """Test entity info is written to entity registry.""" + component = EntityComponent(_LOGGER, DOMAIN, hass, timedelta(seconds=20)) + + entity_default = MockEntity( + unique_id="default", + capability_attributes={"max": 100}, + supported_features=5, + device_class="mock-device-class", + ) + + await component.async_add_entities([entity_default]) + + registry = await hass.helpers.entity_registry.async_get_registry() + + entry_default = registry.async_get_or_create(DOMAIN, DOMAIN, "default") + print(entry_default) + assert entry_default.capabilities == {"max": 100} + assert entry_default.supported_features == 5 + assert entry_default.device_class == "mock-device-class" + + +async def test_override_restored_entities(hass): + """Test that we allow overriding restored entities.""" + registry = mock_registry(hass) + registry.async_get_or_create( + "test_domain", "test_domain", "1234", suggested_object_id="world" + ) + + hass.states.async_set("test_domain.world", "unavailable", {"restored": True}) + + component = EntityComponent(_LOGGER, DOMAIN, hass) + + await component.async_add_entities( + [MockEntity(unique_id="1234", state="on", entity_id="test_domain.world")], True + ) + + state = hass.states.get("test_domain.world") + assert state.state == "on" diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 9debbdbcba7..7f45ff0d174 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -5,11 +5,11 @@ from unittest.mock import patch import asynctest import pytest -from homeassistant.core import valid_entity_id, callback +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 tests.common import MockConfigEntry, mock_registry, flush_store - +from tests.common import MockConfigEntry, flush_store, mock_registry YAML__OPEN_PATH = "homeassistant.util.yaml.loader.open" @@ -58,6 +58,52 @@ def test_get_or_create_suggested_object_id(registry): assert entry.entity_id == "light.beer" +def test_get_or_create_updates_data(registry): + """Test that we update data in get_or_create.""" + orig_config_entry = MockConfigEntry(domain="light") + + orig_entry = registry.async_get_or_create( + "light", + "hue", + "5678", + config_entry=orig_config_entry, + device_id="mock-dev-id", + capabilities={"max": 100}, + supported_features=5, + device_class="mock-device-class", + disabled_by=entity_registry.DISABLED_HASS, + ) + + assert orig_entry.config_entry_id == orig_config_entry.entry_id + assert orig_entry.device_id == "mock-dev-id" + 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 + + new_config_entry = MockConfigEntry(domain="light") + + new_entry = registry.async_get_or_create( + "light", + "hue", + "5678", + config_entry=new_config_entry, + device_id="new-mock-dev-id", + capabilities={"new-max": 100}, + supported_features=10, + device_class="new-mock-device-class", + disabled_by=entity_registry.DISABLED_USER, + ) + + assert new_entry.config_entry_id == new_config_entry.entry_id + assert new_entry.device_id == "new-mock-dev-id" + assert new_entry.capabilities == {"new-max": 100} + assert new_entry.supported_features == 10 + assert new_entry.device_class == "new-mock-device-class" + # Should not be updated + assert new_entry.disabled_by == entity_registry.DISABLED_HASS + + def test_get_or_create_suggested_object_id_conflict_register(registry): """Test that we don't generate an entity id that is already registered.""" entry = registry.async_get_or_create( @@ -92,7 +138,15 @@ async def test_loading_saving_data(hass, registry): orig_entry1 = registry.async_get_or_create("light", "hue", "1234") orig_entry2 = registry.async_get_or_create( - "light", "hue", "5678", config_entry=mock_config + "light", + "hue", + "5678", + device_id="mock-dev-id", + config_entry=mock_config, + capabilities={"max": 100}, + supported_features=5, + device_class="mock-device-class", + disabled_by=entity_registry.DISABLED_HASS, ) assert len(registry.entities) == 2 @@ -105,13 +159,17 @@ async def test_loading_saving_data(hass, registry): # Ensure same order assert list(registry.entities) == list(registry2.entities) new_entry1 = registry.async_get_or_create("light", "hue", "1234") - new_entry2 = registry.async_get_or_create( - "light", "hue", "5678", config_entry=mock_config - ) + new_entry2 = registry.async_get_or_create("light", "hue", "5678") assert orig_entry1 == new_entry1 assert orig_entry2 == new_entry2 + assert new_entry2.device_id == "mock-dev-id" + assert new_entry2.disabled_by == entity_registry.DISABLED_HASS + assert new_entry2.capabilities == {"max": 100} + assert new_entry2.supported_features == 5 + assert new_entry2.device_class == "mock-device-class" + def test_generate_entity_considers_registered_entities(registry): """Test that we don't create entity id that are already registered.""" @@ -418,3 +476,62 @@ async def test_disabled_by_system_options(registry): "light", "hue", "BBBB", config_entry=mock_config, disabled_by="user" ) assert entry2.disabled_by == "user" + + +async def test_restore_states(hass): + """Test restoring states.""" + hass.state = CoreState.not_running + + registry = await entity_registry.async_get_registry(hass) + + registry.async_get_or_create( + "light", "hue", "1234", suggested_object_id="simple", + ) + # Should not be created + registry.async_get_or_create( + "light", + "hue", + "5678", + suggested_object_id="disabled", + disabled_by=entity_registry.DISABLED_HASS, + ) + registry.async_get_or_create( + "light", + "hue", + "9012", + suggested_object_id="all_info_set", + capabilities={"max": 100}, + supported_features=5, + device_class="mock-device-class", + ) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {}) + await hass.async_block_till_done() + + simple = hass.states.get("light.simple") + assert simple is not None + assert simple.state == STATE_UNAVAILABLE + assert simple.attributes == {"restored": True} + + disabled = hass.states.get("light.disabled") + assert disabled is None + + all_info_set = hass.states.get("light.all_info_set") + assert all_info_set is not None + assert all_info_set.state == STATE_UNAVAILABLE + assert all_info_set.attributes == { + "max": 100, + "supported_features": 5, + "device_class": "mock-device-class", + "restored": True, + } + + registry.async_remove("light.disabled") + registry.async_remove("light.simple") + registry.async_remove("light.all_info_set") + + await hass.async_block_till_done() + + assert hass.states.get("light.simple") is None + assert hass.states.get("light.disabled") is None + assert hass.states.get("light.all_info_set") is None diff --git a/tests/helpers/test_entity_values.py b/tests/helpers/test_entity_values.py index d9be6c52b4e..f5ad68ee231 100644 --- a/tests/helpers/test_entity_values.py +++ b/tests/helpers/test_entity_values.py @@ -1,5 +1,6 @@ """Test the entity values helper.""" from collections import OrderedDict + from homeassistant.helpers.entity_values import EntityValues as EV ent = "test.test" diff --git a/tests/helpers/test_entityfilter.py b/tests/helpers/test_entityfilter.py index 8deea67ac16..726e6bd92d0 100644 --- a/tests/helpers/test_entityfilter.py +++ b/tests/helpers/test_entityfilter.py @@ -1,5 +1,5 @@ """The tests for the EntityFilter component.""" -from homeassistant.helpers.entityfilter import generate_filter, FILTER_SCHEMA +from homeassistant.helpers.entityfilter import FILTER_SCHEMA, generate_filter def test_no_filters_case_1(): diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index 3d4804e5079..cef8baec70e 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -7,10 +7,10 @@ from unittest.mock import patch from astral import Astral import pytest -from homeassistant.core import callback -from homeassistant.setup import async_setup_component -import homeassistant.core as ha +from homeassistant.components import sun from homeassistant.const import MATCH_ALL +import homeassistant.core as ha +from homeassistant.core import callback from homeassistant.helpers.event import ( async_call_later, async_track_point_in_time, @@ -25,7 +25,7 @@ from homeassistant.helpers.event import ( async_track_utc_time_change, ) from homeassistant.helpers.template import Template -from homeassistant.components import sun +from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from tests.common import async_fire_time_changed @@ -805,7 +805,7 @@ async def test_call_later(hass): now = datetime(2017, 12, 19, 15, 40, 0, tzinfo=dt_util.UTC) with patch( - "homeassistant.helpers.event" ".async_track_point_in_utc_time" + "homeassistant.helpers.event.async_track_point_in_utc_time" ) as mock, patch("homeassistant.util.dt.utcnow", return_value=now): async_call_later(hass, 3, action) @@ -825,7 +825,7 @@ async def test_async_call_later(hass): now = datetime(2017, 12, 19, 15, 40, 0, tzinfo=dt_util.UTC) with patch( - "homeassistant.helpers.event" ".async_track_point_in_utc_time" + "homeassistant.helpers.event.async_track_point_in_utc_time" ) as mock, patch("homeassistant.util.dt.utcnow", return_value=now): remove = async_call_later(hass, 3, action) diff --git a/tests/helpers/test_icon.py b/tests/helpers/test_icon.py index ce6e95110c9..4f1d4cb223f 100644 --- a/tests/helpers/test_icon.py +++ b/tests/helpers/test_icon.py @@ -44,3 +44,15 @@ def test_battery_icon(): postfix = "" assert iconbase + postfix == icon_for_battery_level(level, False) assert iconbase + postfix_charging == icon_for_battery_level(level, True) + + +def test_signal_icon(): + """Test icon generator for signal sensor.""" + from homeassistant.helpers.icon import icon_for_signal_level + + assert icon_for_signal_level(None) == "mdi:signal-cellular-outline" + assert icon_for_signal_level(0) == "mdi:signal-cellular-outline" + assert icon_for_signal_level(5) == "mdi:signal-cellular-1" + assert icon_for_signal_level(40) == "mdi:signal-cellular-2" + assert icon_for_signal_level(80) == "mdi:signal-cellular-3" + assert icon_for_signal_level(100) == "mdi:signal-cellular-3" diff --git a/tests/helpers/test_integration_platform.py b/tests/helpers/test_integration_platform.py new file mode 100644 index 00000000000..d6c844c0d91 --- /dev/null +++ b/tests/helpers/test_integration_platform.py @@ -0,0 +1,37 @@ +"""Test integration platform helpers.""" +from unittest.mock import Mock + +from homeassistant.setup import ATTR_COMPONENT, EVENT_COMPONENT_LOADED + +from tests.common import mock_platform + + +async def test_process_integration_platforms(hass): + """Test processing integrations.""" + loaded_platform = Mock() + mock_platform(hass, "loaded.platform_to_check", loaded_platform) + hass.config.components.add("loaded") + + event_platform = Mock() + mock_platform(hass, "event.platform_to_check", event_platform) + + processed = [] + + async def _process_platform(hass, domain, platform): + """Process platform.""" + processed.append((domain, platform)) + + await hass.helpers.integration_platform.async_process_integration_platforms( + "platform_to_check", _process_platform + ) + + assert len(processed) == 1 + assert processed[0][0] == "loaded" + assert processed[0][1] == loaded_platform + + hass.bus.async_fire(EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: "event"}) + await hass.async_block_till_done() + + assert len(processed) == 2 + assert processed[1][0] == "event" + assert processed[1][1] == event_platform diff --git a/tests/helpers/test_intent.py b/tests/helpers/test_intent.py index 3988c86f516..bbb6e394ee0 100644 --- a/tests/helpers/test_intent.py +++ b/tests/helpers/test_intent.py @@ -1,11 +1,10 @@ """Tests for the intent helpers.""" +import pytest import voluptuous as vol -import pytest - from homeassistant.core import State -from homeassistant.helpers import intent, config_validation as cv +from homeassistant.helpers import config_validation as cv, intent class MockIntentHandler(intent.IntentHandler): diff --git a/tests/helpers/test_network.py b/tests/helpers/test_network.py index afb9e88c5a4..d4c5366b879 100644 --- a/tests/helpers/test_network.py +++ b/tests/helpers/test_network.py @@ -1,8 +1,8 @@ """Test network helper.""" from unittest.mock import Mock, patch -from homeassistant.helpers import network from homeassistant.components import cloud +from homeassistant.helpers import network async def test_get_external_url(hass): diff --git a/tests/helpers/test_restore_state.py b/tests/helpers/test_restore_state.py index 94a17697eb4..97004362d20 100644 --- a/tests/helpers/test_restore_state.py +++ b/tests/helpers/test_restore_state.py @@ -8,15 +8,14 @@ from homeassistant.core import CoreState, State from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import Entity from homeassistant.helpers.restore_state import ( - RestoreStateData, - RestoreEntity, - StoredState, DATA_RESTORE_STATE_TASK, STORAGE_KEY, + RestoreEntity, + RestoreStateData, + StoredState, ) from homeassistant.util import dt as dt_util - from tests.common import mock_coro diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 4b8be715f37..a7fe2c25236 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -6,21 +6,19 @@ from unittest import mock import asynctest import jinja2 -import voluptuous as vol import pytest - -import homeassistant.components.scene as scene -from homeassistant import exceptions -from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON -from homeassistant.core import Context, callback +import voluptuous as vol # Otherwise can't test just this file (import order issue) +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, callback +from homeassistant.helpers import config_validation as cv, script import homeassistant.util.dt as dt_util -from homeassistant.helpers import script, config_validation as cv from tests.common import async_fire_time_changed - ENTITY_ID = "script.test" diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index de8142a6374..b42b30a836a 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -5,28 +5,29 @@ from copy import deepcopy import unittest from unittest.mock import Mock, patch -import voluptuous as vol import pytest +import voluptuous as vol # To prevent circular import when running just this file -import homeassistant.components # noqa: F401 from homeassistant import core as ha, exceptions -from homeassistant.const import STATE_ON, STATE_OFF, ATTR_ENTITY_ID -from homeassistant.setup import async_setup_component -import homeassistant.helpers.config_validation as cv from homeassistant.auth.permissions import PolicyPermissions +import homeassistant.components # noqa: F401 +from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, STATE_OFF, STATE_ON from homeassistant.helpers import ( - service, - template, device_registry as dev_reg, entity_registry as ent_reg, + service, + template, ) +import homeassistant.helpers.config_validation as cv +from homeassistant.setup import async_setup_component + from tests.common import ( get_test_home_assistant, - mock_service, mock_coro, - mock_registry, mock_device_registry, + mock_registry, + mock_service, ) @@ -334,7 +335,10 @@ async def test_call_context_target_all(hass, mock_service_platform_call, mock_en [Mock(entities=mock_entities)], Mock(), ha.ServiceCall( - "test_domain", "test_service", context=ha.Context(user_id="mock-id") + "test_domain", + "test_service", + data={"entity_id": ENTITY_MATCH_ALL}, + context=ha.Context(user_id="mock-id"), ), ) @@ -407,7 +411,9 @@ async def test_call_no_context_target_all( hass, [Mock(entities=mock_entities)], Mock(), - ha.ServiceCall("test_domain", "test_service"), + ha.ServiceCall( + "test_domain", "test_service", data={"entity_id": ENTITY_MATCH_ALL} + ), ) assert len(mock_service_platform_call.mock_calls) == 1 @@ -453,14 +459,14 @@ async def test_call_with_match_all( mock_entities["light.living_room"], ] assert ( - "Not passing an entity ID to a service to target " "all entities is deprecated" + "Not passing an entity ID to a service to target all entities is deprecated" ) not in caplog.text async def test_call_with_omit_entity_id( - hass, mock_service_platform_call, mock_entities, caplog + hass, mock_service_platform_call, mock_entities ): - """Check we only target allowed entities if targetting all.""" + """Check service call if we do not pass an entity ID.""" await service.entity_service_call( hass, [Mock(entities=mock_entities)], @@ -470,13 +476,7 @@ async def test_call_with_omit_entity_id( assert len(mock_service_platform_call.mock_calls) == 1 entities = mock_service_platform_call.mock_calls[0][1][2] - assert entities == [ - mock_entities["light.kitchen"], - mock_entities["light.living_room"], - ] - assert ( - "Not passing an entity ID to a service to target " "all entities is deprecated" - ) in caplog.text + assert entities == [] async def test_register_admin_service(hass, hass_read_only_user, hass_admin_user): diff --git a/tests/helpers/test_state.py b/tests/helpers/test_state.py index 14bcbde5094..567bac65f5b 100644 --- a/tests/helpers/test_state.py +++ b/tests/helpers/test_state.py @@ -5,21 +5,22 @@ from unittest.mock import patch import pytest -import homeassistant.core as ha -from homeassistant.const import SERVICE_TURN_ON, SERVICE_TURN_OFF -from homeassistant.util import dt as dt_util -from homeassistant.helpers import state -from homeassistant.const import ( - STATE_OPEN, - STATE_CLOSED, - STATE_LOCKED, - STATE_UNLOCKED, - STATE_ON, - STATE_OFF, - STATE_HOME, - STATE_NOT_HOME, -) from homeassistant.components.sun import STATE_ABOVE_HORIZON, STATE_BELOW_HORIZON +from homeassistant.const import ( + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_CLOSED, + STATE_HOME, + STATE_LOCKED, + STATE_NOT_HOME, + STATE_OFF, + STATE_ON, + STATE_OPEN, + STATE_UNLOCKED, +) +import homeassistant.core as ha +from homeassistant.helpers import state +from homeassistant.util import dt as dt_util from tests.common import async_mock_service diff --git a/tests/helpers/test_storage.py b/tests/helpers/test_storage.py index 87a22fcb845..2fef58ad115 100644 --- a/tests/helpers/test_storage.py +++ b/tests/helpers/test_storage.py @@ -2,7 +2,7 @@ import asyncio from datetime import timedelta import json -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch import pytest @@ -12,7 +12,6 @@ from homeassistant.util import dt from tests.common import async_fire_time_changed, mock_coro - MOCK_VERSION = 1 MOCK_KEY = "storage-test" MOCK_DATA = {"hello": "world"} diff --git a/tests/helpers/test_sun.py b/tests/helpers/test_sun.py index 1746c7e6fc0..b8ecd1ed86a 100644 --- a/tests/helpers/test_sun.py +++ b/tests/helpers/test_sun.py @@ -1,11 +1,11 @@ """The tests for the Sun helpers.""" # pylint: disable=protected-access +from datetime import datetime, timedelta from unittest.mock import patch -from datetime import timedelta, datetime from homeassistant.const import SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET -import homeassistant.util.dt as dt_util import homeassistant.helpers.sun as sun +import homeassistant.util.dt as dt_util def test_next_events(hass): diff --git a/tests/helpers/test_temperature.py b/tests/helpers/test_temperature.py index 840e6fd5d9d..5808b661150 100644 --- a/tests/helpers/test_temperature.py +++ b/tests/helpers/test_temperature.py @@ -2,11 +2,11 @@ import pytest from homeassistant.const import ( - TEMP_CELSIUS, - PRECISION_WHOLE, - TEMP_FAHRENHEIT, PRECISION_HALVES, PRECISION_TENTHS, + PRECISION_WHOLE, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, ) from homeassistant.helpers.temperature import display_temp diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index b69fdb17e35..1c3afa472f2 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -1,13 +1,12 @@ """Test Home Assistant template helper methods.""" +from datetime import datetime import math import random -from datetime import datetime from unittest.mock import patch import pytest import pytz -import homeassistant.util.dt as dt_util from homeassistant.components import group from homeassistant.const import ( LENGTH_METERS, @@ -19,6 +18,7 @@ from homeassistant.const import ( ) from homeassistant.exceptions import TemplateError from homeassistant.helpers import template +import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import UnitSystem @@ -153,7 +153,7 @@ def test_iterating_all_states(hass): def test_iterating_domain_states(hass): """Test iterating domain states.""" - tmpl_str = "{% for state in states.sensor %}" "{{ state.state }}{% endfor %}" + tmpl_str = "{% for state in states.sensor %}{{ state.state }}{% endfor %}" info = render_to_info(hass, tmpl_str) assert_result_info(info, "", domains=["sensor"]) @@ -227,6 +227,13 @@ def test_rounding_value(hass): == "12.8" ) + assert ( + template.Template( + '{{ states.sensor.temperature.state | round(1, "half") }}', hass + ).async_render() + == "13.0" + ) + def test_rounding_value_get_original_value_on_error(hass): """Test rounding value get original value on error.""" @@ -811,7 +818,7 @@ def test_states_function(hass): @patch( - "homeassistant.helpers.template.TemplateEnvironment." "is_safe_callable", + "homeassistant.helpers.template.TemplateEnvironment.is_safe_callable", return_value=True, ) def test_now(mock_is_safe, hass): @@ -825,7 +832,7 @@ def test_now(mock_is_safe, hass): @patch( - "homeassistant.helpers.template.TemplateEnvironment." "is_safe_callable", + "homeassistant.helpers.template.TemplateEnvironment.is_safe_callable", return_value=True, ) def test_utcnow(mock_is_safe, hass): @@ -1253,18 +1260,18 @@ async def test_expand(hass): hass.states.async_set("test.object", "happy") info = render_to_info( - hass, "{{ expand('test.object') | map(attribute='entity_id')" " | join(', ') }}" + hass, "{{ expand('test.object') | map(attribute='entity_id') | join(', ') }}" ) assert_result_info(info, "test.object", []) info = render_to_info( hass, - "{{ expand('group.new_group') | map(attribute='entity_id')" " | join(', ') }}", + "{{ expand('group.new_group') | map(attribute='entity_id') | join(', ') }}", ) assert_result_info(info, "", ["group.new_group"]) info = render_to_info( - hass, "{{ expand(states.group) | map(attribute='entity_id')" " | join(', ') }}" + hass, "{{ expand(states.group) | map(attribute='entity_id') | join(', ') }}" ) assert_result_info(info, "", [], ["group"]) @@ -1272,12 +1279,12 @@ async def test_expand(hass): info = render_to_info( hass, - "{{ expand('group.new_group') | map(attribute='entity_id')" " | join(', ') }}", + "{{ expand('group.new_group') | map(attribute='entity_id') | join(', ') }}", ) assert_result_info(info, "test.object", ["group.new_group"]) info = render_to_info( - hass, "{{ expand(states.group) | map(attribute='entity_id')" " | join(', ') }}" + hass, "{{ expand(states.group) | map(attribute='entity_id') | join(', ') }}" ) assert_result_info(info, "test.object", ["group.new_group"], ["group"]) @@ -1430,7 +1437,7 @@ def test_closest_function_to_state(hass): assert ( template.Template( - "{{ closest(states.zone.far_away, " "states.test_domain).entity_id }}", hass + "{{ closest(states.zone.far_away, states.test_domain).entity_id }}", hass ).async_render() == "test_domain.closest_zone" ) @@ -1464,7 +1471,7 @@ def test_closest_function_state_with_invalid_location(hass): assert ( template.Template( - "{{ closest(states.test_domain.closest_home, " "states) }}", hass + "{{ closest(states.test_domain.closest_home, states) }}", hass ).async_render() == "None" ) @@ -1510,7 +1517,7 @@ def test_extract_entities_none_exclude_stuff(hass): assert ( template.extract_entities( - "{{ closest(states.zone.far_away, " "states.test_domain).entity_id }}" + "{{ closest(states.zone.far_away, states.test_domain).entity_id }}" ) == MATCH_ALL ) @@ -1789,3 +1796,10 @@ def test_length_of_states(hass): tpl = template.Template("{{ states.sensor | length }}", hass) assert tpl.async_render() == "2" + + +def test_render_complex_handling_non_template_values(hass): + """Test that we can render non-template fields.""" + assert template.render_complex( + {True: 1, False: template.Template("{{ hello }}", hass)}, {"hello": 2} + ) == {True: 1, False: "2"} diff --git a/tests/helpers/test_translation.py b/tests/helpers/test_translation.py index 1c3748250a5..6b846703914 100644 --- a/tests/helpers/test_translation.py +++ b/tests/helpers/test_translation.py @@ -5,9 +5,10 @@ from unittest.mock import patch import pytest +from homeassistant.generated import config_flows import homeassistant.helpers.translation as translation from homeassistant.setup import async_setup_component -from homeassistant.generated import config_flows + from tests.common import mock_coro diff --git a/tests/scripts/test_auth.py b/tests/scripts/test_auth.py index f762630a42a..3ab19450879 100644 --- a/tests/scripts/test_auth.py +++ b/tests/scripts/test_auth.py @@ -3,8 +3,8 @@ from unittest.mock import Mock, patch import pytest -from homeassistant.scripts import auth as script_auth from homeassistant.auth.providers import homeassistant as hass_auth +from homeassistant.scripts import auth as script_auth from tests.common import register_auth_provider diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index 8e1ffe63e84..ea7ae03b5db 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -2,8 +2,9 @@ import logging from unittest.mock import patch -import homeassistant.scripts.check_config as check_config from homeassistant.config import YAML_CONFIG_FILE +import homeassistant.scripts.check_config as check_config + from tests.common import get_test_config_dir, patch_yaml_files _LOGGER = logging.getLogger(__name__) @@ -19,7 +20,7 @@ BASE_CONFIG = ( "\n\n" ) -BAD_CORE_CONFIG = "homeassistant:\n" " unit_system: bad\n" "\n\n" +BAD_CORE_CONFIG = "homeassistant:\n unit_system: bad\n\n\n" def normalize_yaml_files(check_dict): @@ -91,8 +92,8 @@ def test_secrets(isfile_patch, loop): files = { get_test_config_dir(YAML_CONFIG_FILE): BASE_CONFIG - + ("http:\n" " cors_allowed_origins: !secret http_pw"), - secrets_path: ("logger: debug\n" "http_pw: http://google.com"), + + ("http:\n cors_allowed_origins: !secret http_pw"), + secrets_path: ("logger: debug\nhttp_pw: http://google.com"), } with patch_yaml_files(files): @@ -121,8 +122,7 @@ def test_secrets(isfile_patch, loop): def test_package_invalid(isfile_patch, loop): """Test a valid platform setup.""" files = { - YAML_CONFIG_FILE: BASE_CONFIG - + (" packages:\n" " p1:\n" ' group: ["a"]') + YAML_CONFIG_FILE: BASE_CONFIG + (" packages:\n p1:\n" ' group: ["a"]') } with patch_yaml_files(files): res = check_config.check(get_test_config_dir()) diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index eaabd61af4b..f71be8fe9b1 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -1,20 +1,20 @@ """Test the bootstrapping.""" # pylint: disable=protected-access import asyncio +import logging import os from unittest.mock import Mock, patch -import logging -import homeassistant.config as config_util from homeassistant import bootstrap +import homeassistant.config as config_util import homeassistant.util.dt as dt_util from tests.common import ( - patch_yaml_files, + MockModule, get_test_config_dir, mock_coro, mock_integration, - MockModule, + patch_yaml_files, ) ORIG_TIMEZONE = dt_util.DEFAULT_TIME_ZONE diff --git a/tests/test_config.py b/tests/test_config.py index 1c872369096..1abb6ec7ff1 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,39 +1,39 @@ """Test config utils.""" # pylint: disable=protected-access import asyncio +from collections import OrderedDict import copy import os import unittest.mock as mock -from collections import OrderedDict import asynctest import pytest -from voluptuous import MultipleInvalid, Invalid +from voluptuous import Invalid, MultipleInvalid import yaml -from homeassistant.core import SOURCE_STORAGE, HomeAssistantError import homeassistant.config as config_util -from homeassistant.loader import async_get_integration from homeassistant.const import ( + ATTR_ASSUMED_STATE, ATTR_FRIENDLY_NAME, ATTR_HIDDEN, - ATTR_ASSUMED_STATE, + CONF_AUTH_MFA_MODULES, + CONF_AUTH_PROVIDERS, + CONF_CUSTOMIZE, CONF_LATITUDE, CONF_LONGITUDE, - CONF_UNIT_SYSTEM, CONF_NAME, - CONF_CUSTOMIZE, - __version__, - CONF_UNIT_SYSTEM_METRIC, - CONF_UNIT_SYSTEM_IMPERIAL, CONF_TEMPERATURE_UNIT, - CONF_AUTH_PROVIDERS, - CONF_AUTH_MFA_MODULES, + CONF_UNIT_SYSTEM, + CONF_UNIT_SYSTEM_IMPERIAL, + CONF_UNIT_SYSTEM_METRIC, + __version__, ) +from homeassistant.core import SOURCE_STORAGE, HomeAssistantError +import homeassistant.helpers.check_config as check_config +from homeassistant.helpers.entity import Entity +from homeassistant.loader import async_get_integration from homeassistant.util import dt as dt_util from homeassistant.util.yaml import SECRET_YAML -from homeassistant.helpers.entity import Entity -import homeassistant.helpers.check_config as check_config from tests.common import get_test_config_dir, patch_yaml_files diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index d9dd614c9a5..5b694b2de87 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -12,14 +12,14 @@ from homeassistant.setup import async_setup_component from homeassistant.util import dt from tests.common import ( - MockModule, - mock_coro, MockConfigEntry, - async_fire_time_changed, - MockPlatform, MockEntity, - mock_integration, + MockModule, + MockPlatform, + async_fire_time_changed, + mock_coro, mock_entity_platform, + mock_integration, mock_registry, ) @@ -434,8 +434,8 @@ async def test_saving_and_loading(hass): VERSION = 5 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL - @asyncio.coroutine - def async_step_user(self, user_input=None): + async def async_step_user(self, user_input=None): + await self.async_set_unique_id("unique") return self.async_create_entry(title="Test Title", data={"token": "abcd"}) with patch.dict(config_entries.HANDLERS, {"test": TestFlow}): @@ -477,6 +477,7 @@ async def test_saving_and_loading(hass): assert orig.data == loaded.data assert orig.source == loaded.source assert orig.connection_class == loaded.connection_class + assert orig.unique_id == loaded.unique_id async def test_forward_entry_sets_up_component(hass): @@ -1001,3 +1002,304 @@ async def test_reload_entry_entity_registry_works(hass): await hass.async_block_till_done() assert len(mock_unload_entry.mock_calls) == 1 + + +async def test_unqiue_id_persisted(hass, manager): + """Test that a unique ID is stored in the config entry.""" + mock_setup_entry = MagicMock(return_value=mock_coro(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): + + VERSION = 1 + + async def async_step_user(self, user_input=None): + await self.async_set_unique_id("mock-unique-id") + return self.async_create_entry(title="mock-title", data={}) + + with patch.dict(config_entries.HANDLERS, {"comp": TestFlow}): + await manager.flow.async_init( + "comp", context={"source": config_entries.SOURCE_USER} + ) + + 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.unique_id == "mock-unique-id" + + +async def test_unique_id_existing_entry(hass, manager): + """Test that we remove an entry if there already is an entry with unique ID.""" + hass.config.components.add("comp") + MockConfigEntry( + domain="comp", + state=config_entries.ENTRY_STATE_LOADED, + unique_id="mock-unique-id", + ).add_to_hass(hass) + + async_setup_entry = MagicMock(side_effect=lambda _, _2: mock_coro(True)) + async_unload_entry = MagicMock(side_effect=lambda _, _2: mock_coro(True)) + async_remove_entry = MagicMock(side_effect=lambda _, _2: mock_coro(True)) + + mock_integration( + hass, + MockModule( + "comp", + async_setup_entry=async_setup_entry, + async_unload_entry=async_unload_entry, + async_remove_entry=async_remove_entry, + ), + ) + mock_entity_platform(hass, "config_flow.comp", None) + + class TestFlow(config_entries.ConfigFlow): + + VERSION = 1 + + async def async_step_user(self, user_input=None): + existing_entry = await self.async_set_unique_id("mock-unique-id") + + assert existing_entry is not None + + return self.async_create_entry(title="mock-title", data={"via": "flow"}) + + with patch.dict(config_entries.HANDLERS, {"comp": TestFlow}): + result = await manager.flow.async_init( + "comp", context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + entries = hass.config_entries.async_entries("comp") + assert len(entries) == 1 + assert entries[0].data == {"via": "flow"} + + assert len(async_setup_entry.mock_calls) == 1 + assert len(async_unload_entry.mock_calls) == 1 + assert len(async_remove_entry.mock_calls) == 1 + + +async def test_unique_id_in_progress(hass, manager): + """Test that we abort if there is already a flow in progress with same unique id.""" + mock_integration(hass, MockModule("comp")) + mock_entity_platform(hass, "config_flow.comp", None) + + class TestFlow(config_entries.ConfigFlow): + + VERSION = 1 + + async def async_step_user(self, user_input=None): + await self.async_set_unique_id("mock-unique-id") + return self.async_show_form(step_id="discovery") + + with patch.dict(config_entries.HANDLERS, {"comp": TestFlow}): + # Create one to be in progress + result = await manager.flow.async_init( + "comp", context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + # Will be canceled + result2 = await manager.flow.async_init( + "comp", context={"source": config_entries.SOURCE_USER} + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["reason"] == "already_in_progress" + + +async def test_finish_flow_aborts_progress(hass, manager): + """Test that when finishing a flow, we abort other flows in progress with unique ID.""" + mock_integration( + hass, + MockModule("comp", async_setup_entry=MagicMock(return_value=mock_coro(True))), + ) + mock_entity_platform(hass, "config_flow.comp", None) + + class TestFlow(config_entries.ConfigFlow): + + VERSION = 1 + + async def async_step_user(self, user_input=None): + await self.async_set_unique_id("mock-unique-id", raise_on_progress=False) + + if user_input is None: + return self.async_show_form(step_id="discovery") + + return self.async_create_entry(title="yo", data={}) + + with patch.dict(config_entries.HANDLERS, {"comp": TestFlow}): + # Create one to be in progress + result = await manager.flow.async_init( + "comp", context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + # Will finish and cancel other one. + result2 = await manager.flow.async_init( + "comp", context={"source": config_entries.SOURCE_USER}, data={} + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + assert len(hass.config_entries.flow.async_progress()) == 0 + + +async def test_unique_id_ignore(hass, manager): + """Test that we can ignore flows that are in progress and have a unique ID.""" + async_setup_entry = MagicMock(return_value=mock_coro(False)) + mock_integration(hass, MockModule("comp", async_setup_entry=async_setup_entry)) + mock_entity_platform(hass, "config_flow.comp", None) + + class TestFlow(config_entries.ConfigFlow): + + VERSION = 1 + + async def async_step_user(self, user_input=None): + await self.async_set_unique_id("mock-unique-id") + return self.async_show_form(step_id="discovery") + + with patch.dict(config_entries.HANDLERS, {"comp": TestFlow}): + # Create one to be in progress + result = await manager.flow.async_init( + "comp", context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + result2 = await manager.flow.async_init( + "comp", + context={"source": config_entries.SOURCE_IGNORE}, + data={"unique_id": "mock-unique-id"}, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + # assert len(hass.config_entries.flow.async_progress()) == 0 + + # We should never set up an ignored entry. + assert len(async_setup_entry.mock_calls) == 0 + + entry = hass.config_entries.async_entries("comp")[0] + + assert entry.source == "ignore" + assert entry.unique_id == "mock-unique-id" + + +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 = MagicMock(return_value=mock_coro(True)) + mock_integration(hass, MockModule("comp", async_setup_entry=async_setup_entry)) + mock_entity_platform(hass, "config_flow.comp", None) + + class TestFlow(config_entries.ConfigFlow): + + VERSION = 1 + + async def async_step_unignore(self, user_input): + unique_id = user_input["unique_id"] + await self.async_set_unique_id(unique_id) + return self.async_show_form(step_id="discovery") + + with patch.dict(config_entries.HANDLERS, {"comp": TestFlow}): + result = await manager.flow.async_init( + "comp", + context={"source": config_entries.SOURCE_IGNORE}, + data={"unique_id": "mock-unique-id"}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + entry = hass.config_entries.async_entries("comp")[0] + assert entry.source == "ignore" + assert entry.unique_id == "mock-unique-id" + assert entry.domain == "comp" + + await manager.async_remove(entry.entry_id) + + # Right after removal there shouldn't be an entry or active flows + assert len(hass.config_entries.async_entries("comp")) == 0 + assert len(hass.config_entries.flow.async_progress()) == 0 + + # But after a 'tick' the unignore step has run and we can see an active flow again. + await hass.async_block_till_done() + assert len(hass.config_entries.flow.async_progress()) == 1 + + # and still not config entries + assert len(hass.config_entries.async_entries("comp")) == 0 + + +async def test_unignore_create_entry(hass, manager): + """Test that we can ignore flows that are in progress and have a unique ID, then rediscover them.""" + async_setup_entry = MagicMock(return_value=mock_coro(True)) + mock_integration(hass, MockModule("comp", async_setup_entry=async_setup_entry)) + mock_entity_platform(hass, "config_flow.comp", None) + + class TestFlow(config_entries.ConfigFlow): + + VERSION = 1 + + async def async_step_unignore(self, user_input): + unique_id = user_input["unique_id"] + await self.async_set_unique_id(unique_id) + return self.async_create_entry(title="yo", data={}) + + with patch.dict(config_entries.HANDLERS, {"comp": TestFlow}): + result = await manager.flow.async_init( + "comp", + context={"source": config_entries.SOURCE_IGNORE}, + data={"unique_id": "mock-unique-id"}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + entry = hass.config_entries.async_entries("comp")[0] + assert entry.source == "ignore" + assert entry.unique_id == "mock-unique-id" + assert entry.domain == "comp" + + await manager.async_remove(entry.entry_id) + + # Right after removal there shouldn't be an entry or flow + assert len(hass.config_entries.flow.async_progress()) == 0 + assert len(hass.config_entries.async_entries("comp")) == 0 + + # But after a 'tick' the unignore step has run and we can see a config entry. + await hass.async_block_till_done() + entry = hass.config_entries.async_entries("comp")[0] + assert entry.source == "unignore" + assert entry.unique_id == "mock-unique-id" + assert entry.title == "yo" + + # And still no active flow + assert len(hass.config_entries.flow.async_progress()) == 0 + + +async def test_unignore_default_impl(hass, manager): + """Test that resdicovery is a no-op by default.""" + async_setup_entry = MagicMock(return_value=mock_coro(True)) + mock_integration(hass, MockModule("comp", async_setup_entry=async_setup_entry)) + mock_entity_platform(hass, "config_flow.comp", None) + + class TestFlow(config_entries.ConfigFlow): + + VERSION = 1 + + with patch.dict(config_entries.HANDLERS, {"comp": TestFlow}): + result = await manager.flow.async_init( + "comp", + context={"source": config_entries.SOURCE_IGNORE}, + data={"unique_id": "mock-unique-id"}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + entry = hass.config_entries.async_entries("comp")[0] + assert entry.source == "ignore" + assert entry.unique_id == "mock-unique-id" + assert entry.domain == "comp" + + await manager.async_remove(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.config_entries.async_entries("comp")) == 0 + assert len(hass.config_entries.flow.async_progress()) == 0 diff --git a/tests/test_core.py b/tests/test_core.py index 5ac13027f28..1999065e31d 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,40 +1,40 @@ """Test to verify that Home Assistant core works.""" # pylint: disable=protected-access import asyncio +from datetime import datetime, timedelta import functools import logging import os -import unittest -from unittest.mock import patch, MagicMock -from datetime import datetime, timedelta from tempfile import TemporaryDirectory +import unittest +from unittest.mock import MagicMock, patch -import voluptuous as vol -import pytz import pytest +import pytz +import voluptuous as vol +from homeassistant.const import ( + ATTR_FRIENDLY_NAME, + ATTR_NOW, + ATTR_SECONDS, + CONF_UNIT_SYSTEM, + EVENT_CALL_SERVICE, + EVENT_CORE_CONFIG_UPDATE, + EVENT_HOMEASSISTANT_CLOSE, + EVENT_HOMEASSISTANT_STOP, + EVENT_SERVICE_REGISTERED, + EVENT_SERVICE_REMOVED, + EVENT_STATE_CHANGED, + EVENT_TIME_CHANGED, + EVENT_TIMER_OUT_OF_SYNC, + __version__, +) import homeassistant.core as ha from homeassistant.exceptions import InvalidEntityFormatError, InvalidStateError import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import METRIC_SYSTEM -from homeassistant.const import ( - __version__, - EVENT_STATE_CHANGED, - ATTR_FRIENDLY_NAME, - CONF_UNIT_SYSTEM, - ATTR_NOW, - EVENT_TIME_CHANGED, - EVENT_TIMER_OUT_OF_SYNC, - ATTR_SECONDS, - EVENT_HOMEASSISTANT_STOP, - EVENT_HOMEASSISTANT_CLOSE, - EVENT_SERVICE_REGISTERED, - EVENT_SERVICE_REMOVED, - EVENT_CALL_SERVICE, - EVENT_CORE_CONFIG_UPDATE, -) -from tests.common import get_test_home_assistant, async_mock_service +from tests.common import async_mock_service, get_test_home_assistant PST = pytz.timezone("America/Los_Angeles") diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py index 175efebd755..a6bdd2b5cb6 100644 --- a/tests/test_data_entry_flow.py +++ b/tests/test_data_entry_flow.py @@ -94,7 +94,7 @@ async def test_configure_two_steps(manager): async def test_show_form(manager): - """Test that abort removes the flow from progress.""" + """Test that we can show a form.""" schema = vol.Schema({vol.Required("username"): str, vol.Required("password"): str}) @manager.mock_reg_handler("test") @@ -271,3 +271,17 @@ async def test_external_step(hass, manager): result = await manager.async_configure(result["flow_id"]) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == "Hello" + + +async def test_abort_flow_exception(manager): + """Test that the AbortFlow exception works.""" + + @manager.mock_reg_handler("test") + class TestFlow(data_entry_flow.FlowHandler): + async def async_step_init(self, user_input=None): + raise data_entry_flow.AbortFlow("mock-reason", {"placeholder": "yo"}) + + form = await manager.async_init("test") + assert form["type"] == "abort" + assert form["reason"] == "mock-reason" + assert form["description_placeholders"] == {"placeholder": "yo"} diff --git a/tests/test_loader.py b/tests/test_loader.py index e7011997f73..f3e7c3bd884 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -2,9 +2,9 @@ from asynctest.mock import ANY, patch import pytest -import homeassistant.loader as loader from homeassistant.components import http, hue from homeassistant.components.hue import light as hue_light +import homeassistant.loader as loader from tests.common import MockModule, async_mock_service, mock_integration diff --git a/tests/test_main.py b/tests/test_main.py index 29454d269af..5ec6460301f 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,5 +1,5 @@ """Test methods in __main__.""" -from unittest.mock import patch, PropertyMock +from unittest.mock import PropertyMock, patch from homeassistant import __main__ as main from homeassistant.const import REQUIRED_PYTHON_VER diff --git a/tests/test_requirements.py b/tests/test_requirements.py index 2627a077a87..e95db4e533d 100644 --- a/tests/test_requirements.py +++ b/tests/test_requirements.py @@ -1,20 +1,26 @@ """Test requirements module.""" import os from pathlib import Path -from unittest.mock import patch, call -from pytest import raises +from unittest.mock import call, patch -from homeassistant import setup +import pytest + +from homeassistant import loader, setup from homeassistant.requirements import ( CONSTRAINT_FILE, + PROGRESS_FILE, + RequirementsNotFound, + _install, async_get_integration_with_requirements, async_process_requirements, - PROGRESS_FILE, - _install, - RequirementsNotFound, ) -from tests.common import get_test_home_assistant, MockModule, mock_integration +from tests.common import ( + MockModule, + get_test_home_assistant, + mock_coro, + mock_integration, +) def env_without_wheel_links(): @@ -103,7 +109,7 @@ async def test_install_missing_package(hass): with patch( "homeassistant.util.package.install_package", return_value=False ) as mock_inst: - with raises(RequirementsNotFound): + with pytest.raises(RequirementsNotFound): await async_process_requirements(hass, "test_component", ["hello==1.0.0"]) assert len(mock_inst.mock_calls) == 1 @@ -221,3 +227,44 @@ async def test_progress_lock(hass): _install(hass, "hello", kwargs) assert not progress_path.exists() + + +async def test_discovery_requirements_ssdp(hass): + """Test that we load discovery requirements.""" + hass.config.skip_pip = False + ssdp = await loader.async_get_integration(hass, "ssdp") + + mock_integration( + hass, MockModule("ssdp_comp", partial_manifest={"ssdp": [{"st": "roku:ecp"}]}) + ) + with patch( + "homeassistant.requirements.async_process_requirements", + side_effect=lambda _, _2, _3: mock_coro(), + ) as mock_process: + await async_get_integration_with_requirements(hass, "ssdp_comp") + + assert len(mock_process.mock_calls) == 1 + assert mock_process.mock_calls[0][1][2] == ssdp.requirements + + +@pytest.mark.parametrize( + "partial_manifest", + [{"zeroconf": ["_googlecast._tcp.local."]}, {"homekit": {"models": ["LIFX"]}}], +) +async def test_discovery_requirements_zeroconf(hass, partial_manifest): + """Test that we load discovery requirements.""" + hass.config.skip_pip = False + zeroconf = await loader.async_get_integration(hass, "zeroconf") + + mock_integration( + hass, MockModule("comp", partial_manifest=partial_manifest), + ) + + with patch( + "homeassistant.requirements.async_process_requirements", + side_effect=lambda _, _2, _3: mock_coro(), + ) as mock_process: + await async_get_integration_with_requirements(hass, "comp") + + assert len(mock_process.mock_calls) == 2 # zeroconf also depends on http + assert mock_process.mock_calls[0][1][2] == zeroconf.requirements diff --git a/tests/test_setup.py b/tests/test_setup.py index 8fd25091eb6..c19d92db4b6 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -1,32 +1,32 @@ """Test component/platform setup.""" # pylint: disable=protected-access import asyncio -import os -from unittest import mock -import threading import logging +import os +import threading +from unittest import mock import voluptuous as vol -from homeassistant.core import callback -from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_COMPONENT_LOADED -import homeassistant.config as config_util from homeassistant import setup -import homeassistant.util.dt as dt_util +import homeassistant.config as config_util +from homeassistant.const import EVENT_COMPONENT_LOADED, EVENT_HOMEASSISTANT_START +from homeassistant.core import callback +from homeassistant.helpers import discovery from homeassistant.helpers.config_validation import ( PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) -from homeassistant.helpers import discovery +import homeassistant.util.dt as dt_util from tests.common import ( - get_test_home_assistant, MockModule, MockPlatform, assert_setup_component, get_test_config_dir, - mock_integration, + get_test_home_assistant, mock_entity_platform, + mock_integration, ) ORIG_TIMEZONE = dt_util.DEFAULT_TIME_ZONE diff --git a/tests/test_util/aiohttp.py b/tests/test_util/aiohttp.py index ce13ca5a594..e0e1ded32c3 100644 --- a/tests/test_util/aiohttp.py +++ b/tests/test_util/aiohttp.py @@ -7,11 +7,10 @@ from unittest import mock from urllib.parse import parse_qs from aiohttp import ClientSession +from aiohttp.client_exceptions import ClientResponseError from aiohttp.streams import StreamReader from yarl import URL -from aiohttp.client_exceptions import ClientResponseError - from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE retype = type(re.compile("")) diff --git a/tests/test_util/test_aiohttp.py b/tests/test_util/test_aiohttp.py index 0a0ead54dca..2761f5b9ea3 100644 --- a/tests/test_util/test_aiohttp.py +++ b/tests/test_util/test_aiohttp.py @@ -1,8 +1,8 @@ """Tests for our aiohttp mocker.""" -from .aiohttp import AiohttpClientMocker - import pytest +from .aiohttp import AiohttpClientMocker + async def test_matching_url(): """Test we can match urls.""" 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 0e2842f8695..065ea3e3980 100644 --- a/tests/testing_config/custom_components/test/alarm_control_panel.py +++ b/tests/testing_config/custom_components/test/alarm_control_panel.py @@ -4,6 +4,12 @@ Provide a mock alarm_control_panel platform. Call init before using it in your tests to ensure clean test data. """ from homeassistant.components.alarm_control_panel import AlarmControlPanel +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, + SUPPORT_ALARM_TRIGGER, +) from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, @@ -11,6 +17,7 @@ from homeassistant.const import ( STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, ) + from tests.common import MockEntity ENTITIES = {} @@ -64,6 +71,16 @@ class MockAlarm(MockEntity, AlarmControlPanel): """Return the state of the device.""" return self._state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return ( + SUPPORT_ALARM_ARM_HOME + | SUPPORT_ALARM_ARM_AWAY + | SUPPORT_ALARM_ARM_NIGHT + | SUPPORT_ALARM_TRIGGER + ) + def alarm_arm_away(self, code=None): """Send arm away command.""" self._state = STATE_ALARM_ARMED_AWAY diff --git a/tests/testing_config/custom_components/test/binary_sensor.py b/tests/testing_config/custom_components/test/binary_sensor.py index 5052b8e47f1..bcff0adb4e4 100644 --- a/tests/testing_config/custom_components/test/binary_sensor.py +++ b/tests/testing_config/custom_components/test/binary_sensor.py @@ -3,9 +3,9 @@ Provide a mock binary sensor platform. Call init before using it in your tests to ensure clean test data. """ -from homeassistant.components.binary_sensor import BinarySensorDevice, DEVICE_CLASSES -from tests.common import MockEntity +from homeassistant.components.binary_sensor import DEVICE_CLASSES, BinarySensorDevice +from tests.common import MockEntity ENTITIES = {} diff --git a/tests/testing_config/custom_components/test/cover.py b/tests/testing_config/custom_components/test/cover.py index d7c771e2b28..ce5462790bb 100644 --- a/tests/testing_config/custom_components/test/cover.py +++ b/tests/testing_config/custom_components/test/cover.py @@ -4,8 +4,8 @@ Provide a mock cover platform. Call init before using it in your tests to ensure clean test data. """ from homeassistant.components.cover import CoverDevice -from tests.common import MockEntity +from tests.common import MockEntity ENTITIES = {} diff --git a/tests/testing_config/custom_components/test/light.py b/tests/testing_config/custom_components/test/light.py index 0a48388b718..4b018adb5cb 100644 --- a/tests/testing_config/custom_components/test/light.py +++ b/tests/testing_config/custom_components/test/light.py @@ -3,9 +3,9 @@ Provide a mock light platform. Call init before using it in your tests to ensure clean test data. """ -from homeassistant.const import STATE_ON, STATE_OFF -from tests.common import MockToggleEntity +from homeassistant.const import STATE_OFF, STATE_ON +from tests.common import MockToggleEntity ENTITIES = [] diff --git a/tests/testing_config/custom_components/test/lock.py b/tests/testing_config/custom_components/test/lock.py index db6ce38b097..24b04903541 100644 --- a/tests/testing_config/custom_components/test/lock.py +++ b/tests/testing_config/custom_components/test/lock.py @@ -3,7 +3,8 @@ Provide a mock lock platform. Call init before using it in your tests to ensure clean test data. """ -from homeassistant.components.lock import LockDevice, SUPPORT_OPEN +from homeassistant.components.lock import SUPPORT_OPEN, LockDevice + from tests.common import MockEntity ENTITIES = {} diff --git a/tests/testing_config/custom_components/test/sensor.py b/tests/testing_config/custom_components/test/sensor.py index 651ee17bd65..26497b16a16 100644 --- a/tests/testing_config/custom_components/test/sensor.py +++ b/tests/testing_config/custom_components/test/sensor.py @@ -4,8 +4,8 @@ Provide a mock sensor platform. Call init before using it in your tests to ensure clean test data. """ import homeassistant.components.sensor as sensor -from tests.common import MockEntity +from tests.common import MockEntity DEVICE_CLASSES = list(sensor.DEVICE_CLASSES) DEVICE_CLASSES.append("none") diff --git a/tests/testing_config/custom_components/test/switch.py b/tests/testing_config/custom_components/test/switch.py index 484c47d1190..7dd1862d88f 100644 --- a/tests/testing_config/custom_components/test/switch.py +++ b/tests/testing_config/custom_components/test/switch.py @@ -3,9 +3,9 @@ Provide a mock switch platform. Call init before using it in your tests to ensure clean test data. """ -from homeassistant.const import STATE_ON, STATE_OFF -from tests.common import MockToggleEntity +from homeassistant.const import STATE_OFF, STATE_ON +from tests.common import MockToggleEntity ENTITIES = [] diff --git a/tests/testing_config/custom_components/test_package/__init__.py b/tests/testing_config/custom_components/test_package/__init__.py index f5cd2c34edf..44f62380c92 100644 --- a/tests/testing_config/custom_components/test_package/__init__.py +++ b/tests/testing_config/custom_components/test_package/__init__.py @@ -1,7 +1,6 @@ """Provide a mock package component.""" from .const import TEST # noqa: F401 - DOMAIN = "test_package" diff --git a/tests/util/test_async.py b/tests/util/test_async.py index 9cda40c1b8b..098b04a3048 100644 --- a/tests/util/test_async.py +++ b/tests/util/test_async.py @@ -1,8 +1,7 @@ """Tests for async util methods from Python source.""" import asyncio -import sys -from unittest.mock import MagicMock, patch from unittest import TestCase +from unittest.mock import MagicMock, patch import pytest @@ -112,11 +111,7 @@ class RunThreadsafeTests(TestCase): """Wait 0.05 second and return a + b.""" yield from asyncio.sleep(0.05, loop=self.loop) if cancel: - if sys.version_info[:2] >= (3, 7): - current_task = asyncio.current_task - else: - current_task = asyncio.tasks.Task.current_task - current_task(self.loop).cancel() + asyncio.current_task(self.loop).cancel() yield return self.add_callback(a, b, fail, invalid) diff --git a/tests/util/test_distance.py b/tests/util/test_distance.py index 581257d6f49..27b77a883c7 100644 --- a/tests/util/test_distance.py +++ b/tests/util/test_distance.py @@ -2,13 +2,13 @@ import pytest -import homeassistant.util.distance as distance_util from homeassistant.const import ( + LENGTH_FEET, LENGTH_KILOMETERS, LENGTH_METERS, - LENGTH_FEET, LENGTH_MILES, ) +import homeassistant.util.distance as distance_util INVALID_SYMBOL = "bob" VALID_SYMBOL = LENGTH_KILOMETERS diff --git a/tests/util/test_init.py b/tests/util/test_init.py index 261280cc20e..2ffca07082b 100644 --- a/tests/util/test_init.py +++ b/tests/util/test_init.py @@ -1,6 +1,6 @@ """Test Home Assistant util methods.""" -from unittest.mock import patch, MagicMock from datetime import datetime, timedelta +from unittest.mock import MagicMock, patch import pytest diff --git a/tests/util/test_json.py b/tests/util/test_json.py index bec3230c01e..26245482c2f 100644 --- a/tests/util/test_json.py +++ b/tests/util/test_json.py @@ -1,16 +1,15 @@ """Test Home Assistant json utility functions.""" from json import JSONEncoder import os -import unittest -from unittest.mock import Mock import sys from tempfile import mkdtemp +import unittest +from unittest.mock import Mock import pytest -from homeassistant.util.json import SerializationError, load_json, save_json from homeassistant.exceptions import HomeAssistantError - +from homeassistant.util.json import SerializationError, load_json, save_json # Test data that can be saved as JSON TEST_JSON_A = {"a": 1, "B": "two"} diff --git a/tests/util/test_location.py b/tests/util/test_location.py index 4908018410b..6dd6eafca1d 100644 --- a/tests/util/test_location.py +++ b/tests/util/test_location.py @@ -1,5 +1,5 @@ """Test Home Assistant location util methods.""" -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch import aiohttp import pytest diff --git a/tests/util/test_package.py b/tests/util/test_package.py index 79676cbfcd9..ca4ed83734a 100644 --- a/tests/util/test_package.py +++ b/tests/util/test_package.py @@ -2,8 +2,8 @@ import asyncio import logging import os -import sys from subprocess import PIPE +import sys from unittest.mock import MagicMock, call, patch import pkg_resources @@ -11,7 +11,6 @@ import pytest import homeassistant.util.package as package - RESOURCE_DIR = os.path.abspath( os.path.join(os.path.dirname(__file__), "..", "resources") ) diff --git a/tests/util/test_pressure.py b/tests/util/test_pressure.py index 18fa238e43b..df65618dc48 100644 --- a/tests/util/test_pressure.py +++ b/tests/util/test_pressure.py @@ -2,10 +2,10 @@ import pytest from homeassistant.const import ( - PRESSURE_PA, PRESSURE_HPA, - PRESSURE_MBAR, PRESSURE_INHG, + PRESSURE_MBAR, + PRESSURE_PA, PRESSURE_PSI, ) import homeassistant.util.pressure as pressure_util diff --git a/tests/util/test_ruamel_yaml.py b/tests/util/test_ruamel_yaml.py index 9ae806f31ea..79ed4a4f4d1 100644 --- a/tests/util/test_ruamel_yaml.py +++ b/tests/util/test_ruamel_yaml.py @@ -1,14 +1,13 @@ """Test Home Assistant ruamel.yaml loader.""" import os from tempfile import mkdtemp -import pytest +import pytest from ruamel.yaml import YAML from homeassistant.exceptions import HomeAssistantError import homeassistant.util.ruamel_yaml as util_yaml - TEST_YAML_A = """\ title: My Awesome Home # Include external resources diff --git a/tests/util/test_unit_system.py b/tests/util/test_unit_system.py index 827143bc447..e0e4524a2f2 100644 --- a/tests/util/test_unit_system.py +++ b/tests/util/test_unit_system.py @@ -1,20 +1,20 @@ """Test the unit system helper.""" import pytest -from homeassistant.util.unit_system import UnitSystem, METRIC_SYSTEM, IMPERIAL_SYSTEM from homeassistant.const import ( - LENGTH_METERS, - LENGTH_KILOMETERS, - MASS_GRAMS, - PRESSURE_PA, - VOLUME_LITERS, - TEMP_CELSIUS, LENGTH, + LENGTH_KILOMETERS, + LENGTH_METERS, MASS, + MASS_GRAMS, PRESSURE, + PRESSURE_PA, + TEMP_CELSIUS, TEMPERATURE, VOLUME, + VOLUME_LITERS, ) +from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM, UnitSystem SYSTEM_NAME = "TEST" INVALID_UNIT = "INVALID" diff --git a/tests/util/test_volume.py b/tests/util/test_volume.py index 4bf9b7c075d..9bd3e4b1a98 100644 --- a/tests/util/test_volume.py +++ b/tests/util/test_volume.py @@ -2,13 +2,13 @@ import pytest -import homeassistant.util.volume as volume_util from homeassistant.const import ( + VOLUME_FLUID_OUNCE, + VOLUME_GALLONS, VOLUME_LITERS, VOLUME_MILLILITERS, - VOLUME_GALLONS, - VOLUME_FLUID_OUNCE, ) +import homeassistant.util.volume as volume_util INVALID_SYMBOL = "bob" VALID_SYMBOL = VOLUME_LITERS diff --git a/tests/util/test_yaml.py b/tests/util/test_yaml.py index 1e5797e33e7..622d87d1a27 100644 --- a/tests/util/test_yaml.py +++ b/tests/util/test_yaml.py @@ -1,16 +1,17 @@ """Test Home Assistant yaml loader.""" import io +import logging import os import unittest -import logging from unittest.mock import patch import pytest -from homeassistant.exceptions import HomeAssistantError -from homeassistant.util.yaml import loader as yaml_loader -import homeassistant.util.yaml as yaml from homeassistant.config import YAML_CONFIG_FILE, load_yaml_config_file +from homeassistant.exceptions import HomeAssistantError +import homeassistant.util.yaml as yaml +from homeassistant.util.yaml import loader as yaml_loader + from tests.common import get_test_config_dir, patch_yaml_files @@ -391,7 +392,7 @@ class TestSecrets(unittest.TestCase): with pytest.raises(HomeAssistantError): load_yaml( os.path.join(self._sub_folder_path, "sub.yaml"), - "http:\n" " api_password: !secret test", + "http:\n api_password: !secret test", ) def test_secrets_keyring(self): @@ -430,9 +431,9 @@ class TestSecrets(unittest.TestCase): def test_secrets_are_not_dict(self): """Did secrets handle non-dict file.""" - FILES[self._secret_path] = ( - "- http_pw: pwhttp\n" " comp1_un: un1\n" " comp1_pw: pw1\n" - ) + 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( diff --git a/tox.ini b/tox.ini index dc2a9f79b90..17253e1d1e1 100644 --- a/tox.ini +++ b/tox.ini @@ -37,6 +37,7 @@ commands = python -m script.gen_requirements_all validate python -m script.hassfest validate pre-commit run flake8 {posargs: --all-files} + pre-commit run bandit {posargs: --all-files} [testenv:typing] deps =