diff --git a/.coveragerc b/.coveragerc index aa8f2d8c03d..748ca511dd5 100644 --- a/.coveragerc +++ b/.coveragerc @@ -10,7 +10,15 @@ omit = homeassistant/util/async.py # omit pieces of code that rely on external devices being present - homeassistant/components/abode/* + homeassistant/components/abode/__init__.py + homeassistant/components/abode/alarm_control_panel.py + homeassistant/components/abode/binary_sensor.py + homeassistant/components/abode/camera.py + homeassistant/components/abode/cover.py + homeassistant/components/abode/light.py + homeassistant/components/abode/lock.py + homeassistant/components/abode/sensor.py + homeassistant/components/abode/switch.py homeassistant/components/acer_projector/switch.py homeassistant/components/actiontec/device_tracker.py homeassistant/components/adguard/__init__.py @@ -19,6 +27,10 @@ omit = homeassistant/components/adguard/switch.py homeassistant/components/ads/* homeassistant/components/aftership/sensor.py + homeassistant/components/airly/__init__.py + homeassistant/components/airly/air_quality.py + homeassistant/components/airly/sensor.py + homeassistant/components/airly/const.py homeassistant/components/airvisual/sensor.py homeassistant/components/aladdin_connect/cover.py homeassistant/components/alarm_control_panel/manual_mqtt.py @@ -88,6 +100,7 @@ omit = homeassistant/components/bt_home_hub_5/device_tracker.py homeassistant/components/bt_smarthub/device_tracker.py homeassistant/components/buienradar/sensor.py + homeassistant/components/buienradar/util.py homeassistant/components/buienradar/weather.py homeassistant/components/caldav/calendar.py homeassistant/components/canary/alarm_control_panel.py @@ -113,7 +126,9 @@ omit = homeassistant/components/comfoconnect/* homeassistant/components/concord232/alarm_control_panel.py homeassistant/components/concord232/binary_sensor.py + homeassistant/components/coolmaster/__init__.py homeassistant/components/coolmaster/climate.py + homeassistant/components/coolmaster/const.py homeassistant/components/cppm_tracker/device_tracker.py homeassistant/components/cpuspeed/sensor.py homeassistant/components/crimereports/sensor.py @@ -221,6 +236,7 @@ omit = homeassistant/components/fortios/device_tracker.py homeassistant/components/fortigate/* homeassistant/components/foscam/camera.py + homeassistant/components/foscam/const.py homeassistant/components/foursquare/* homeassistant/components/free_mobile/notify.py homeassistant/components/freebox/* @@ -240,6 +256,7 @@ omit = homeassistant/components/github/sensor.py homeassistant/components/gitlab_ci/sensor.py homeassistant/components/gitter/sensor.py + homeassistant/components/glances/__init__.py homeassistant/components/glances/sensor.py homeassistant/components/gntp/notify.py homeassistant/components/goalfeed/* @@ -271,7 +288,6 @@ omit = homeassistant/components/heatmiser/climate.py homeassistant/components/hikvision/binary_sensor.py homeassistant/components/hikvisioncam/switch.py - homeassistant/components/hipchat/notify.py homeassistant/components/hitron_coda/device_tracker.py homeassistant/components/hive/* homeassistant/components/hlk_sw16/* @@ -279,7 +295,6 @@ omit = homeassistant/components/homematic/climate.py homeassistant/components/homematic/cover.py homeassistant/components/homematic/notify.py - homeassistant/components/homematicip_cloud/* homeassistant/components/homeworks/* homeassistant/components/honeywell/climate.py homeassistant/components/hook/switch.py @@ -405,6 +420,7 @@ omit = homeassistant/components/mpchc/media_player.py homeassistant/components/mpd/media_player.py homeassistant/components/mqtt_room/sensor.py + homeassistant/components/msteams/notify.py homeassistant/components/mvglive/sensor.py homeassistant/components/mychevy/* homeassistant/components/mycroft/* @@ -417,7 +433,10 @@ omit = homeassistant/components/n26/* homeassistant/components/nad/media_player.py homeassistant/components/nanoleaf/light.py - homeassistant/components/neato/* + homeassistant/components/neato/camera.py + homeassistant/components/neato/sensor.py + homeassistant/components/neato/switch.py + homeassistant/components/neato/vacuum.py homeassistant/components/nederlandse_spoorwegen/sensor.py homeassistant/components/nello/lock.py homeassistant/components/nest/* @@ -461,7 +480,10 @@ omit = homeassistant/components/openhome/media_player.py homeassistant/components/opensensemap/air_quality.py homeassistant/components/opensky/sensor.py - homeassistant/components/opentherm_gw/* + homeassistant/components/opentherm_gw/__init__.py + homeassistant/components/opentherm_gw/binary_sensor.py + homeassistant/components/opentherm_gw/climate.py + homeassistant/components/opentherm_gw/sensor.py homeassistant/components/openuv/__init__.py homeassistant/components/openuv/binary_sensor.py homeassistant/components/openuv/sensor.py @@ -469,6 +491,7 @@ omit = homeassistant/components/openweathermap/weather.py homeassistant/components/opple/light.py homeassistant/components/orangepi_gpio/* + homeassistant/components/oru/* homeassistant/components/orvibo/switch.py homeassistant/components/osramlightify/light.py homeassistant/components/otp/sensor.py @@ -491,6 +514,7 @@ omit = homeassistant/components/plex/media_player.py homeassistant/components/plex/sensor.py homeassistant/components/plex/server.py + homeassistant/components/plex/websockets.py homeassistant/components/plugwise/* homeassistant/components/plum_lightpad/* homeassistant/components/pocketcasts/sensor.py @@ -586,6 +610,7 @@ omit = homeassistant/components/skybeacon/sensor.py homeassistant/components/skybell/* homeassistant/components/slack/notify.py + homeassistant/components/sinch/* homeassistant/components/slide/* homeassistant/components/sma/sensor.py homeassistant/components/smappee/* @@ -599,6 +624,7 @@ omit = homeassistant/components/solaredge/__init__.py homeassistant/components/solaredge/sensor.py homeassistant/components/solaredge_local/sensor.py + homeassistant/components/solarlog/* homeassistant/components/solax/sensor.py homeassistant/components/soma/cover.py homeassistant/components/soma/__init__.py @@ -618,7 +644,6 @@ omit = homeassistant/components/steam_online/sensor.py homeassistant/components/stiebel_eltron/* homeassistant/components/streamlabswater/* - homeassistant/components/stride/notify.py homeassistant/components/suez_water/* homeassistant/components/supervisord/sensor.py homeassistant/components/swiss_hydrological_data/sensor.py @@ -676,9 +701,9 @@ omit = homeassistant/components/tradfri/* homeassistant/components/tradfri/light.py homeassistant/components/tradfri/cover.py + homeassistant/components/tradfri/base_class.py homeassistant/components/trafikverket_train/sensor.py homeassistant/components/trafikverket_weatherstation/sensor.py - homeassistant/components/transmission/__init__.py homeassistant/components/transmission/sensor.py homeassistant/components/transmission/switch.py homeassistant/components/transmission/const.py diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index afb273331aa..5bfd37fab36 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -5,7 +5,7 @@ "dockerFile": "../Dockerfile.dev", "postCreateCommand": "mkdir -p config && pip3 install -e .", "appPort": 8123, - "runArgs": ["-e", "GIT_EDITOR=\"code --wait\""], + "runArgs": ["-e", "GIT_EDITOR=code --wait"], "extensions": [ "ms-python.python", "visualstudioexptteam.vscodeintellicode", diff --git a/.gitignore b/.gitignore index 15f0896975d..2473aeb4bf6 100644 --- a/.gitignore +++ b/.gitignore @@ -128,3 +128,6 @@ monkeytype.sqlite3 # This is left behind by Azure Restore Cache tmp_cache + +# python-language-server / Rope +.ropeproject diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 78b7ec29859..268cff9ea78 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,6 +6,7 @@ repos: args: - --safe - --quiet + files: ^((homeassistant|script|tests)/.+)?[^/]+\.py$ - repo: https://gitlab.com/pycqa/flake8 rev: 3.7.8 hooks: @@ -13,3 +14,18 @@ repos: additional_dependencies: - flake8-docstrings==1.3.1 - pydocstyle==4.0.0 + files: ^(homeassistant|script|tests)/.+\.py$ +# 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 +# another set of our dependencies there... no. Use the "system" one +# and reuse the environment that is set up anyway already instead. +- repo: local + hooks: + - id: mypy + name: mypy + entry: mypy + language: system + types: [python] + require_serial: true + files: ^homeassistant/.+\.py$ diff --git a/.travis.yml b/.travis.yml index 0e9e030128e..6d5b43c2f03 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,7 +19,7 @@ matrix: - python: "3.6.1" env: TOXENV=lint - python: "3.6.1" - env: TOXENV=pylint + env: TOXENV=pylint PYLINT_ARGS=--jobs=0 - python: "3.6.1" env: TOXENV=typing - python: "3.6.1" @@ -27,7 +27,10 @@ matrix: - python: "3.7" env: TOXENV=py37 -cache: pip +cache: + pip: true + directories: + - $HOME/.cache/pre-commit install: pip install -U tox language: python script: travis_wait 50 tox --develop diff --git a/CODEOWNERS b/CODEOWNERS index d2cda1f1d07..eb29ee28915 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -13,10 +13,12 @@ homeassistant/util/* @home-assistant/core homeassistant/scripts/check_config.py @kellerza # Integrations +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 +homeassistant/components/alexa/* @home-assistant/cloud @ochlocracy homeassistant/components/alpha_vantage/* @fabaff homeassistant/components/amazon_polly/* @robbiet480 homeassistant/components/ambiclimate/* @danielhiversen @@ -24,6 +26,7 @@ homeassistant/components/ambient_station/* @bachya homeassistant/components/androidtv/* @JeffLIrion homeassistant/components/apache_kafka/* @bachya homeassistant/components/api/* @home-assistant/core +homeassistant/components/apprise/* @caronc homeassistant/components/aprs/* @PhilRW homeassistant/components/arcam_fmj/* @elupus homeassistant/components/arduino/* @fabaff @@ -49,7 +52,7 @@ homeassistant/components/broadlink/* @danielhiversen homeassistant/components/brunt/* @eavanvalkenburg homeassistant/components/bt_smarthub/* @jxwolstenholme homeassistant/components/buienradar/* @mjj4791 @ties -homeassistant/components/cert_expiry/* @cereal2nd +homeassistant/components/cert_expiry/* @Cereal2nd @jjlawren homeassistant/components/cisco_ios/* @fbradyirl homeassistant/components/cisco_mobility_express/* @fbradyirl homeassistant/components/cisco_webex_teams/* @fbradyirl @@ -97,15 +100,17 @@ homeassistant/components/flock/* @fabaff homeassistant/components/flunearyou/* @bachya homeassistant/components/fortigate/* @kifeo homeassistant/components/fortios/* @kimfrellsen +homeassistant/components/foscam/* @skgsergio homeassistant/components/foursquare/* @robbiet480 homeassistant/components/freebox/* @snoof85 homeassistant/components/fronius/* @nielstron homeassistant/components/frontend/* @home-assistant/frontend homeassistant/components/gearbest/* @HerrHofrat homeassistant/components/geniushub/* @zxdavb +homeassistant/components/geo_rss_events/* @exxamalte homeassistant/components/geonetnz_quakes/* @exxamalte homeassistant/components/gitter/* @fabaff -homeassistant/components/glances/* @fabaff +homeassistant/components/glances/* @fabaff @engrbm87 homeassistant/components/gntp/* @robbiet480 homeassistant/components/google_assistant/* @home-assistant/cloud homeassistant/components/google_cloud/* @lufton @@ -152,6 +157,7 @@ homeassistant/components/izone/* @Swamp-Ig homeassistant/components/jewish_calendar/* @tsvi homeassistant/components/kaiterra/* @Michsior14 homeassistant/components/keba/* @dannerph +homeassistant/components/keenetic_ndms2/* @foxel homeassistant/components/knx/* @Julius2342 homeassistant/components/kodi/* @armills homeassistant/components/konnected/* @heythisisnate @@ -165,7 +171,7 @@ homeassistant/components/liveboxplaytv/* @pschmitt homeassistant/components/logger/* @home-assistant/core homeassistant/components/logi_circle/* @evanjd homeassistant/components/lovelace/* @home-assistant/frontend -homeassistant/components/luci/* @fbradyirl +homeassistant/components/luci/* @fbradyirl @mzdrale homeassistant/components/luftdaten/* @fabaff homeassistant/components/mastodon/* @fabaff homeassistant/components/matrix/* @tinloaf @@ -184,8 +190,10 @@ homeassistant/components/monoprice/* @etsinko homeassistant/components/moon/* @fabaff homeassistant/components/mpd/* @fabaff homeassistant/components/mqtt/* @home-assistant/core +homeassistant/components/msteams/* @peroyvind homeassistant/components/mysensors/* @MartinHjelmare homeassistant/components/mystrom/* @fabaff +homeassistant/components/neato/* @dshokouhi @Santobert homeassistant/components/nello/* @pschmitt homeassistant/components/ness_alarm/* @nickw444 homeassistant/components/nest/* @awarecan @@ -209,6 +217,7 @@ homeassistant/components/opentherm_gw/* @mvn23 homeassistant/components/openuv/* @bachya homeassistant/components/openweathermap/* @fabaff homeassistant/components/orangepi_gpio/* @pascallj +homeassistant/components/oru/* @bvlaicu homeassistant/components/owlet/* @oblogic7 homeassistant/components/panel_custom/* @home-assistant/frontend homeassistant/components/panel_iframe/* @home-assistant/frontend @@ -249,6 +258,7 @@ homeassistant/components/shell_command/* @home-assistant/core homeassistant/components/shiftr/* @fabaff homeassistant/components/shodan/* @fabaff homeassistant/components/simplisafe/* @bachya +homeassistant/components/sinch/* @bendikrb homeassistant/components/slide/* @ualex73 homeassistant/components/sma/* @kellerza homeassistant/components/smarthab/* @outadoc @@ -256,6 +266,7 @@ homeassistant/components/smartthings/* @andrewsayre homeassistant/components/smarty/* @z0mbieprocess homeassistant/components/smtp/* @fabaff homeassistant/components/solaredge_local/* @drobtravels @scheric +homeassistant/components/solarlog/* @Ernst79 homeassistant/components/solax/* @squishykid homeassistant/components/soma/* @ratsept homeassistant/components/somfy/* @tetienne @@ -312,6 +323,7 @@ homeassistant/components/velux/* @Julius2342 homeassistant/components/version/* @fabaff homeassistant/components/vesync/* @markperdue @webdjoe homeassistant/components/vicare/* @oischinger +homeassistant/components/vivotek/* @HarlemSquirrel homeassistant/components/vizio/* @raman325 homeassistant/components/vlc_telnet/* @rodripf homeassistant/components/waqi/* @andrey-git diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index 2c3728f1f8c..f1abf2ff9db 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -45,9 +45,10 @@ stages: . venv/bin/activate pip install -r requirements_test.txt -c homeassistant/package_constraints.txt + pre-commit install-hooks - script: | . venv/bin/activate - flake8 homeassistant tests script + pre-commit run flake8 --all-files displayName: 'Run flake8' - job: 'Validate' pool: @@ -83,9 +84,10 @@ stages: . venv/bin/activate pip install -r requirements_test.txt -c homeassistant/package_constraints.txt + pre-commit install-hooks - script: | . venv/bin/activate - ./script/check_format + pre-commit run black --all-files displayName: 'Check Black formatting' - stage: 'Tests' @@ -112,7 +114,7 @@ stages: python -m venv venv . venv/bin/activate - pip install -U pip setuptools pytest-azurepipelines -c homeassistant/package_constraints.txt + pip install -U pip setuptools pytest-azurepipelines pytest-xdist -c homeassistant/package_constraints.txt pip install -r requirements_test_all.txt -c homeassistant/package_constraints.txt # This is a TEMP. Eventually we should make sure our 4 dependencies drop typing. # Find offending deps with `pipdeptree -r -p typing` @@ -125,7 +127,7 @@ stages: set -e . venv/bin/activate - pytest --timeout=9 --durations=10 -qq -o console_output_style=count -p no:sugar tests + pytest --timeout=9 --durations=10 -n 2 --dist loadfile -qq -o console_output_style=count -p no:sugar tests script/check_dirty displayName: 'Run pytest for python $(python.container)' condition: and(succeeded(), ne(variables['python.container'], variables['PythonMain'])) @@ -133,7 +135,7 @@ stages: set -e . venv/bin/activate - pytest --timeout=9 --durations=10 --cov homeassistant --cov-report html -qq -o console_output_style=count -p no:sugar tests + pytest --timeout=9 --durations=10 -n 2 --dist loadfile --cov homeassistant --cov-report html -qq -o console_output_style=count -p no:sugar tests codecov --token $(codecovToken) script/check_dirty displayName: 'Run pytest for python $(python.container) / coverage' @@ -172,17 +174,16 @@ stages: vmImage: 'ubuntu-latest' container: $[ variables['PythonMain'] ] steps: - - script: | - python -m venv venv + - template: templates/azp-step-cache.yaml@azure + parameters: + keyfile: 'requirements_test.txt | setup.py | homeassistant/package_constraints.txt' + build: | + python -m venv venv - . venv/bin/activate - pip install -e . - pip install -r requirements_test.txt -c homeassistant/package_constraints.txt - displayName: 'Setup Env' + . venv/bin/activate + pip install -e . -r requirements_test.txt -c homeassistant/package_constraints.txt + pre-commit install-hooks - script: | - TYPING_FILES=$(cat mypyrc) - echo -e "Run mypy on: \n$TYPING_FILES" - . venv/bin/activate - mypy $TYPING_FILES + pre-commit run mypy --all-files displayName: 'Run mypy' diff --git a/azure-pipelines-wheels.yml b/azure-pipelines-wheels.yml index 42815d8c8ae..5092010c49c 100644 --- a/azure-pipelines-wheels.yml +++ b/azure-pipelines-wheels.yml @@ -18,7 +18,7 @@ schedules: always: true variables: - name: versionWheels - value: '1.3-3.7-alpine3.10' + value: '1.4-3.7-alpine3.10' resources: repositories: - repository: azure diff --git a/docs/source/api/helpers.rst b/docs/source/api/helpers.rst index 28f4059d60d..8ad645b7977 100644 --- a/docs/source/api/helpers.rst +++ b/docs/source/api/helpers.rst @@ -56,7 +56,7 @@ homeassistant.helpers.data_entry_flow module homeassistant.helpers.deprecation module ---------------------------------------- -.. automodule:: homeassistant.helpers.depracation +.. automodule:: homeassistant.helpers.deprecation :members: :undoc-members: :show-inheritance: diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index ee0d6c08441..921bec71e78 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -45,7 +45,7 @@ async def auth_manager_from_config( ) ) else: - providers = () + providers = [] # So returned auth providers are in same order as config provider_hash: _ProviderDict = OrderedDict() for provider in providers: @@ -57,7 +57,7 @@ async def auth_manager_from_config( *(auth_mfa_module_from_config(hass, config) for config in module_configs) ) else: - modules = () + modules = [] # So returned auth modules are in same order as config module_hash: _MfaModuleDict = OrderedDict() for module in modules: @@ -86,18 +86,6 @@ class AuthManager: hass, self._async_create_login_flow, self._async_finish_login_flow ) - @property - def support_legacy(self) -> bool: - """ - Return if legacy_api_password auth providers are registered. - - Should be removed when we removed legacy_api_password auth providers. - """ - for provider_type, _ in self._providers: - if provider_type == "legacy_api_password": - return True - return False - @property def auth_providers(self) -> List[AuthProvider]: """Return a list of available auth providers.""" diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 7c4ec731b49..6118f4f2bd7 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -64,13 +64,9 @@ async def async_from_config_dict( ) core_config = config.get(core.DOMAIN, {}) - api_password = config.get("http", {}).get("api_password") - trusted_networks = config.get("http", {}).get("trusted_networks") try: - await conf_util.async_process_ha_core_config( - hass, core_config, api_password, trusted_networks - ) + await conf_util.async_process_ha_core_config(hass, core_config) except vol.Invalid as config_err: conf_util.async_log_exception(config_err, "homeassistant", core_config, hass) return None @@ -97,11 +93,11 @@ async def async_from_config_dict( stop = time() _LOGGER.info("Home Assistant initialized in %.2fs", stop - start) - if sys.version_info[:3] < (3, 6, 1): + if sys.version_info[:3] < (3, 7, 0): msg = ( - "Python 3.6.0 support is deprecated and will " - "be removed in the first release after October 2. Please " - "upgrade Python to 3.6.1 or higher." + "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." ) _LOGGER.warning(msg) hass.components.persistent_notification.async_create( @@ -264,7 +260,7 @@ def _get_domains(hass: core.HomeAssistant, config: Dict[str, Any]) -> Set[str]: domains = set(key.split(" ")[0] for key in config.keys() if key != core.DOMAIN) # Add config entry domains - domains.update(hass.config_entries.async_domains()) # type: ignore + domains.update(hass.config_entries.async_domains()) # Make sure the Hass.io component is loaded if "HASSIO" in os.environ: diff --git a/homeassistant/components/.translations/airly.de.json b/homeassistant/components/.translations/airly.de.json deleted file mode 100644 index cb290dc46c0..00000000000 --- a/homeassistant/components/.translations/airly.de.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "config": { - "error": { - "name_exists": "Name existiert bereits" - }, - "step": { - "user": { - "data": { - "latitude": "Breitengrad", - "longitude": "L\u00e4ngengrad", - "name": "Name der Integration" - }, - "title": "Airly" - } - }, - "title": "Airly" - } -} \ No newline at end of file diff --git a/homeassistant/components/abode/.translations/ca.json b/homeassistant/components/abode/.translations/ca.json new file mode 100644 index 00000000000..2424fd9b5f0 --- /dev/null +++ b/homeassistant/components/abode/.translations/ca.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Nom\u00e9s es permet una \u00fanica configuraci\u00f3 d'Abode." + }, + "error": { + "connection_error": "No es pot connectar amb Abode.", + "identifier_exists": "Compte ja registrat.", + "invalid_credentials": "Credencials inv\u00e0lides." + }, + "step": { + "user": { + "data": { + "password": "Contrasenya", + "username": "Correu electr\u00f2nic" + }, + "title": "Introdueix la teva informaci\u00f3 d'inici de sessi\u00f3 a Abode." + } + }, + "title": "Abode" + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/.translations/da.json b/homeassistant/components/abode/.translations/da.json new file mode 100644 index 00000000000..3f094cb93bd --- /dev/null +++ b/homeassistant/components/abode/.translations/da.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning af Abode." + }, + "error": { + "connection_error": "Kunne ikke oprette forbindelse til Abode.", + "identifier_exists": "Konto er allerede registreret.", + "invalid_credentials": "Ugyldige legitimationsoplysninger." + }, + "step": { + "user": { + "data": { + "password": "Adgangskode", + "username": "Email adresse" + }, + "title": "Udfyld dine Abode-loginoplysninger" + } + }, + "title": "Abode" + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/.translations/de.json b/homeassistant/components/abode/.translations/de.json new file mode 100644 index 00000000000..ed5ec85a5d7 --- /dev/null +++ b/homeassistant/components/abode/.translations/de.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Es ist nur eine einzige Konfiguration von Abode erlaubt." + }, + "error": { + "connection_error": "Es kann keine Verbindung zu Abode hergestellt werden.", + "identifier_exists": "Das Konto ist bereits registriert.", + "invalid_credentials": "Ung\u00fcltige Anmeldeinformationen" + }, + "step": { + "user": { + "data": { + "password": "Passwort", + "username": "E-Mail-Adresse" + }, + "title": "Gib deine Abode-Anmeldeinformationen ein" + } + }, + "title": "Abode" + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/.translations/en.json b/homeassistant/components/abode/.translations/en.json new file mode 100644 index 00000000000..e8daeb22c0a --- /dev/null +++ b/homeassistant/components/abode/.translations/en.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Only a single configuration of Abode is allowed." + }, + "error": { + "connection_error": "Unable to connect to Abode.", + "identifier_exists": "Account already registered.", + "invalid_credentials": "Invalid credentials." + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Email Address" + }, + "title": "Fill in your Abode login information" + } + }, + "title": "Abode" + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/.translations/es.json b/homeassistant/components/abode/.translations/es.json new file mode 100644 index 00000000000..908e8f0fbc3 --- /dev/null +++ b/homeassistant/components/abode/.translations/es.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Solo se permite una \u00fanica configuraci\u00f3n de Abode." + }, + "error": { + "connection_error": "No se puede conectar a Abode.", + "identifier_exists": "Cuenta ya registrada.", + "invalid_credentials": "Credenciales inv\u00e1lidas." + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Direcci\u00f3n de correo electr\u00f3nico" + }, + "title": "Rellene la informaci\u00f3n de acceso Abode" + } + }, + "title": "Abode" + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/.translations/fr.json b/homeassistant/components/abode/.translations/fr.json new file mode 100644 index 00000000000..c0c2a35081b --- /dev/null +++ b/homeassistant/components/abode/.translations/fr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Une seule configuration d'Abode est autoris\u00e9e." + }, + "error": { + "connection_error": "Impossible de se connecter \u00e0 Abode.", + "identifier_exists": "Compte d\u00e9j\u00e0 enregistr\u00e9.", + "invalid_credentials": "Informations d'identification invalides." + }, + "step": { + "user": { + "data": { + "password": "Mot de passe", + "username": "Adresse e-mail" + }, + "title": "Remplissez vos informations de connexion Abode" + } + }, + "title": "Abode" + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/.translations/it.json b/homeassistant/components/abode/.translations/it.json new file mode 100644 index 00000000000..af51aca8af9 --- /dev/null +++ b/homeassistant/components/abode/.translations/it.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u00c8 consentita una sola configurazione di Abode." + }, + "error": { + "connection_error": "Impossibile connettersi ad Abode.", + "identifier_exists": "Account gi\u00e0 registrato", + "invalid_credentials": "Credenziali non valide" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Indirizzo email" + }, + "title": "Inserisci le tue informazioni di accesso Abode" + } + }, + "title": "Abode" + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/.translations/ko.json b/homeassistant/components/abode/.translations/ko.json new file mode 100644 index 00000000000..9560dde6b3d --- /dev/null +++ b/homeassistant/components/abode/.translations/ko.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\ud558\ub098\uc758 Abode \ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." + }, + "error": { + "connection_error": "Abode \uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", + "identifier_exists": "\uacc4\uc815\uc774 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "invalid_credentials": "\uc0ac\uc6a9\uc790 \uc774\ub984 \ud639\uc740 \ube44\ubc00\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc774\uba54\uc77c \uc8fc\uc18c" + }, + "title": "Abode \uc0ac\uc6a9\uc790 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694" + } + }, + "title": "Abode" + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/.translations/lb.json b/homeassistant/components/abode/.translations/lb.json new file mode 100644 index 00000000000..ed65a5df7c5 --- /dev/null +++ b/homeassistant/components/abode/.translations/lb.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "N\u00ebmmen eng eenzeg Konfiguratioun vun ZHA ass erlaabt." + }, + "error": { + "connection_error": "Kann sech net mat Abode verbannen.", + "identifier_exists": "Konto ass scho registr\u00e9iert", + "invalid_credentials": "Ong\u00eblteg Login Informatioune" + }, + "step": { + "user": { + "data": { + "password": "Passwuert", + "username": "E-Mail Adress" + }, + "title": "F\u00ebllt \u00e4r Abode Login Informatiounen aus." + } + }, + "title": "Abode" + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/.translations/nl.json b/homeassistant/components/abode/.translations/nl.json new file mode 100644 index 00000000000..89b5ae0c4a5 --- /dev/null +++ b/homeassistant/components/abode/.translations/nl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Slechts een enkele configuratie van Abode is toegestaan." + }, + "error": { + "connection_error": "Kan geen verbinding maken met Abode.", + "identifier_exists": "Account is al geregistreerd.", + "invalid_credentials": "Ongeldige inloggegevens." + }, + "step": { + "user": { + "data": { + "password": "Wachtwoord", + "username": "E-mailadres" + }, + "title": "Vul uw Abode-inloggegevens in" + } + }, + "title": "Abode" + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/.translations/nn.json b/homeassistant/components/abode/.translations/nn.json new file mode 100644 index 00000000000..e0c1b6d6a7d --- /dev/null +++ b/homeassistant/components/abode/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Abode" + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/.translations/no.json b/homeassistant/components/abode/.translations/no.json new file mode 100644 index 00000000000..542381cbb64 --- /dev/null +++ b/homeassistant/components/abode/.translations/no.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Bare en enkelt konfigurasjon av Abode er tillatt." + }, + "error": { + "connection_error": "Kan ikke koble til Abode.", + "identifier_exists": "Kontoen er allerede registrert.", + "invalid_credentials": "Ugyldig brukerinformasjon" + }, + "step": { + "user": { + "data": { + "password": "Passord", + "username": "E-postadresse" + }, + "title": "Fyll ut innloggingsinformasjonen for Abode" + } + }, + "title": "Abode" + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/.translations/pl.json b/homeassistant/components/abode/.translations/pl.json new file mode 100644 index 00000000000..c3f3b8f2c88 --- /dev/null +++ b/homeassistant/components/abode/.translations/pl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Dozwolona jest tylko jedna konfiguracja Abode." + }, + "error": { + "connection_error": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z Abode.", + "identifier_exists": "Konto zosta\u0142o ju\u017c zarejestrowane", + "invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce" + }, + "step": { + "user": { + "data": { + "password": "Has\u0142o", + "username": "Adres e-mail" + }, + "title": "Wprowad\u017a informacje logowania Abode" + } + }, + "title": "Abode" + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/.translations/pt-BR.json b/homeassistant/components/abode/.translations/pt-BR.json new file mode 100644 index 00000000000..7a117a81993 --- /dev/null +++ b/homeassistant/components/abode/.translations/pt-BR.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Endere\u00e7o de e-mail" + } + } + }, + "title": "" + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/.translations/pt.json b/homeassistant/components/abode/.translations/pt.json new file mode 100644 index 00000000000..512bf59906c --- /dev/null +++ b/homeassistant/components/abode/.translations/pt.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "identifier_exists": "Conta j\u00e1 registada" + }, + "step": { + "user": { + "data": { + "username": "Endere\u00e7o de e-mail" + } + } + }, + "title": "" + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/.translations/ru.json b/homeassistant/components/abode/.translations/ru.json new file mode 100644 index 00000000000..f39e6b1443b --- /dev/null +++ b/homeassistant/components/abode/.translations/ru.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "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": { + "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." + }, + "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" + }, + "title": "Abode" + } + }, + "title": "Abode" + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/.translations/sl.json b/homeassistant/components/abode/.translations/sl.json new file mode 100644 index 00000000000..b840913b7be --- /dev/null +++ b/homeassistant/components/abode/.translations/sl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Dovoljena je samo ena konfiguracija Abode." + }, + "error": { + "connection_error": "Ni mogo\u010de vzpostaviti povezave z Abode.", + "identifier_exists": "Ra\u010dun je \u017ee registriran.", + "invalid_credentials": "Neveljavne poverilnice." + }, + "step": { + "user": { + "data": { + "password": "Geslo", + "username": "E-po\u0161tni naslov" + }, + "title": "Izpolnite svoje podatke za prijavo v Abode" + } + }, + "title": "Abode" + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/.translations/zh-Hant.json b/homeassistant/components/abode/.translations/zh-Hant.json new file mode 100644 index 00000000000..5bc9efc3696 --- /dev/null +++ b/homeassistant/components/abode/.translations/zh-Hant.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u50c5\u5141\u8a31\u8a2d\u5b9a\u4e00\u7d44 Abode\u3002" + }, + "error": { + "connection_error": "\u7121\u6cd5\u9023\u7dda\u81f3 Abode\u3002", + "identifier_exists": "\u5e33\u865f\u5df2\u8a3b\u518a\u3002", + "invalid_credentials": "\u6191\u8b49\u7121\u6548\u3002" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u96fb\u5b50\u90f5\u4ef6\u5730\u5740" + }, + "title": "\u586b\u5beb Abode \u767b\u5165\u8cc7\u8a0a" + } + }, + "title": "Abode" + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/__init__.py b/homeassistant/components/abode/__init__.py index f43cbc50f98..6a72ac64145 100644 --- a/homeassistant/components/abode/__init__.py +++ b/homeassistant/components/abode/__init__.py @@ -1,45 +1,36 @@ -"""Support for Abode Home Security system.""" -import logging +"""Support for the Abode Security System.""" +from asyncio import gather +from copy import deepcopy from functools import partial -from requests.exceptions import HTTPError, ConnectTimeout +import logging +from abodepy import Abode +from abodepy.exceptions import AbodeException +import abodepy.helpers.timeline as TIMELINE +from requests.exceptions import ConnectTimeout, HTTPError import voluptuous as vol +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_DATE, - ATTR_TIME, ATTR_ENTITY_ID, - CONF_USERNAME, + ATTR_TIME, CONF_PASSWORD, - CONF_EXCLUDE, - CONF_NAME, - CONF_LIGHTS, + CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, - EVENT_HOMEASSISTANT_START, ) from homeassistant.helpers import config_validation as cv -from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity +from .const import ATTRIBUTION, DOMAIN + _LOGGER = logging.getLogger(__name__) -ATTRIBUTION = "Data provided by goabode.com" - CONF_POLLING = "polling" -DOMAIN = "abode" DEFAULT_CACHEDB = "./abodepy_cache.pickle" -NOTIFICATION_ID = "abode_notification" -NOTIFICATION_TITLE = "Abode Security Setup" - -EVENT_ABODE_ALARM = "abode_alarm" -EVENT_ABODE_ALARM_END = "abode_alarm_end" -EVENT_ABODE_AUTOMATION = "abode_automation" -EVENT_ABODE_FAULT = "abode_panel_fault" -EVENT_ABODE_RESTORE = "abode_panel_restore" - SERVICE_SETTINGS = "change_setting" SERVICE_CAPTURE_IMAGE = "capture_image" SERVICE_TRIGGER = "trigger_quick_action" @@ -53,6 +44,8 @@ ATTR_EVENT_TYPE = "event_type" ATTR_EVENT_UTC = "event_utc" ATTR_SETTING = "setting" ATTR_USER_NAME = "user_name" +ATTR_APP_TYPE = "app_type" +ATTR_EVENT_BY = "event_by" ATTR_VALUE = "value" ABODE_DEVICE_ID_LIST_SCHEMA = vol.Schema([str]) @@ -63,10 +56,7 @@ CONFIG_SCHEMA = vol.Schema( { vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_POLLING, default=False): cv.boolean, - vol.Optional(CONF_EXCLUDE, default=[]): ABODE_DEVICE_ID_LIST_SCHEMA, - vol.Optional(CONF_LIGHTS, default=[]): ABODE_DEVICE_ID_LIST_SCHEMA, } ) }, @@ -96,83 +86,86 @@ ABODE_PLATFORMS = [ class AbodeSystem: """Abode System class.""" - def __init__(self, username, password, cache, name, polling, exclude, lights): + def __init__(self, abode, polling): """Initialize the system.""" - import abodepy - self.abode = abodepy.Abode( - username, - password, - auto_login=True, - get_devices=True, - get_automations=True, - cache_path=cache, - ) - self.name = name + self.abode = abode self.polling = polling - self.exclude = exclude - self.lights = lights self.devices = [] - - def is_excluded(self, device): - """Check if a device is configured to be excluded.""" - return device.device_id in self.exclude - - def is_automation_excluded(self, automation): - """Check if an automation is configured to be excluded.""" - return automation.automation_id in self.exclude - - def is_light(self, device): - """Check if a switch device is configured as a light.""" - import abodepy.helpers.constants as CONST - - return device.generic_type == CONST.TYPE_LIGHT or ( - device.generic_type == CONST.TYPE_SWITCH and device.device_id in self.lights - ) + self.logout_listener = None -def setup(hass, config): - """Set up Abode component.""" - from abodepy.exceptions import AbodeException +async def async_setup(hass, config): + """Set up Abode integration.""" + if DOMAIN not in config: + return True conf = config[DOMAIN] - username = conf.get(CONF_USERNAME) - password = conf.get(CONF_PASSWORD) - name = conf.get(CONF_NAME) - polling = conf.get(CONF_POLLING) - exclude = conf.get(CONF_EXCLUDE) - lights = conf.get(CONF_LIGHTS) + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=deepcopy(conf) + ) + ) + + return True + + +async def async_setup_entry(hass, config_entry): + """Set up Abode integration from a config entry.""" + username = config_entry.data.get(CONF_USERNAME) + password = config_entry.data.get(CONF_PASSWORD) + polling = config_entry.data.get(CONF_POLLING) try: cache = hass.config.path(DEFAULT_CACHEDB) - hass.data[DOMAIN] = AbodeSystem( - username, password, cache, name, polling, exclude, lights + abode = await hass.async_add_executor_job( + Abode, username, password, True, True, True, cache ) + hass.data[DOMAIN] = AbodeSystem(abode, polling) + except (AbodeException, ConnectTimeout, HTTPError) as ex: _LOGGER.error("Unable to connect to Abode: %s", str(ex)) - - hass.components.persistent_notification.create( - "Error: {}
" - "You will need to restart hass after fixing." - "".format(ex), - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID, - ) return False - setup_hass_services(hass) - setup_hass_events(hass) - setup_abode_events(hass) + for platform in ABODE_PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, platform) + ) + + await setup_hass_events(hass) + await hass.async_add_executor_job(setup_hass_services, hass) + await hass.async_add_executor_job(setup_abode_events, hass) + + return True + + +async def async_unload_entry(hass, config_entry): + """Unload a config entry.""" + hass.services.async_remove(DOMAIN, SERVICE_SETTINGS) + hass.services.async_remove(DOMAIN, SERVICE_CAPTURE_IMAGE) + hass.services.async_remove(DOMAIN, SERVICE_TRIGGER) + + tasks = [] for platform in ABODE_PLATFORMS: - discovery.load_platform(hass, platform, DOMAIN, {}, config) + tasks.append( + hass.config_entries.async_forward_entry_unload(config_entry, platform) + ) + + await gather(*tasks) + + await hass.async_add_executor_job(hass.data[DOMAIN].abode.events.stop) + await hass.async_add_executor_job(hass.data[DOMAIN].abode.logout) + + hass.data[DOMAIN].logout_listener() + hass.data.pop(DOMAIN) return True def setup_hass_services(hass): """Home assistant services.""" - from abodepy.exceptions import AbodeException def change_setting(call): """Change an Abode system setting.""" @@ -223,13 +216,9 @@ def setup_hass_services(hass): ) -def setup_hass_events(hass): +async def setup_hass_events(hass): """Home Assistant start and stop callbacks.""" - def startup(event): - """Listen for push events.""" - hass.data[DOMAIN].abode.events.start() - def logout(event): """Logout of Abode.""" if not hass.data[DOMAIN].polling: @@ -239,14 +228,15 @@ def setup_hass_events(hass): _LOGGER.info("Logged out of Abode") if not hass.data[DOMAIN].polling: - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, startup) + await hass.async_add_executor_job(hass.data[DOMAIN].abode.events.start) - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, logout) + hass.data[DOMAIN].logout_listener = hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, logout + ) def setup_abode_events(hass): """Event callbacks.""" - import abodepy.helpers.timeline as TIMELINE def event_callback(event, event_json): """Handle an event callback from Abode.""" @@ -259,6 +249,8 @@ def setup_abode_events(hass): ATTR_EVENT_TYPE: event_json.get(ATTR_EVENT_TYPE, ""), ATTR_EVENT_UTC: event_json.get(ATTR_EVENT_UTC, ""), ATTR_USER_NAME: event_json.get(ATTR_USER_NAME, ""), + ATTR_APP_TYPE: event_json.get(ATTR_APP_TYPE, ""), + ATTR_EVENT_BY: event_json.get(ATTR_EVENT_BY, ""), ATTR_DATE: event_json.get(ATTR_DATE, ""), ATTR_TIME: event_json.get(ATTR_TIME, ""), } @@ -271,6 +263,12 @@ def setup_abode_events(hass): TIMELINE.PANEL_FAULT_GROUP, TIMELINE.PANEL_RESTORE_GROUP, TIMELINE.AUTOMATION_GROUP, + TIMELINE.DISARM_GROUP, + TIMELINE.ARM_GROUP, + TIMELINE.TEST_GROUP, + TIMELINE.CAPTURE_GROUP, + TIMELINE.DEVICE_GROUP, + TIMELINE.AUTOMATION_EDIT_GROUP, ] for event in events: @@ -283,30 +281,36 @@ class AbodeDevice(Entity): """Representation of an Abode device.""" def __init__(self, data, device): - """Initialize a sensor for Abode device.""" + """Initialize Abode device.""" self._data = data self._device = device async def async_added_to_hass(self): - """Subscribe Abode events.""" + """Subscribe to device events.""" self.hass.async_add_job( self._data.abode.events.add_device_callback, self._device.device_id, self._update_callback, ) + async def async_will_remove_from_hass(self): + """Unsubscribe from device events.""" + self.hass.async_add_job( + self._data.abode.events.remove_all_device_callbacks, self._device.device_id + ) + @property def should_poll(self): """Return the polling state.""" return self._data.polling def update(self): - """Update automation state.""" + """Update device and automation states.""" self._device.refresh() @property def name(self): - """Return the name of the sensor.""" + """Return the name of the device.""" return self._device.name @property @@ -320,6 +324,21 @@ class AbodeDevice(Entity): "device_type": self._device.type, } + @property + def unique_id(self): + """Return a unique ID to use for this device.""" + return self._device.device_uuid + + @property + def device_info(self): + """Return device registry information for this entity.""" + return { + "identifiers": {(DOMAIN, self._device.device_id)}, + "manufacturer": "Abode", + "name": self._device.name, + "device_type": self._device.type, + } + def _update_callback(self, device): """Update the device state.""" self.schedule_update_ha_state() @@ -354,7 +373,7 @@ class AbodeAutomation(Entity): @property def name(self): - """Return the name of the sensor.""" + """Return the name of the automation.""" return self._automation.name @property @@ -368,6 +387,6 @@ class AbodeAutomation(Entity): } def _update_callback(self, device): - """Update the device state.""" + """Update the automation state.""" self._automation.refresh() self.schedule_update_ha_state() diff --git a/homeassistant/components/abode/alarm_control_panel.py b/homeassistant/components/abode/alarm_control_panel.py index c5c10e65302..f774e773cb5 100644 --- a/homeassistant/components/abode/alarm_control_panel.py +++ b/homeassistant/components/abode/alarm_control_panel.py @@ -9,32 +9,31 @@ from homeassistant.const import ( STATE_ALARM_DISARMED, ) -from . import ATTRIBUTION, DOMAIN as ABODE_DOMAIN, AbodeDevice +from . import AbodeDevice +from .const import ATTRIBUTION, DOMAIN _LOGGER = logging.getLogger(__name__) ICON = "mdi:security" -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Platform uses config entry setup.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): """Set up an alarm control panel for an Abode device.""" - data = hass.data[ABODE_DOMAIN] + data = hass.data[DOMAIN] - alarm_devices = [AbodeAlarm(data, data.abode.get_alarm(), data.name)] - - data.devices.extend(alarm_devices) - - add_entities(alarm_devices) + async_add_entities( + [AbodeAlarm(data, await hass.async_add_executor_job(data.abode.get_alarm))] + ) class AbodeAlarm(AbodeDevice, alarm.AlarmControlPanel): """An alarm_control_panel implementation for Abode.""" - def __init__(self, data, device, name): - """Initialize the alarm control panel.""" - super().__init__(data, device) - self._name = name - @property def icon(self): """Return the icon.""" @@ -65,11 +64,6 @@ class AbodeAlarm(AbodeDevice, alarm.AlarmControlPanel): """Send arm away command.""" self._device.set_away() - @property - def name(self): - """Return the name of the alarm.""" - return self._name or super().name - @property def device_state_attributes(self): """Return the state attributes.""" diff --git a/homeassistant/components/abode/binary_sensor.py b/homeassistant/components/abode/binary_sensor.py index e37f6a465a4..31f74448496 100644 --- a/homeassistant/components/abode/binary_sensor.py +++ b/homeassistant/components/abode/binary_sensor.py @@ -1,19 +1,25 @@ """Support for Abode Security System binary sensors.""" import logging +import abodepy.helpers.constants as CONST +import abodepy.helpers.timeline as TIMELINE + from homeassistant.components.binary_sensor import BinarySensorDevice -from . import DOMAIN as ABODE_DOMAIN, AbodeAutomation, AbodeDevice +from . import AbodeAutomation, AbodeDevice +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up a sensor for an Abode device.""" - import abodepy.helpers.constants as CONST - import abodepy.helpers.timeline as TIMELINE +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Platform uses config entry setup.""" + pass - data = hass.data[ABODE_DOMAIN] + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up a sensor for an Abode device.""" + data = hass.data[DOMAIN] device_types = [ CONST.TYPE_CONNECTIVITY, @@ -24,25 +30,18 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ] devices = [] - for device in data.abode.get_devices(generic_type=device_types): - if data.is_excluded(device): - continue + for device in data.abode.get_devices(generic_type=device_types): devices.append(AbodeBinarySensor(data, device)) for automation in data.abode.get_automations(generic_type=CONST.TYPE_QUICK_ACTION): - if data.is_automation_excluded(automation): - continue - devices.append( AbodeQuickActionBinarySensor( data, automation, TIMELINE.AUTOMATION_EDIT_GROUP ) ) - data.devices.extend(devices) - - add_entities(devices) + async_add_entities(devices) class AbodeBinarySensor(AbodeDevice, BinarySensorDevice): diff --git a/homeassistant/components/abode/camera.py b/homeassistant/components/abode/camera.py index 95755a644e2..e98a59a985c 100644 --- a/homeassistant/components/abode/camera.py +++ b/homeassistant/components/abode/camera.py @@ -2,35 +2,36 @@ from datetime import timedelta import logging +import abodepy.helpers.constants as CONST +import abodepy.helpers.timeline as TIMELINE import requests from homeassistant.components.camera import Camera from homeassistant.util import Throttle -from . import DOMAIN as ABODE_DOMAIN, AbodeDevice +from . import AbodeDevice +from .const import DOMAIN MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=90) _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up Abode camera devices.""" - import abodepy.helpers.constants as CONST - import abodepy.helpers.timeline as TIMELINE +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Platform uses config entry setup.""" + pass - data = hass.data[ABODE_DOMAIN] + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up a camera for an Abode device.""" + + data = hass.data[DOMAIN] devices = [] for device in data.abode.get_devices(generic_type=CONST.TYPE_CAMERA): - if data.is_excluded(device): - continue - devices.append(AbodeCamera(data, device, TIMELINE.CAPTURE_IMAGE)) - data.devices.extend(devices) - - add_entities(devices) + async_add_entities(devices) class AbodeCamera(AbodeDevice, Camera): diff --git a/homeassistant/components/abode/config_flow.py b/homeassistant/components/abode/config_flow.py new file mode 100644 index 00000000000..d8d914f7998 --- /dev/null +++ b/homeassistant/components/abode/config_flow.py @@ -0,0 +1,79 @@ +"""Config flow for the Abode Security System component.""" +import logging + +from abodepy import Abode +from abodepy.exceptions import AbodeException +from requests.exceptions import ConnectTimeout, HTTPError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import callback + +from .const import DOMAIN # pylint: disable=W0611 + +CONF_POLLING = "polling" + +_LOGGER = logging.getLogger(__name__) + + +class AbodeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Config flow for Abode.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + def __init__(self): + """Initialize.""" + self.data_schema = { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + if not user_input: + return self._show_form() + + username = user_input[CONF_USERNAME] + password = user_input[CONF_PASSWORD] + polling = user_input.get(CONF_POLLING, False) + + try: + await self.hass.async_add_executor_job(Abode, username, password, True) + + except (AbodeException, ConnectTimeout, HTTPError) as ex: + _LOGGER.error("Unable to connect to Abode: %s", str(ex)) + if ex.errcode == 400: + return self._show_form({"base": "invalid_credentials"}) + return self._show_form({"base": "connection_error"}) + + return self.async_create_entry( + title=user_input[CONF_USERNAME], + data={ + CONF_USERNAME: username, + CONF_PASSWORD: password, + CONF_POLLING: polling, + }, + ) + + @callback + def _show_form(self, errors=None): + """Show the form to the user.""" + return self.async_show_form( + step_id="user", + data_schema=vol.Schema(self.data_schema), + errors=errors if errors else {}, + ) + + async def async_step_import(self, import_config): + """Import a config entry from configuration.yaml.""" + if self._async_current_entries(): + _LOGGER.warning("Only one configuration of abode is allowed.") + return self.async_abort(reason="single_instance_allowed") + + return await self.async_step_user(import_config) diff --git a/homeassistant/components/abode/const.py b/homeassistant/components/abode/const.py new file mode 100644 index 00000000000..35e74e154cf --- /dev/null +++ b/homeassistant/components/abode/const.py @@ -0,0 +1,3 @@ +"""Constants for the Abode Security System component.""" +DOMAIN = "abode" +ATTRIBUTION = "Data provided by goabode.com" diff --git a/homeassistant/components/abode/cover.py b/homeassistant/components/abode/cover.py index 4c868daf4ba..ebe59ee45c7 100644 --- a/homeassistant/components/abode/cover.py +++ b/homeassistant/components/abode/cover.py @@ -1,29 +1,31 @@ """Support for Abode Security System covers.""" import logging +import abodepy.helpers.constants as CONST + from homeassistant.components.cover import CoverDevice -from . import DOMAIN as ABODE_DOMAIN, AbodeDevice +from . import AbodeDevice +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up Abode cover devices.""" - import abodepy.helpers.constants as CONST +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Platform uses config entry setup.""" + pass - data = hass.data[ABODE_DOMAIN] + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Abode cover devices.""" + + data = hass.data[DOMAIN] devices = [] for device in data.abode.get_devices(generic_type=CONST.TYPE_COVER): - if data.is_excluded(device): - continue - devices.append(AbodeCover(data, device)) - data.devices.extend(devices) - - add_entities(devices) + async_add_entities(devices) class AbodeCover(AbodeDevice, CoverDevice): diff --git a/homeassistant/components/abode/light.py b/homeassistant/components/abode/light.py index 8e6691560e5..163982d040e 100644 --- a/homeassistant/components/abode/light.py +++ b/homeassistant/components/abode/light.py @@ -2,6 +2,8 @@ import logging from math import ceil +import abodepy.helpers.constants as CONST + from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, @@ -16,31 +18,27 @@ from homeassistant.util.color import ( color_temperature_mired_to_kelvin, ) -from . import DOMAIN as ABODE_DOMAIN, AbodeDevice +from . import AbodeDevice +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Platform uses config entry setup.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Abode light devices.""" - import abodepy.helpers.constants as CONST - - data = hass.data[ABODE_DOMAIN] - - device_types = [CONST.TYPE_LIGHT, CONST.TYPE_SWITCH] + data = hass.data[DOMAIN] devices = [] - # Get all regular lights that are not excluded or switches marked as lights - for device in data.abode.get_devices(generic_type=device_types): - if data.is_excluded(device) or not data.is_light(device): - continue - + for device in data.abode.get_devices(generic_type=CONST.TYPE_LIGHT): devices.append(AbodeLight(data, device)) - data.devices.extend(devices) - - add_entities(devices) + async_add_entities(devices) class AbodeLight(AbodeDevice, Light): diff --git a/homeassistant/components/abode/lock.py b/homeassistant/components/abode/lock.py index c1272a3de5f..11f792f88fd 100644 --- a/homeassistant/components/abode/lock.py +++ b/homeassistant/components/abode/lock.py @@ -1,29 +1,31 @@ -"""Support for Abode Security System locks.""" +"""Support for the Abode Security System locks.""" import logging +import abodepy.helpers.constants as CONST + from homeassistant.components.lock import LockDevice -from . import DOMAIN as ABODE_DOMAIN, AbodeDevice +from . import AbodeDevice +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up Abode lock devices.""" - import abodepy.helpers.constants as CONST +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Platform uses config entry setup.""" + pass - data = hass.data[ABODE_DOMAIN] + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Abode lock devices.""" + + data = hass.data[DOMAIN] devices = [] for device in data.abode.get_devices(generic_type=CONST.TYPE_LOCK): - if data.is_excluded(device): - continue - devices.append(AbodeLock(data, device)) - data.devices.extend(devices) - - add_entities(devices) + async_add_entities(devices) class AbodeLock(AbodeDevice, LockDevice): diff --git a/homeassistant/components/abode/manifest.json b/homeassistant/components/abode/manifest.json index 793c19cc466..b54120c7cbd 100644 --- a/homeassistant/components/abode/manifest.json +++ b/homeassistant/components/abode/manifest.json @@ -1,10 +1,13 @@ { "domain": "abode", "name": "Abode", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/abode", "requirements": [ - "abodepy==0.15.0" + "abodepy==0.16.6" ], "dependencies": [], - "codeowners": [] -} + "codeowners": [ + "@shred86" + ] +} \ No newline at end of file diff --git a/homeassistant/components/abode/sensor.py b/homeassistant/components/abode/sensor.py index ba28eab79c7..e25921f295f 100644 --- a/homeassistant/components/abode/sensor.py +++ b/homeassistant/components/abode/sensor.py @@ -1,13 +1,16 @@ """Support for Abode Security System sensors.""" import logging +import abodepy.helpers.constants as CONST + from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, ) -from . import DOMAIN as ABODE_DOMAIN, AbodeDevice +from . import AbodeDevice +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -19,23 +22,22 @@ SENSOR_TYPES = { } -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up a sensor for an Abode device.""" - import abodepy.helpers.constants as CONST +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Platform uses config entry setup.""" + pass - data = hass.data[ABODE_DOMAIN] + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up a sensor for an Abode device.""" + + data = hass.data[DOMAIN] devices = [] for device in data.abode.get_devices(generic_type=CONST.TYPE_SENSOR): - if data.is_excluded(device): - continue - for sensor_type in SENSOR_TYPES: devices.append(AbodeSensor(data, device, sensor_type)) - data.devices.extend(devices) - - add_entities(devices) + async_add_entities(devices) class AbodeSensor(AbodeDevice): diff --git a/homeassistant/components/abode/strings.json b/homeassistant/components/abode/strings.json new file mode 100644 index 00000000000..bf7e768f6e3 --- /dev/null +++ b/homeassistant/components/abode/strings.json @@ -0,0 +1,22 @@ +{ + "config": { + "title": "Abode", + "step": { + "user": { + "title": "Fill in your Abode login information", + "data": { + "username": "Email Address", + "password": "Password" + } + } + }, + "error": { + "identifier_exists": "Account already registered.", + "invalid_credentials": "Invalid credentials.", + "connection_error": "Unable to connect to Abode." + }, + "abort": { + "single_instance_allowed": "Only a single configuration of Abode is allowed." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/switch.py b/homeassistant/components/abode/switch.py index 82a550df1a5..7bd7f394d30 100644 --- a/homeassistant/components/abode/switch.py +++ b/homeassistant/components/abode/switch.py @@ -1,41 +1,37 @@ """Support for Abode Security System switches.""" import logging +import abodepy.helpers.constants as CONST +import abodepy.helpers.timeline as TIMELINE + from homeassistant.components.switch import SwitchDevice -from . import DOMAIN as ABODE_DOMAIN, AbodeAutomation, AbodeDevice +from . import AbodeAutomation, AbodeDevice +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up Abode switch devices.""" - import abodepy.helpers.constants as CONST - import abodepy.helpers.timeline as TIMELINE +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Platform uses config entry setup.""" + pass - data = hass.data[ABODE_DOMAIN] + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Abode switch devices.""" + data = hass.data[DOMAIN] devices = [] - # Get all regular switches that are not excluded or marked as lights for device in data.abode.get_devices(generic_type=CONST.TYPE_SWITCH): - if data.is_excluded(device) or data.is_light(device): - continue - devices.append(AbodeSwitch(data, device)) - # Get all Abode automations that can be enabled/disabled for automation in data.abode.get_automations(generic_type=CONST.TYPE_AUTOMATION): - if data.is_automation_excluded(automation): - continue - devices.append( AbodeAutomationSwitch(data, automation, TIMELINE.AUTOMATION_EDIT_GROUP) ) - data.devices.extend(devices) - - add_entities(devices) + async_add_entities(devices) class AbodeSwitch(AbodeDevice, SwitchDevice): diff --git a/homeassistant/components/acer_projector/switch.py b/homeassistant/components/acer_projector/switch.py index 558cf84d0e1..39a79636c93 100644 --- a/homeassistant/components/acer_projector/switch.py +++ b/homeassistant/components/acer_projector/switch.py @@ -1,6 +1,7 @@ """Use serial protocol of Acer projector to obtain state of the projector.""" import logging import re +import serial import voluptuous as vol @@ -73,7 +74,6 @@ class AcerSwitch(SwitchDevice): def __init__(self, serial_port, name, timeout, write_timeout, **kwargs): """Init of the Acer projector.""" - import serial self.ser = serial.Serial( port=serial_port, timeout=timeout, write_timeout=write_timeout, **kwargs @@ -90,7 +90,6 @@ class AcerSwitch(SwitchDevice): def _write_read(self, msg): """Write to the projector and read the return.""" - import serial ret = "" # Sometimes the projector won't answer for no reason or the projector diff --git a/homeassistant/components/adguard/.translations/ca.json b/homeassistant/components/adguard/.translations/ca.json index 30fd509cb7a..9b7b3c39b03 100644 --- a/homeassistant/components/adguard/.translations/ca.json +++ b/homeassistant/components/adguard/.translations/ca.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "adguard_home_addon_outdated": "Aquesta integraci\u00f3 necessita la versi\u00f3 d'AdGuard Home {minimal_version} o una superior, tens la {current_version}. Actualitza el complement de Hass.io d'AdGuard Home.", + "adguard_home_outdated": "Aquesta integraci\u00f3 necessita la versi\u00f3 d'AdGuard Home {minimal_version} o una superior, tens la {current_version}.", "existing_instance_updated": "S'ha actualitzat la configuraci\u00f3 existent.", "single_instance_allowed": "Nom\u00e9s es permet una \u00fanica configuraci\u00f3 d'AdGuard Home." }, diff --git a/homeassistant/components/adguard/.translations/en.json b/homeassistant/components/adguard/.translations/en.json index 6e3b5b58503..00d048c3343 100644 --- a/homeassistant/components/adguard/.translations/en.json +++ b/homeassistant/components/adguard/.translations/en.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "adguard_home_addon_outdated": "This integration requires AdGuard Home {minimal_version} or higher, you have {current_version}. Please update your Hass.io AdGuard Home add-on.", + "adguard_home_outdated": "This integration requires AdGuard Home {minimal_version} or higher, you have {current_version}.", "existing_instance_updated": "Updated existing configuration.", "single_instance_allowed": "Only a single configuration of AdGuard Home is allowed." }, diff --git a/homeassistant/components/adguard/.translations/es.json b/homeassistant/components/adguard/.translations/es.json index 5886d8e5c5b..c6946ab6120 100644 --- a/homeassistant/components/adguard/.translations/es.json +++ b/homeassistant/components/adguard/.translations/es.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "adguard_home_addon_outdated": "Esta integraci\u00f3n requiere AdGuard Home {minimal_version} o superior, usted tiene {current_version}. Por favor, actualice su complemento Hass.io AdGuard Home.", + "adguard_home_outdated": "Esta integraci\u00f3n requiere AdGuard Home {minimal_version} o superior, usted tiene {current_version}.", "existing_instance_updated": "Se ha actualizado la configuraci\u00f3n existente.", "single_instance_allowed": "S\u00f3lo se permite una \u00fanica configuraci\u00f3n de AdGuard Home." }, diff --git a/homeassistant/components/adguard/.translations/fr.json b/homeassistant/components/adguard/.translations/fr.json index 6543ddd50bc..749ba7d9c03 100644 --- a/homeassistant/components/adguard/.translations/fr.json +++ b/homeassistant/components/adguard/.translations/fr.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "adguard_home_addon_outdated": "Cette int\u00e9gration n\u00e9cessite AdGuard Home {minimal_version} ou une version ult\u00e9rieure, vous disposez de {current_version}. Veuillez mettre \u00e0 jour votre compl\u00e9ment Hass.io AdGuard Home.", + "adguard_home_outdated": "Cette int\u00e9gration n\u00e9cessite AdGuard Home {minimal_version} ou une version ult\u00e9rieure, vous disposez de {current_version}.", "existing_instance_updated": "La configuration existante a \u00e9t\u00e9 mise \u00e0 jour.", "single_instance_allowed": "Une seule configuration d'AdGuard Home est autoris\u00e9e." }, diff --git a/homeassistant/components/adguard/.translations/ko.json b/homeassistant/components/adguard/.translations/ko.json index bb93d675103..e1f39259292 100644 --- a/homeassistant/components/adguard/.translations/ko.json +++ b/homeassistant/components/adguard/.translations/ko.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "adguard_home_addon_outdated": "\uc774 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub294 AdGuard Home {minimal_version} \uc774\uc0c1\uc774 \ud544\uc694\ud569\ub2c8\ub2e4. \ud604\uc7ac \ubc84\uc804\uc740 {current_version} \uc785\ub2c8\ub2e4. Hass.io AdGuard Home \uc560\ub4dc\uc628\uc744 \uc5c5\ub370\uc774\ud2b8 \ud574\uc8fc\uc138\uc694.", + "adguard_home_outdated": "\uc774 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub294 AdGuard Home {minimal_version} \uc774\uc0c1\uc774 \ud544\uc694\ud569\ub2c8\ub2e4. \ud604\uc7ac \ubc84\uc804\uc740 {current_version} \uc785\ub2c8\ub2e4.", "existing_instance_updated": "\uae30\uc874 \uad6c\uc131\uc744 \uc5c5\ub370\uc774\ud2b8\ud588\uc2b5\ub2c8\ub2e4.", "single_instance_allowed": "\ud558\ub098\uc758 AdGuard Home \ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." }, diff --git a/homeassistant/components/adguard/.translations/lb.json b/homeassistant/components/adguard/.translations/lb.json index cc3ecf5db87..e449f668fd9 100644 --- a/homeassistant/components/adguard/.translations/lb.json +++ b/homeassistant/components/adguard/.translations/lb.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "adguard_home_addon_outdated": "D\u00ebs Integratioun ben\u00e9idegt AdgGuard Home {minimal_version} oder m\u00e9i, dir hutt {current_version}. Aktualis\u00e9iert w.e.g. \u00e4ren Hass.io AdGuard Home Add-on.", + "adguard_home_outdated": "D\u00ebs Integratioun ben\u00e9idegt AdgGuard Home {minimal_version} oder m\u00e9i, dir hutt {current_version}.", "existing_instance_updated": "D\u00e9i bestehend Konfiguratioun ass ge\u00e4nnert.", "single_instance_allowed": "N\u00ebmmen eng eenzeg Konfiguratioun vun AdGuard Home ass erlaabt." }, diff --git a/homeassistant/components/adguard/.translations/nl.json b/homeassistant/components/adguard/.translations/nl.json index 3ef86c30a3f..bd0dcc5fa43 100644 --- a/homeassistant/components/adguard/.translations/nl.json +++ b/homeassistant/components/adguard/.translations/nl.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "adguard_home_addon_outdated": "Deze integratie vereist AdGuard Home {minimal_version} of hoger, u heeft {current_version}. Update uw Hass.io AdGuard Home-add-on.", + "adguard_home_outdated": "Deze integratie vereist AdGuard Home {minimal_version} of hoger, u heeft {current_version}.", "existing_instance_updated": "Bestaande configuratie bijgewerkt.", "single_instance_allowed": "Slechts \u00e9\u00e9n configuratie van AdGuard Home is toegestaan." }, diff --git a/homeassistant/components/adguard/.translations/nn.json b/homeassistant/components/adguard/.translations/nn.json index 7c129cba3af..0e2e82437e8 100644 --- a/homeassistant/components/adguard/.translations/nn.json +++ b/homeassistant/components/adguard/.translations/nn.json @@ -6,6 +6,7 @@ "username": "Brukarnamn" } } - } + }, + "title": "AdGuard Home" } } \ No newline at end of file diff --git a/homeassistant/components/adguard/.translations/no.json b/homeassistant/components/adguard/.translations/no.json index 2cd6cd72f6d..22a8c23644f 100644 --- a/homeassistant/components/adguard/.translations/no.json +++ b/homeassistant/components/adguard/.translations/no.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "adguard_home_addon_outdated": "Denne integrasjonen krever AdGuard Home {minimal_version} eller h\u00f8yere, du har {current_version}. Vennligst oppdater Hass.io AdGuard Home-tillegget.", + "adguard_home_outdated": "Denne integrasjonen krever AdGuard Home {minimal_version} eller h\u00f8yere, du har {current_version}.", "existing_instance_updated": "Oppdatert eksisterende konfigurasjon.", "single_instance_allowed": "Kun en konfigurasjon av AdGuard Hjemer tillatt." }, diff --git a/homeassistant/components/adguard/.translations/pl.json b/homeassistant/components/adguard/.translations/pl.json index f8f64d54260..69ba6b024e2 100644 --- a/homeassistant/components/adguard/.translations/pl.json +++ b/homeassistant/components/adguard/.translations/pl.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "adguard_home_addon_outdated": "Ta integracja wymaga AdGuard Home {minimal_version} lub nowszej wersji, masz {current_version}. Zaktualizuj sw\u00f3j dodatek Hass.io AdGuard Home.", + "adguard_home_outdated": "Ta integracja wymaga AdGuard Home {minimal_version} lub nowszej wersji, masz {current_version}.", "existing_instance_updated": "Zaktualizowano istniej\u0105c\u0105 konfiguracj\u0119.", "single_instance_allowed": "Dozwolona jest tylko jedna konfiguracja AdGuard Home." }, diff --git a/homeassistant/components/adguard/.translations/pt.json b/homeassistant/components/adguard/.translations/pt.json new file mode 100644 index 00000000000..f681da4210f --- /dev/null +++ b/homeassistant/components/adguard/.translations/pt.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Servidor", + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/adguard/.translations/ru.json b/homeassistant/components/adguard/.translations/ru.json index c50d0197351..eca46d7db00 100644 --- a/homeassistant/components/adguard/.translations/ru.json +++ b/homeassistant/components/adguard/.translations/ru.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "adguard_home_addon_outdated": "\u042d\u0442\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 AdGuard Home \u0432\u0435\u0440\u0441\u0438\u0438 {current_version}. \u0414\u043b\u044f \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u043e\u0439 \u0440\u0430\u0431\u043e\u0442\u044b \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0432\u0435\u0440\u0441\u0438\u044f {minimal_version}, \u0438\u043b\u0438 \u0431\u043e\u043b\u0435\u0435 \u043d\u043e\u0432\u0430\u044f. \u0414\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u043d\u043e\u0432\u043e\u0439 \u0432\u0435\u0440\u0441\u0438\u0438, \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u0435 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u0435 Hass.io.", + "adguard_home_outdated": "\u042d\u0442\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 AdGuard Home \u0432\u0435\u0440\u0441\u0438\u0438 {current_version}. \u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0432\u0435\u0440\u0441\u0438\u044e {minimal_version} \u0438\u043b\u0438 \u0431\u043e\u043b\u0435\u0435 \u043d\u043e\u0432\u0443\u044e.", "existing_instance_updated": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0430.", "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." }, diff --git a/homeassistant/components/adguard/.translations/sl.json b/homeassistant/components/adguard/.translations/sl.json index f1ca796363d..974524c932d 100644 --- a/homeassistant/components/adguard/.translations/sl.json +++ b/homeassistant/components/adguard/.translations/sl.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "adguard_home_addon_outdated": "Za to integracijo je potrebna AdGuard Home {minimal_version} ali vi\u0161ja, vi imate {current_version}. Prosimo posodobite va\u0161 hass.io AdGuard Home dodatek.", + "adguard_home_outdated": "Za to integracijo je potrebna AdGuard Home {minimal_version} ali vi\u0161ja, vi imate {current_version}.", "existing_instance_updated": "Posodobljena obstoje\u010da konfiguracija.", "single_instance_allowed": "Dovoljena je samo ena konfiguracija AdGuard Home." }, diff --git a/homeassistant/components/adguard/.translations/zh-Hant.json b/homeassistant/components/adguard/.translations/zh-Hant.json index a693652fedf..d08a5715a8e 100644 --- a/homeassistant/components/adguard/.translations/zh-Hant.json +++ b/homeassistant/components/adguard/.translations/zh-Hant.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "adguard_home_addon_outdated": "\u6574\u5408\u9700\u8981 AdGuard Home {minimal_version} \u6216\u66f4\u65b0\u7248\u672c\uff0c\u60a8\u76ee\u524d\u4f7f\u7528\u7248\u672c\u70ba {current_version}\u3002\u8acb\u66f4\u65b0 Hass.io AdGuard Home \u5143\u4ef6\u3002", + "adguard_home_outdated": "\u6574\u5408\u9700\u8981 AdGuard Home {minimal_version} \u6216\u66f4\u65b0\u7248\u672c\uff0c\u60a8\u76ee\u524d\u4f7f\u7528\u7248\u672c\u70ba {current_version}\u3002", "existing_instance_updated": "\u5df2\u66f4\u65b0\u73fe\u6709\u8a2d\u5b9a\u3002", "single_instance_allowed": "\u50c5\u5141\u8a31\u8a2d\u5b9a\u4e00\u7d44 AdGuard Home\u3002" }, diff --git a/homeassistant/components/adguard/__init__.py b/homeassistant/components/adguard/__init__.py index ba716ae0f9c..bb53d00aab8 100644 --- a/homeassistant/components/adguard/__init__.py +++ b/homeassistant/components/adguard/__init__.py @@ -1,8 +1,9 @@ """Support for AdGuard Home.""" +from distutils.version import LooseVersion import logging from typing import Any, Dict -from adguardhome import AdGuardHome, AdGuardHomeError +from adguardhome import AdGuardHome, AdGuardHomeConnectionError, AdGuardHomeError import voluptuous as vol from homeassistant.components.adguard.const import ( @@ -10,6 +11,7 @@ from homeassistant.components.adguard.const import ( DATA_ADGUARD_CLIENT, DATA_ADGUARD_VERION, DOMAIN, + MIN_ADGUARD_HOME_VERSION, SERVICE_ADD_URL, SERVICE_DISABLE_URL, SERVICE_ENABLE_URL, @@ -27,6 +29,7 @@ from homeassistant.const import ( CONF_USERNAME, CONF_VERIFY_SSL, ) +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity import Entity @@ -64,6 +67,17 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool hass.data.setdefault(DOMAIN, {})[DATA_ADGUARD_CLIENT] = adguard + try: + version = await adguard.version() + except AdGuardHomeConnectionError as exception: + raise ConfigEntryNotReady from exception + + if LooseVersion(MIN_ADGUARD_HOME_VERSION) > LooseVersion(version): + _LOGGER.error( + "This integration requires AdGuard Home v0.99.0 or higher to work correctly" + ) + raise ConfigEntryNotReady + for component in "sensor", "switch": hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, component) diff --git a/homeassistant/components/adguard/config_flow.py b/homeassistant/components/adguard/config_flow.py index 5a096aeceed..9f5645edb8d 100644 --- a/homeassistant/components/adguard/config_flow.py +++ b/homeassistant/components/adguard/config_flow.py @@ -1,11 +1,12 @@ """Config flow to configure the AdGuard Home integration.""" +from distutils.version import LooseVersion import logging from adguardhome import AdGuardHome, AdGuardHomeConnectionError import voluptuous as vol from homeassistant import config_entries -from homeassistant.components.adguard.const import DOMAIN +from homeassistant.components.adguard.const import DOMAIN, MIN_ADGUARD_HOME_VERSION from homeassistant.config_entries import ConfigFlow from homeassistant.const import ( CONF_HOST, @@ -83,11 +84,20 @@ class AdGuardHomeFlowHandler(ConfigFlow): ) try: - await adguard.version() + version = await adguard.version() except AdGuardHomeConnectionError: errors["base"] = "connection_error" return await self._show_setup_form(errors) + if LooseVersion(MIN_ADGUARD_HOME_VERSION) > LooseVersion(version): + return self.async_abort( + reason="adguard_home_outdated", + description_placeholders={ + "current_version": version, + "minimal_version": MIN_ADGUARD_HOME_VERSION, + }, + ) + return self.async_create_entry( title=user_input[CONF_HOST], data={ @@ -156,11 +166,20 @@ class AdGuardHomeFlowHandler(ConfigFlow): ) try: - await adguard.version() + version = await adguard.version() except AdGuardHomeConnectionError: errors["base"] = "connection_error" return await self._show_hassio_form(errors) + if LooseVersion(MIN_ADGUARD_HOME_VERSION) > LooseVersion(version): + return self.async_abort( + reason="adguard_home_addon_outdated", + description_placeholders={ + "current_version": version, + "minimal_version": MIN_ADGUARD_HOME_VERSION, + }, + ) + return self.async_create_entry( title=self._hassio_discovery["addon"], data={ diff --git a/homeassistant/components/adguard/const.py b/homeassistant/components/adguard/const.py index c77d76a70cf..eb12a9c163f 100644 --- a/homeassistant/components/adguard/const.py +++ b/homeassistant/components/adguard/const.py @@ -7,6 +7,8 @@ DATA_ADGUARD_VERION = "adguard_version" CONF_FORCE = "force" +MIN_ADGUARD_HOME_VERSION = "v0.99.0" + SERVICE_ADD_URL = "add_url" SERVICE_DISABLE_URL = "disable_url" SERVICE_ENABLE_URL = "enable_url" diff --git a/homeassistant/components/adguard/manifest.json b/homeassistant/components/adguard/manifest.json index f207e6dff09..45fd21f4fc8 100644 --- a/homeassistant/components/adguard/manifest.json +++ b/homeassistant/components/adguard/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/adguard", "requirements": [ - "adguardhome==0.2.1" + "adguardhome==0.3.0" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/adguard/strings.json b/homeassistant/components/adguard/strings.json index b3966bca820..d33ba2b397a 100644 --- a/homeassistant/components/adguard/strings.json +++ b/homeassistant/components/adguard/strings.json @@ -23,8 +23,10 @@ "connection_error": "Failed to connect." }, "abort": { - "single_instance_allowed": "Only a single configuration of AdGuard Home is allowed.", - "existing_instance_updated": "Updated existing configuration." + "adguard_home_outdated": "This integration requires AdGuard Home {minimal_version} or higher, you have {current_version}.", + "adguard_home_addon_outdated": "This integration requires AdGuard Home {minimal_version} or higher, you have {current_version}. Please update your Hass.io AdGuard Home add-on.", + "existing_instance_updated": "Updated existing configuration.", + "single_instance_allowed": "Only a single configuration of AdGuard Home is allowed." } } -} +} \ No newline at end of file diff --git a/homeassistant/components/ads/__init__.py b/homeassistant/components/ads/__init__.py index 1b4f11c7cc1..ba4762da84a 100644 --- a/homeassistant/components/ads/__init__.py +++ b/homeassistant/components/ads/__init__.py @@ -7,6 +7,8 @@ from collections import namedtuple import asyncio import async_timeout +import pyads + import voluptuous as vol from homeassistant.const import ( @@ -78,7 +80,6 @@ SCHEMA_SERVICE_WRITE_DATA_BY_NAME = vol.Schema( def setup(hass, config): """Set up the ADS component.""" - import pyads conf = config[DOMAIN] @@ -161,7 +162,6 @@ class AdsHub: def shutdown(self, *args, **kwargs): """Shutdown ADS connection.""" - import pyads _LOGGER.debug("Shutting down ADS") for notification_item in self._notification_items.values(): @@ -187,7 +187,6 @@ class AdsHub: def write_by_name(self, name, value, plc_datatype): """Write a value to the device.""" - import pyads with self._lock: try: @@ -197,7 +196,6 @@ class AdsHub: def read_by_name(self, name, plc_datatype): """Read a value from the device.""" - import pyads with self._lock: try: @@ -207,7 +205,6 @@ class AdsHub: def add_device_notification(self, name, plc_datatype, callback): """Add a notification to the ADS devices.""" - import pyads attr = pyads.NotificationAttrib(ctypes.sizeof(plc_datatype)) diff --git a/homeassistant/components/aftership/sensor.py b/homeassistant/components/aftership/sensor.py index e54a48f7ee4..c41e5aec7b5 100644 --- a/homeassistant/components/aftership/sensor.py +++ b/homeassistant/components/aftership/sensor.py @@ -146,10 +146,10 @@ class AfterShipSensor(Entity): async def async_added_to_hass(self): """Register callbacks.""" self.hass.helpers.dispatcher.async_dispatcher_connect( - UPDATE_TOPIC, self.force_update + UPDATE_TOPIC, self._force_update ) - async def force_update(self): + async def _force_update(self): """Force update of data.""" await self.async_update(no_throttle=True) await self.async_update_ha_state() diff --git a/homeassistant/components/.translations/airly.ca.json b/homeassistant/components/airly/.translations/ca.json similarity index 100% rename from homeassistant/components/.translations/airly.ca.json rename to homeassistant/components/airly/.translations/ca.json diff --git a/homeassistant/components/.translations/airly.da.json b/homeassistant/components/airly/.translations/da.json similarity index 100% rename from homeassistant/components/.translations/airly.da.json rename to homeassistant/components/airly/.translations/da.json diff --git a/homeassistant/components/airly/.translations/de.json b/homeassistant/components/airly/.translations/de.json new file mode 100644 index 00000000000..83c23a90389 --- /dev/null +++ b/homeassistant/components/airly/.translations/de.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "Der API-Schl\u00fcssel ist nicht korrekt.", + "name_exists": "Name existiert bereits", + "wrong_location": "Keine Airly Luftmessstation an diesem Ort" + }, + "step": { + "user": { + "data": { + "api_key": "Airly API-Schl\u00fcssel", + "latitude": "Breitengrad", + "longitude": "L\u00e4ngengrad", + "name": "Name der Integration" + }, + "description": "Einrichtung der Airly-Luftqualit\u00e4t Integration. Um einen API-Schl\u00fcssel zu generieren, registriere dich auf https://developer.airly.eu/register", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.en.json b/homeassistant/components/airly/.translations/en.json similarity index 100% rename from homeassistant/components/.translations/airly.en.json rename to homeassistant/components/airly/.translations/en.json diff --git a/homeassistant/components/.translations/airly.es.json b/homeassistant/components/airly/.translations/es.json similarity index 100% rename from homeassistant/components/.translations/airly.es.json rename to homeassistant/components/airly/.translations/es.json diff --git a/homeassistant/components/.translations/airly.fr.json b/homeassistant/components/airly/.translations/fr.json similarity index 76% rename from homeassistant/components/.translations/airly.fr.json rename to homeassistant/components/airly/.translations/fr.json index cf756a9f492..374e578eed2 100644 --- a/homeassistant/components/.translations/airly.fr.json +++ b/homeassistant/components/airly/.translations/fr.json @@ -13,6 +13,7 @@ "longitude": "Longitude", "name": "Nom de l'int\u00e9gration" }, + "description": "Configurez l'int\u00e9gration de la qualit\u00e9 de l'air Airly. Pour g\u00e9n\u00e9rer une cl\u00e9 API, rendez-vous sur https://developer.airly.eu/register.", "title": "Airly" } }, diff --git a/homeassistant/components/.translations/airly.it.json b/homeassistant/components/airly/.translations/it.json similarity index 100% rename from homeassistant/components/.translations/airly.it.json rename to homeassistant/components/airly/.translations/it.json diff --git a/homeassistant/components/airly/.translations/ko.json b/homeassistant/components/airly/.translations/ko.json new file mode 100644 index 00000000000..eb20c9174b4 --- /dev/null +++ b/homeassistant/components/airly/.translations/ko.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "API \ud0a4\uac00 \uc62c\ubc14\ub974\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.", + "name_exists": "\uc774\ub984\uc774 \uc774\ubbf8 \uc874\uc7ac\ud569\ub2c8\ub2e4.", + "wrong_location": "\uc774 \uc9c0\uc5ed\uc5d0\ub294 Airly \uce21\uc815 \uc2a4\ud14c\uc774\uc158\uc774 \uc5c6\uc2b5\ub2c8\ub2e4." + }, + "step": { + "user": { + "data": { + "api_key": "Airly API \ud0a4", + "latitude": "\uc704\ub3c4", + "longitude": "\uacbd\ub3c4", + "name": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uc758 \uc774\ub984" + }, + "description": "Airly \uacf5\uae30 \ud488\uc9c8 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4. API \ud0a4\ub97c \uc0dd\uc131\ud558\ub824\uba74 https://developer.airly.eu/register \ub85c \uc774\ub3d9\ud574\uc8fc\uc138\uc694", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.lb.json b/homeassistant/components/airly/.translations/lb.json similarity index 100% rename from homeassistant/components/.translations/airly.lb.json rename to homeassistant/components/airly/.translations/lb.json diff --git a/homeassistant/components/airly/.translations/nl.json b/homeassistant/components/airly/.translations/nl.json new file mode 100644 index 00000000000..232d5d54d85 --- /dev/null +++ b/homeassistant/components/airly/.translations/nl.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "API-sleutel is niet correct.", + "name_exists": "Naam bestaat al.", + "wrong_location": "Geen Airly meetstations in dit gebied." + }, + "step": { + "user": { + "data": { + "api_key": "Airly API-sleutel", + "latitude": "Breedtegraad", + "longitude": "Lengtegraad", + "name": "Naam van de integratie" + }, + "description": "Airly-integratie van luchtkwaliteit instellen. Ga naar https://developer.airly.eu/register om de API-sleutel te genereren", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.nn.json b/homeassistant/components/airly/.translations/nn.json similarity index 100% rename from homeassistant/components/.translations/airly.nn.json rename to homeassistant/components/airly/.translations/nn.json diff --git a/homeassistant/components/.translations/airly.no.json b/homeassistant/components/airly/.translations/no.json similarity index 100% rename from homeassistant/components/.translations/airly.no.json rename to homeassistant/components/airly/.translations/no.json diff --git a/homeassistant/components/.translations/airly.pl.json b/homeassistant/components/airly/.translations/pl.json similarity index 100% rename from homeassistant/components/.translations/airly.pl.json rename to homeassistant/components/airly/.translations/pl.json diff --git a/homeassistant/components/airly/.translations/pt.json b/homeassistant/components/airly/.translations/pt.json new file mode 100644 index 00000000000..d99bcb90733 --- /dev/null +++ b/homeassistant/components/airly/.translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "user": { + "data": { + "latitude": "Latitude", + "longitude": "Longitude" + }, + "title": "" + } + }, + "title": "" + } +} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.ru.json b/homeassistant/components/airly/.translations/ru.json similarity index 100% rename from homeassistant/components/.translations/airly.ru.json rename to homeassistant/components/airly/.translations/ru.json diff --git a/homeassistant/components/.translations/airly.sl.json b/homeassistant/components/airly/.translations/sl.json similarity index 100% rename from homeassistant/components/.translations/airly.sl.json rename to homeassistant/components/airly/.translations/sl.json diff --git a/homeassistant/components/.translations/airly.zh-Hant.json b/homeassistant/components/airly/.translations/zh-Hant.json similarity index 100% rename from homeassistant/components/.translations/airly.zh-Hant.json rename to homeassistant/components/airly/.translations/zh-Hant.json diff --git a/homeassistant/components/airly/__init__.py b/homeassistant/components/airly/__init__.py new file mode 100644 index 00000000000..dc2323ddd4e --- /dev/null +++ b/homeassistant/components/airly/__init__.py @@ -0,0 +1,114 @@ +"""The Airly component.""" +import asyncio +import logging +from datetime import timedelta + +import async_timeout +from aiohttp.client_exceptions import ClientConnectorError +from airly import Airly +from airly.exceptions import AirlyError + +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.core import Config, HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.util import Throttle + +from .const import ( + ATTR_API_ADVICE, + ATTR_API_CAQI, + ATTR_API_CAQI_DESCRIPTION, + ATTR_API_CAQI_LEVEL, + DATA_CLIENT, + DOMAIN, + NO_AIRLY_SENSORS, +) + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_SCAN_INTERVAL = timedelta(minutes=10) + + +async def async_setup(hass: HomeAssistant, config: Config) -> bool: + """Set up configured Airly.""" + return True + + +async def async_setup_entry(hass, config_entry): + """Set up Airly as config entry.""" + api_key = config_entry.data[CONF_API_KEY] + latitude = config_entry.data[CONF_LATITUDE] + longitude = config_entry.data[CONF_LONGITUDE] + + websession = async_get_clientsession(hass) + + airly = AirlyData(websession, api_key, latitude, longitude) + + await airly.async_update() + + hass.data[DOMAIN] = {} + hass.data[DOMAIN][DATA_CLIENT] = {} + hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = airly + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, "air_quality") + ) + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, "sensor") + ) + 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") + await hass.config_entries.async_forward_entry_unload(config_entry, "sensor") + return True + + +class AirlyData: + """Define an object to hold Airly data.""" + + def __init__(self, session, api_key, latitude, longitude): + """Initialize.""" + self.latitude = latitude + self.longitude = longitude + self.airly = Airly(api_key, session) + self.data = {} + + @Throttle(DEFAULT_SCAN_INTERVAL) + async def async_update(self): + """Update Airly data.""" + + try: + with async_timeout.timeout(10): + measurements = self.airly.create_measurements_session_point( + self.latitude, self.longitude + ) + await measurements.update() + + values = measurements.current["values"] + index = measurements.current["indexes"][0] + standards = measurements.current["standards"] + + if index["description"] == NO_AIRLY_SENSORS: + _LOGGER.error("Can't retrieve data: no Airly sensors in this area") + return + for value in values: + self.data[value["name"]] = value["value"] + for standard in standards: + self.data[f"{standard['pollutant']}_LIMIT"] = standard["limit"] + self.data[f"{standard['pollutant']}_PERCENT"] = standard["percent"] + self.data[ATTR_API_CAQI] = index["value"] + self.data[ATTR_API_CAQI_LEVEL] = index["level"].lower().replace("_", " ") + self.data[ATTR_API_CAQI_DESCRIPTION] = index["description"] + self.data[ATTR_API_ADVICE] = index["advice"] + _LOGGER.debug("Data retrieved from Airly") + except ( + ValueError, + AirlyError, + asyncio.TimeoutError, + ClientConnectorError, + ) as error: + _LOGGER.error(error) + self.data = {} diff --git a/homeassistant/components/airly/air_quality.py b/homeassistant/components/airly/air_quality.py new file mode 100644 index 00000000000..082344c14e3 --- /dev/null +++ b/homeassistant/components/airly/air_quality.py @@ -0,0 +1,138 @@ +"""Support for the Airly air_quality service.""" +from homeassistant.components.air_quality import ( + AirQualityEntity, + ATTR_AQI, + ATTR_PM_10, + ATTR_PM_2_5, +) +from homeassistant.const import CONF_NAME + +from .const import ( + ATTR_API_ADVICE, + ATTR_API_CAQI, + ATTR_API_CAQI_DESCRIPTION, + ATTR_API_CAQI_LEVEL, + ATTR_API_PM10, + ATTR_API_PM10_LIMIT, + ATTR_API_PM10_PERCENT, + ATTR_API_PM25, + ATTR_API_PM25_LIMIT, + ATTR_API_PM25_PERCENT, + DATA_CLIENT, + DOMAIN, +) + +ATTRIBUTION = "Data provided by Airly" + +LABEL_ADVICE = "advice" +LABEL_AQI_LEVEL = f"{ATTR_AQI}_level" +LABEL_PM_2_5_LIMIT = f"{ATTR_PM_2_5}_limit" +LABEL_PM_2_5_PERCENT = f"{ATTR_PM_2_5}_percent_of_limit" +LABEL_PM_10_LIMIT = f"{ATTR_PM_10}_limit" +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] + + data = hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] + + async_add_entities([AirlyAirQuality(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 AirlyAirQuality(AirQualityEntity): + """Define an Airly air quality.""" + + def __init__(self, airly, name): + """Initialize.""" + self.airly = airly + self.data = airly.data + self._name = name + self._pm_2_5 = None + self._pm_10 = None + self._aqi = None + self._icon = "mdi:blur" + self._attrs = {} + + @property + def name(self): + """Return the name.""" + return self._name + + @property + def icon(self): + """Return the icon.""" + return self._icon + + @property + @round_state + 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 + def attribution(self): + """Return the attribution.""" + return ATTRIBUTION + + @property + def state(self): + """Return the CAQI description.""" + return self.data[ATTR_API_CAQI_DESCRIPTION] + + @property + def unique_id(self): + """Return a unique_id for this entity.""" + return f"{self.airly.latitude}-{self.airly.longitude}" + + @property + def available(self): + """Return True if entity is available.""" + return bool(self.airly.data) + + @property + def device_state_attributes(self): + """Return the state attributes.""" + self._attrs[LABEL_ADVICE] = self.data[ATTR_API_ADVICE] + self._attrs[LABEL_AQI_LEVEL] = self.data[ATTR_API_CAQI_LEVEL] + self._attrs[LABEL_PM_2_5_LIMIT] = self.data[ATTR_API_PM25_LIMIT] + self._attrs[LABEL_PM_2_5_PERCENT] = round(self.data[ATTR_API_PM25_PERCENT]) + self._attrs[LABEL_PM_10_LIMIT] = self.data[ATTR_API_PM10_LIMIT] + self._attrs[LABEL_PM_10_PERCENT] = round(self.data[ATTR_API_PM10_PERCENT]) + return self._attrs + + async def async_update(self): + """Update the entity.""" + await self.airly.async_update() + + 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] diff --git a/homeassistant/components/airly/config_flow.py b/homeassistant/components/airly/config_flow.py new file mode 100644 index 00000000000..b361930fa7d --- /dev/null +++ b/homeassistant/components/airly/config_flow.py @@ -0,0 +1,114 @@ +"""Adds config flow for Airly.""" +import async_timeout +import voluptuous as vol +from airly import Airly +from airly.exceptions import AirlyError + +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 + +from .const import DEFAULT_NAME, DOMAIN, NO_AIRLY_SENSORS + + +@callback +def configured_instances(hass): + """Return a set of configured Airly instances.""" + return set( + entry.data[CONF_NAME] for entry in hass.config_entries.async_entries(DOMAIN) + ) + + +class AirlyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Config flow for Airly.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + def __init__(self): + """Initialize.""" + self._errors = {} + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + self._errors = {} + + websession = async_get_clientsession(self.hass) + + if user_input is not None: + if user_input[CONF_NAME] in configured_instances(self.hass): + self._errors[CONF_NAME] = "name_exists" + api_key_valid = await self._test_api_key(websession, user_input["api_key"]) + if not api_key_valid: + self._errors["base"] = "auth" + else: + location_valid = await self._test_location( + websession, + user_input["api_key"], + user_input["latitude"], + user_input["longitude"], + ) + if not location_valid: + self._errors["base"] = "wrong_location" + + if not self._errors: + return self.async_create_entry( + title=user_input[CONF_NAME], data=user_input + ) + + return self._show_config_form( + name=DEFAULT_NAME, + api_key="", + latitude=self.hass.config.latitude, + longitude=self.hass.config.longitude, + ) + + def _show_config_form(self, name=None, api_key=None, latitude=None, longitude=None): + """Show the configuration form to edit data.""" + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_API_KEY, default=api_key): str, + vol.Optional( + CONF_LATITUDE, default=self.hass.config.latitude + ): cv.latitude, + vol.Optional( + CONF_LONGITUDE, default=self.hass.config.longitude + ): cv.longitude, + vol.Optional(CONF_NAME, default=name): str, + } + ), + errors=self._errors, + ) + + async def _test_api_key(self, client, api_key): + """Return true if api_key is valid.""" + + with async_timeout.timeout(10): + airly = Airly(api_key, client) + measurements = airly.create_measurements_session_point( + latitude=52.24131, longitude=20.99101 + ) + try: + await measurements.update() + except AirlyError: + return False + return True + + async def _test_location(self, client, api_key, latitude, longitude): + """Return true if location is valid.""" + + with async_timeout.timeout(10): + airly = Airly(api_key, client) + measurements = airly.create_measurements_session_point( + latitude=latitude, longitude=longitude + ) + + await measurements.update() + current = measurements.current + if current["indexes"][0]["description"] == NO_AIRLY_SENSORS: + return False + return True diff --git a/homeassistant/components/airly/const.py b/homeassistant/components/airly/const.py new file mode 100644 index 00000000000..2040faea6b6 --- /dev/null +++ b/homeassistant/components/airly/const.py @@ -0,0 +1,19 @@ +"""Constants for Airly integration.""" +ATTR_API_ADVICE = "ADVICE" +ATTR_API_CAQI = "CAQI" +ATTR_API_CAQI_DESCRIPTION = "DESCRIPTION" +ATTR_API_CAQI_LEVEL = "LEVEL" +ATTR_API_HUMIDITY = "HUMIDITY" +ATTR_API_PM1 = "PM1" +ATTR_API_PM10 = "PM10" +ATTR_API_PM10_LIMIT = "PM10_LIMIT" +ATTR_API_PM10_PERCENT = "PM10_PERCENT" +ATTR_API_PM25 = "PM25" +ATTR_API_PM25_LIMIT = "PM25_LIMIT" +ATTR_API_PM25_PERCENT = "PM25_PERCENT" +ATTR_API_PRESSURE = "PRESSURE" +ATTR_API_TEMPERATURE = "TEMPERATURE" +DATA_CLIENT = "client" +DEFAULT_NAME = "Airly" +DOMAIN = "airly" +NO_AIRLY_SENSORS = "There are no Airly sensors in this area yet." diff --git a/homeassistant/components/airly/manifest.json b/homeassistant/components/airly/manifest.json new file mode 100644 index 00000000000..1859f084bf1 --- /dev/null +++ b/homeassistant/components/airly/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "airly", + "name": "Airly", + "documentation": "https://www.home-assistant.io/integrations/airly", + "dependencies": [], + "codeowners": ["@bieniu"], + "requirements": ["airly==0.0.2"], + "config_flow": true +} diff --git a/homeassistant/components/airly/sensor.py b/homeassistant/components/airly/sensor.py new file mode 100644 index 00000000000..bce32d64041 --- /dev/null +++ b/homeassistant/components/airly/sensor.py @@ -0,0 +1,150 @@ +"""Support for the Airly sensor service.""" +from homeassistant.const import ( + ATTR_ATTRIBUTION, + ATTR_DEVICE_CLASS, + CONF_NAME, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + PRESSURE_HPA, + TEMP_CELSIUS, +) +from homeassistant.helpers.entity import Entity + +from .const import ( + ATTR_API_HUMIDITY, + ATTR_API_PM1, + ATTR_API_PRESSURE, + ATTR_API_TEMPERATURE, + DATA_CLIENT, + DOMAIN, +) + +ATTRIBUTION = "Data provided by Airly" + +ATTR_ICON = "icon" +ATTR_LABEL = "label" +ATTR_UNIT = "unit" + +HUMI_PERCENT = "%" +VOLUME_MICROGRAMS_PER_CUBIC_METER = "µg/m³" + +SENSOR_TYPES = { + ATTR_API_PM1: { + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:blur", + ATTR_LABEL: ATTR_API_PM1, + ATTR_UNIT: VOLUME_MICROGRAMS_PER_CUBIC_METER, + }, + ATTR_API_HUMIDITY: { + ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + ATTR_ICON: None, + ATTR_LABEL: ATTR_API_HUMIDITY.capitalize(), + ATTR_UNIT: HUMI_PERCENT, + }, + ATTR_API_PRESSURE: { + ATTR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, + ATTR_ICON: None, + ATTR_LABEL: ATTR_API_PRESSURE.capitalize(), + ATTR_UNIT: PRESSURE_HPA, + }, + ATTR_API_TEMPERATURE: { + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ICON: None, + ATTR_LABEL: ATTR_API_TEMPERATURE.capitalize(), + ATTR_UNIT: TEMP_CELSIUS, + }, +} + + +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] + + data = hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] + + sensors = [] + for sensor in SENSOR_TYPES: + sensors.append(AirlySensor(data, name, sensor)) + async_add_entities(sensors, 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 AirlySensor(Entity): + """Define an Airly sensor.""" + + def __init__(self, airly, name, kind): + """Initialize.""" + self.airly = airly + self.data = airly.data + self._name = name + self.kind = kind + self._device_class = None + self._state = None + self._icon = None + self._unit_of_measurement = None + self._attrs = {ATTR_ATTRIBUTION: ATTRIBUTION} + + @property + def name(self): + """Return the name.""" + return f"{self._name} {SENSOR_TYPES[self.kind][ATTR_LABEL]}" + + @property + def state(self): + """Return the state.""" + self._state = self.data[self.kind] + if self.kind in [ATTR_API_PM1, ATTR_API_PRESSURE]: + self._state = round(self._state) + if self.kind in [ATTR_API_TEMPERATURE, ATTR_API_HUMIDITY]: + self._state = round(self._state, 1) + return self._state + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._attrs + + @property + def icon(self): + """Return the icon.""" + self._icon = SENSOR_TYPES[self.kind][ATTR_ICON] + return self._icon + + @property + def device_class(self): + """Return the device_class.""" + return SENSOR_TYPES[self.kind][ATTR_DEVICE_CLASS] + + @property + def unique_id(self): + """Return a unique_id for this entity.""" + return f"{self.airly.latitude}-{self.airly.longitude}-{self.kind.lower()}" + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return SENSOR_TYPES[self.kind][ATTR_UNIT] + + @property + def available(self): + """Return True if entity is available.""" + return bool(self.airly.data) + + async def async_update(self): + """Update the sensor.""" + await self.airly.async_update() + + if self.airly.data: + self.data = self.airly.data diff --git a/homeassistant/components/airly/strings.json b/homeassistant/components/airly/strings.json new file mode 100644 index 00000000000..116b6df83e6 --- /dev/null +++ b/homeassistant/components/airly/strings.json @@ -0,0 +1,22 @@ +{ + "config": { + "title": "Airly", + "step": { + "user": { + "title": "Airly", + "description": "Set up Airly air quality integration. To generate API key go to https://developer.airly.eu/register", + "data": { + "name": "Name of the integration", + "api_key": "Airly API key", + "latitude": "Latitude", + "longitude": "Longitude" + } + } + }, + "error": { + "name_exists": "Name already exists.", + "wrong_location": "No Airly measuring stations in this area.", + "auth": "API key is not correct." + } + } +} diff --git a/homeassistant/components/airvisual/sensor.py b/homeassistant/components/airvisual/sensor.py index 20e5196c0f1..888d6ae6ec9 100644 --- a/homeassistant/components/airvisual/sensor.py +++ b/homeassistant/components/airvisual/sensor.py @@ -1,7 +1,9 @@ """Support for AirVisual air quality sensors.""" -from logging import getLogger from datetime import timedelta +from logging import getLogger +from pyairvisual import Client +from pyairvisual.errors import AirVisualError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -14,8 +16,8 @@ from homeassistant.const import ( CONF_LONGITUDE, CONF_MONITORED_CONDITIONS, CONF_SCAN_INTERVAL, - CONF_STATE, CONF_SHOW_ON_MAP, + CONF_STATE, ) from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.entity import Entity @@ -97,7 +99,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 pyairvisual import Client city = config.get(CONF_CITY) state = config.get(CONF_STATE) @@ -249,7 +250,6 @@ class AirVisualData: async def _async_update(self): """Update AirVisual data.""" - from pyairvisual.errors import AirVisualError try: if self.city and self.state and self.country: diff --git a/homeassistant/components/aladdin_connect/cover.py b/homeassistant/components/aladdin_connect/cover.py index b3da4fb4cbc..4cfcd5403dd 100644 --- a/homeassistant/components/aladdin_connect/cover.py +++ b/homeassistant/components/aladdin_connect/cover.py @@ -1,21 +1,22 @@ """Platform for the Aladdin Connect cover component.""" import logging +from aladdin_connect import AladdinConnectClient import voluptuous as vol from homeassistant.components.cover import ( - CoverDevice, PLATFORM_SCHEMA, - SUPPORT_OPEN, SUPPORT_CLOSE, + SUPPORT_OPEN, + CoverDevice, ) from homeassistant.const import ( - CONF_USERNAME, CONF_PASSWORD, + CONF_USERNAME, STATE_CLOSED, - STATE_OPENING, STATE_CLOSING, STATE_OPEN, + STATE_OPENING, ) import homeassistant.helpers.config_validation as cv @@ -40,7 +41,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Aladdin Connect platform.""" - from aladdin_connect import AladdinConnectClient username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) diff --git a/homeassistant/components/alarm_control_panel/.translations/ca.json b/homeassistant/components/alarm_control_panel/.translations/ca.json new file mode 100644 index 00000000000..8d95d5f6485 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/.translations/ca.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "Activa {entity_name} fora", + "arm_home": "Activa {entity_name} a casa", + "arm_night": "Activa {entity_name} nocturn", + "disarm": "Desactiva {entity_name}", + "trigger": "Dispara {entity_name}" + } + } +} \ 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 new file mode 100644 index 00000000000..74e02e10de4 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/.translations/da.json @@ -0,0 +1,7 @@ +{ + "device_automation": { + "action_type": { + "trigger": "Udl\u00f8s {entity_name}" + } + } +} \ 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 new file mode 100644 index 00000000000..b8eeb1d2e8c --- /dev/null +++ b/homeassistant/components/alarm_control_panel/.translations/en.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "Arm {entity_name} away", + "arm_home": "Arm {entity_name} home", + "arm_night": "Arm {entity_name} night", + "disarm": "Disarm {entity_name}", + "trigger": "Trigger {entity_name}" + } + } +} \ 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 new file mode 100644 index 00000000000..273efeeaba5 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/.translations/es.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "Armar {entity_name} exterior", + "arm_home": "Armar {entity_name} modo casa", + "arm_night": "Armar {entity_name} por la noche", + "disarm": "Desarmar {entity_name}", + "trigger": "Lanzar {entity_name}" + } + } +} \ 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 new file mode 100644 index 00000000000..c3ba6db0c62 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/.translations/fr.json @@ -0,0 +1,11 @@ +{ + "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", + "disarm": "D\u00e9sarmer {entity_name}", + "trigger": "D\u00e9clencheur {entity_name}" + } + } +} \ 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 new file mode 100644 index 00000000000..e39967e9dac --- /dev/null +++ b/homeassistant/components/alarm_control_panel/.translations/it.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "Armare {entity_name} uscito", + "arm_home": "Armare {entity_name} casa", + "arm_night": "Armare {entity_name} notte", + "disarm": "Disarmare {entity_name}", + "trigger": "Attivazione {entity_name}" + } + } +} \ 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 new file mode 100644 index 00000000000..5d6caa5fe12 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/.translations/ko.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "{entity_name} \uc678\ucd9c\uacbd\ube44", + "arm_home": "{entity_name} \uc7ac\uc2e4\uacbd\ube44", + "arm_night": "{entity_name} \uc57c\uac04\uacbd\ube44", + "disarm": "{entity_name} \uacbd\ube44\ud574\uc81c", + "trigger": "{entity_name} \ud2b8\ub9ac\uac70" + } + } +} \ 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 new file mode 100644 index 00000000000..ff265a52c38 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/.translations/lb.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "{entity_name} fir \u00ebnnerwee uschalten", + "arm_home": "{entity_name} fir doheem uschalten", + "arm_night": "{entity_name} fir Nuecht uschalten", + "disarm": "{entity_name} entsch\u00e4rfen", + "trigger": "{entity_name} ausl\u00e9isen" + } + } +} \ 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 new file mode 100644 index 00000000000..86cacad9fd6 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/.translations/nl.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "action_type": { + "disarm": "Uitschakelen {entity_name}", + "trigger": "Trigger {entity_name}" + } + } +} \ 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 new file mode 100644 index 00000000000..93833f33d41 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/.translations/no.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "Aktiver {entity_name} borte", + "arm_home": "Aktiver {entity_name} hjemme", + "arm_night": "Aktiver {entity_name} natt", + "disarm": "Deaktiver {entity_name}", + "trigger": "Utl\u00f8ser {entity_name}" + } + } +} \ 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 new file mode 100644 index 00000000000..a5dc326c267 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/.translations/pl.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "uzbr\u00f3j (poza domem) {entity_name}", + "arm_home": "uzbr\u00f3j (w domu) {entity_name}", + "arm_night": "uzbr\u00f3j (noc) {entity_name}", + "disarm": "rozbr\u00f3j {entity_name}", + "trigger": "wyzw\u00f3l {entity_name}" + } + } +} \ 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 new file mode 100644 index 00000000000..1f7c994330d --- /dev/null +++ b/homeassistant/components/alarm_control_panel/.translations/pt-BR.json @@ -0,0 +1,7 @@ +{ + "device_automation": { + "action_type": { + "trigger": "Disparar {entidade_nome}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/pt.json b/homeassistant/components/alarm_control_panel/.translations/pt.json new file mode 100644 index 00000000000..90b9b1d43d5 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/.translations/pt.json @@ -0,0 +1,9 @@ +{ + "device_automation": { + "action_type": { + "arm_home": "Armar casa {entity_name}", + "arm_night": "Armar noite {entity_name}", + "disarm": "Desarmar {entity_name}" + } + } +} \ 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 new file mode 100644 index 00000000000..acea0ae7551 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/.translations/ru.json @@ -0,0 +1,7 @@ +{ + "device_automation": { + "action_type": { + "trigger": "{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 new file mode 100644 index 00000000000..9bf01fc62de --- /dev/null +++ b/homeassistant/components/alarm_control_panel/.translations/sl.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "Vklju\u010di {entity_name} zdoma", + "arm_home": "Vklju\u010di {entity_name} doma", + "arm_night": "Vklju\u010di {entity_name} no\u010d", + "disarm": "Razoro\u017ei {entity_name}", + "trigger": "Spro\u017ei {entity_name}" + } + } +} \ 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 new file mode 100644 index 00000000000..c52288802d1 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/.translations/zh-Hant.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "\u8a2d\u5b9a {entity_name} \u5916\u51fa\u6a21\u5f0f", + "arm_home": "\u8a2d\u5b9a {entity_name} \u8fd4\u5bb6\u6a21\u5f0f", + "arm_night": "\u8a2d\u5b9a {entity_name} \u591c\u9593\u6a21\u5f0f", + "disarm": "\u89e3\u9664 {entity_name}", + "trigger": "\u89f8\u767c {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/device_action.py b/homeassistant/components/alarm_control_panel/device_action.py new file mode 100644 index 00000000000..a3c2b482261 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/device_action.py @@ -0,0 +1,126 @@ +"""Provides device automations for Alarm control panel.""" +from typing import Optional, List +import voluptuous as vol + +from homeassistant.const import ( + ATTR_CODE, + ATTR_ENTITY_ID, + CONF_CODE, + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_ENTITY_ID, + CONF_TYPE, + SERVICE_ALARM_ARM_AWAY, + SERVICE_ALARM_ARM_HOME, + SERVICE_ALARM_ARM_NIGHT, + SERVICE_ALARM_DISARM, + SERVICE_ALARM_TRIGGER, +) +from homeassistant.core import HomeAssistant, Context +from homeassistant.helpers import entity_registry +import homeassistant.helpers.config_validation as cv +from . import ATTR_CODE_ARM_REQUIRED, DOMAIN + +ACTION_TYPES = {"arm_away", "arm_home", "arm_night", "disarm", "trigger"} + +ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( + { + vol.Required(CONF_TYPE): vol.In(ACTION_TYPES), + vol.Required(CONF_ENTITY_ID): cv.entity_domain(DOMAIN), + vol.Optional(CONF_CODE): cv.string, + } +) + + +async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device actions for Alarm control panel devices.""" + registry = await entity_registry.async_get_registry(hass) + actions = [] + + # 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 + + # 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", + } + ) + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "disarm", + } + ) + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "trigger", + } + ) + + return actions + + +async def async_call_action_from_config( + hass: HomeAssistant, config: dict, variables: dict, context: Optional[Context] +) -> None: + """Execute a device action.""" + config = ACTION_SCHEMA(config) + + service_data = {ATTR_ENTITY_ID: config[CONF_ENTITY_ID]} + if CONF_CODE in config: + service_data[ATTR_CODE] = config[CONF_CODE] + + if config[CONF_TYPE] == "arm_away": + service = SERVICE_ALARM_ARM_AWAY + elif config[CONF_TYPE] == "arm_home": + service = SERVICE_ALARM_ARM_HOME + elif config[CONF_TYPE] == "arm_night": + service = SERVICE_ALARM_ARM_NIGHT + elif config[CONF_TYPE] == "disarm": + service = SERVICE_ALARM_DISARM + elif config[CONF_TYPE] == "trigger": + service = SERVICE_ALARM_TRIGGER + + await hass.services.async_call( + DOMAIN, service, service_data, blocking=True, context=context + ) + + +async def async_get_action_capabilities(hass, config): + """List action capabilities.""" + state = hass.states.get(config[CONF_ENTITY_ID]) + code_required = state.attributes.get(ATTR_CODE_ARM_REQUIRED) if state else False + + if config[CONF_TYPE] == "trigger" or ( + config[CONF_TYPE] != "disarm" and not code_required + ): + return {} + + return {"extra_fields": vol.Schema({vol.Optional(CONF_CODE): str})} diff --git a/homeassistant/components/alarm_control_panel/strings.json b/homeassistant/components/alarm_control_panel/strings.json new file mode 100644 index 00000000000..f67635776dd --- /dev/null +++ b/homeassistant/components/alarm_control_panel/strings.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "Arm {entity_name} away", + "arm_home": "Arm {entity_name} home", + "arm_night": "Arm {entity_name} night", + "disarm": "Disarm {entity_name}", + "trigger": "Trigger {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarmdecoder/__init__.py b/homeassistant/components/alarmdecoder/__init__.py index e0ff80ae9fa..61cb0effe53 100644 --- a/homeassistant/components/alarmdecoder/__init__.py +++ b/homeassistant/components/alarmdecoder/__init__.py @@ -174,7 +174,7 @@ def setup(hass, config): hass.helpers.dispatcher.dispatcher_send(SIGNAL_ZONE_RESTORE, zone) def handle_rel_message(sender, message): - """Handle relay message from AlarmDecoder.""" + """Handle relay or zone expander message from AlarmDecoder.""" hass.helpers.dispatcher.dispatcher_send(SIGNAL_REL_MESSAGE, message) controller = False @@ -195,7 +195,7 @@ def setup(hass, config): controller.on_zone_fault += zone_fault_callback controller.on_zone_restore += zone_restore_callback controller.on_close += handle_closed_connection - controller.on_relay_changed += handle_rel_message + controller.on_expander_message += handle_rel_message hass.data[DATA_AD] = controller diff --git a/homeassistant/components/alarmdecoder/binary_sensor.py b/homeassistant/components/alarmdecoder/binary_sensor.py index bbcc4fd6eae..dc3f16b7d22 100644 --- a/homeassistant/components/alarmdecoder/binary_sensor.py +++ b/homeassistant/components/alarmdecoder/binary_sensor.py @@ -151,10 +151,15 @@ class AlarmDecoderBinarySensor(BinarySensorDevice): self.schedule_update_ha_state() def _rel_message_callback(self, message): - """Update relay state.""" + """Update relay / expander state.""" + if self._relay_addr == message.address and self._relay_chan == message.channel: _LOGGER.debug( - "Relay %d:%d value:%d", message.address, message.channel, message.value + "%s %d:%d value:%d", + "Relay" if message.type == message.RELAY else "ZoneExpander", + message.address, + message.channel, + message.value, ) self._state = message.value self.schedule_update_ha_state() diff --git a/homeassistant/components/alarmdotcom/alarm_control_panel.py b/homeassistant/components/alarmdotcom/alarm_control_panel.py index f80e8d6eb1e..07d69960e0b 100644 --- a/homeassistant/components/alarmdotcom/alarm_control_panel.py +++ b/homeassistant/components/alarmdotcom/alarm_control_panel.py @@ -2,6 +2,7 @@ import logging import re +from pyalarmdotcom import Alarmdotcom import voluptuous as vol import homeassistant.components.alarm_control_panel as alarm @@ -49,7 +50,6 @@ class AlarmDotCom(alarm.AlarmControlPanel): def __init__(self, hass, name, code, username, password): """Initialize the Alarm.com status.""" - from pyalarmdotcom import Alarmdotcom _LOGGER.debug("Setting up Alarm.com...") self._hass = hass diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index b8bd3841a78..deb83813dbc 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -5,6 +5,10 @@ from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, ATTR_UNIT_OF_MEASUREMENT, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_CUSTOM_BYPASS, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, STATE_LOCKED, STATE_OFF, STATE_ON, @@ -13,24 +17,26 @@ from homeassistant.const import ( STATE_UNKNOWN, ) import homeassistant.components.climate.const as climate +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, ) from .errors import UnsupportedProperty - _LOGGER = logging.getLogger(__name__) -class AlexaCapibility: +class AlexaCapability: """Base class for Alexa capability interfaces. The Smart Home Skills API defines a number of "capability interfaces", @@ -40,9 +46,10 @@ class AlexaCapibility: https://developer.amazon.com/docs/device-apis/message-guide.html """ - def __init__(self, entity): - """Initialize an Alexa capibility.""" + def __init__(self, entity, instance=None): + """Initialize an Alexa capability.""" self.entity = entity + self.instance = instance def name(self): """Return the Alexa API name of this interface.""" @@ -63,6 +70,11 @@ class AlexaCapibility: """Return True if properties can be retrieved.""" return False + @staticmethod + def properties_non_controllable(): + """Return True if non controllable.""" + return None + @staticmethod def get_property(name): """Read and return a property. @@ -79,23 +91,65 @@ class AlexaCapibility: """Applicable only to scenes.""" return None + @staticmethod + def capability_proactively_reported(): + """Return True if the capability is proactively reported. + + Set properties_proactively_reported() for proactively reported properties. + Applicable to DoorbellEventSource. + """ + return None + + @staticmethod + def capability_resources(): + """Applicable to ToggleController, RangeController, and ModeController interfaces.""" + return [] + + @staticmethod + def configuration(): + """Return the Configuration object.""" + return [] + def serialize_discovery(self): """Serialize according to the Discovery API.""" - result = { - "type": "AlexaInterface", - "interface": self.name(), - "version": "3", - "properties": { + result = {"type": "AlexaInterface", "interface": self.name(), "version": "3"} + + properties_supported = self.properties_supported() + if properties_supported: + result["properties"] = { "supported": self.properties_supported(), "proactivelyReported": self.properties_proactively_reported(), "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() + if capability_resources: + result["capabilityResources"] = capability_resources + + configuration = self.configuration() + if configuration: + result["configuration"] = configuration + + # pylint: disable=assignment-from-none + instance = self.instance + if instance is not None: + result["instance"] = instance + return result def serialize_properties(self): @@ -105,16 +159,51 @@ class AlexaCapibility: # pylint: disable=assignment-from-no-return prop_value = self.get_property(prop_name) if prop_value is not None: - yield { + result = { "name": prop_name, "namespace": self.name(), "value": prop_value, "timeOfSample": dt_util.utcnow().strftime(DATE_FORMAT), "uncertaintyInMilliseconds": 0, } + instance = self.instance + if instance is not None: + result["instance"] = instance + + 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 + + @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"}, + } + ) + + return friendly_names -class AlexaEndpointHealth(AlexaCapibility): +class AlexaEndpointHealth(AlexaCapability): """Implements Alexa.EndpointHealth. https://developer.amazon.com/docs/smarthome/state-reporting-for-a-smart-home-skill.html#report-state-when-alexa-requests-it @@ -151,7 +240,7 @@ class AlexaEndpointHealth(AlexaCapibility): return {"value": "OK"} -class AlexaPowerController(AlexaCapibility): +class AlexaPowerController(AlexaCapability): """Implements Alexa.PowerController. https://developer.amazon.com/docs/device-apis/alexa-powercontroller.html @@ -187,7 +276,7 @@ class AlexaPowerController(AlexaCapibility): return "ON" if is_on else "OFF" -class AlexaLockController(AlexaCapibility): +class AlexaLockController(AlexaCapability): """Implements Alexa.LockController. https://developer.amazon.com/docs/device-apis/alexa-lockcontroller.html @@ -221,7 +310,7 @@ class AlexaLockController(AlexaCapibility): return "JAMMED" -class AlexaSceneController(AlexaCapibility): +class AlexaSceneController(AlexaCapability): """Implements Alexa.SceneController. https://developer.amazon.com/docs/device-apis/alexa-scenecontroller.html @@ -237,7 +326,7 @@ class AlexaSceneController(AlexaCapibility): return "Alexa.SceneController" -class AlexaBrightnessController(AlexaCapibility): +class AlexaBrightnessController(AlexaCapability): """Implements Alexa.BrightnessController. https://developer.amazon.com/docs/device-apis/alexa-brightnesscontroller.html @@ -268,7 +357,7 @@ class AlexaBrightnessController(AlexaCapibility): return 0 -class AlexaColorController(AlexaCapibility): +class AlexaColorController(AlexaCapability): """Implements Alexa.ColorController. https://developer.amazon.com/docs/device-apis/alexa-colorcontroller.html @@ -300,7 +389,7 @@ class AlexaColorController(AlexaCapibility): } -class AlexaColorTemperatureController(AlexaCapibility): +class AlexaColorTemperatureController(AlexaCapability): """Implements Alexa.ColorTemperatureController. https://developer.amazon.com/docs/device-apis/alexa-colortemperaturecontroller.html @@ -329,7 +418,7 @@ class AlexaColorTemperatureController(AlexaCapibility): return None -class AlexaPercentageController(AlexaCapibility): +class AlexaPercentageController(AlexaCapability): """Implements Alexa.PercentageController. https://developer.amazon.com/docs/device-apis/alexa-percentagecontroller.html @@ -363,7 +452,7 @@ class AlexaPercentageController(AlexaCapibility): return 0 -class AlexaSpeaker(AlexaCapibility): +class AlexaSpeaker(AlexaCapability): """Implements Alexa.Speaker. https://developer.amazon.com/docs/device-apis/alexa-speaker.html @@ -374,7 +463,7 @@ class AlexaSpeaker(AlexaCapibility): return "Alexa.Speaker" -class AlexaStepSpeaker(AlexaCapibility): +class AlexaStepSpeaker(AlexaCapability): """Implements Alexa.StepSpeaker. https://developer.amazon.com/docs/device-apis/alexa-stepspeaker.html @@ -385,7 +474,7 @@ class AlexaStepSpeaker(AlexaCapibility): return "Alexa.StepSpeaker" -class AlexaPlaybackController(AlexaCapibility): +class AlexaPlaybackController(AlexaCapability): """Implements Alexa.PlaybackController. https://developer.amazon.com/docs/device-apis/alexa-playbackcontroller.html @@ -396,7 +485,7 @@ class AlexaPlaybackController(AlexaCapibility): return "Alexa.PlaybackController" -class AlexaInputController(AlexaCapibility): +class AlexaInputController(AlexaCapability): """Implements Alexa.InputController. https://developer.amazon.com/docs/device-apis/alexa-inputcontroller.html @@ -407,7 +496,7 @@ class AlexaInputController(AlexaCapibility): return "Alexa.InputController" -class AlexaTemperatureSensor(AlexaCapibility): +class AlexaTemperatureSensor(AlexaCapability): """Implements Alexa.TemperatureSensor. https://developer.amazon.com/docs/device-apis/alexa-temperaturesensor.html @@ -457,7 +546,7 @@ class AlexaTemperatureSensor(AlexaCapibility): return {"value": temp, "scale": API_TEMP_UNITS[unit]} -class AlexaContactSensor(AlexaCapibility): +class AlexaContactSensor(AlexaCapability): """Implements Alexa.ContactSensor. The Alexa.ContactSensor interface describes the properties and events used @@ -499,7 +588,7 @@ class AlexaContactSensor(AlexaCapibility): return "NOT_DETECTED" -class AlexaMotionSensor(AlexaCapibility): +class AlexaMotionSensor(AlexaCapability): """Implements Alexa.MotionSensor. https://developer.amazon.com/docs/device-apis/alexa-motionsensor.html @@ -536,7 +625,7 @@ class AlexaMotionSensor(AlexaCapibility): return "NOT_DETECTED" -class AlexaThermostatController(AlexaCapibility): +class AlexaThermostatController(AlexaCapability): """Implements Alexa.ThermostatController. https://developer.amazon.com/docs/device-apis/alexa-thermostatcontroller.html @@ -614,3 +703,378 @@ class AlexaThermostatController(AlexaCapibility): return None return {"value": temp, "scale": API_TEMP_UNITS[unit]} + + +class AlexaPowerLevelController(AlexaCapability): + """Implements Alexa.PowerLevelController. + + https://developer.amazon.com/docs/device-apis/alexa-powerlevelcontroller.html + """ + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.PowerLevelController" + + def properties_supported(self): + """Return what properties this entity supports.""" + return [{"name": "powerLevel"}] + + def properties_proactively_reported(self): + """Return True if properties asynchronously reported.""" + return True + + 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 != "powerLevel": + raise UnsupportedProperty(name) + + if self.entity.domain == fan.DOMAIN: + speed = self.entity.attributes.get(fan.ATTR_SPEED) + + return PERCENTAGE_FAN_MAP.get(speed, None) + + return None + + +class AlexaSecurityPanelController(AlexaCapability): + """Implements Alexa.SecurityPanelController. + + https://developer.amazon.com/docs/device-apis/alexa-securitypanelcontroller.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.SecurityPanelController" + + def properties_supported(self): + """Return what properties this entity supports.""" + return [{"name": "armState"}] + + def properties_proactively_reported(self): + """Return True if properties asynchronously reported.""" + return True + + 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 != "armState": + raise UnsupportedProperty(name) + + arm_state = self.entity.state + if arm_state == STATE_ALARM_ARMED_HOME: + return "ARMED_STAY" + if arm_state == STATE_ALARM_ARMED_AWAY: + return "ARMED_AWAY" + if arm_state == STATE_ALARM_ARMED_NIGHT: + return "ARMED_NIGHT" + if arm_state == STATE_ALARM_ARMED_CUSTOM_BYPASS: + return "ARMED_STAY" + return "DISARMED" + + def configuration(self): + """Return configuration object with supported authorization types.""" + code_format = self.entity.attributes.get(ATTR_CODE_FORMAT) + + if code_format == FORMAT_NUMBER: + return {"supportedAuthorizationTypes": [{"type": "FOUR_DIGIT_PIN"}]} + return None + + +class AlexaModeController(AlexaCapability): + """Implements Alexa.ModeController. + + https://developer.amazon.com/docs/device-apis/alexa-modecontroller.html + """ + + def __init__(self, entity, instance, non_controllable=False): + """Initialize the entity.""" + super().__init__(entity, instance) + self.properties_non_controllable = lambda: non_controllable + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.ModeController" + + def properties_supported(self): + """Return what properties this entity supports.""" + return [{"name": "mode"}] + + def properties_proactively_reported(self): + """Return True if properties asynchronously reported.""" + return True + + def properties_retrievable(self): + """Return True if properties can be retrieved.""" + + def get_property(self, name): + """Read and return a property.""" + if name != "mode": + raise UnsupportedProperty(name) + + if self.instance == f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}": + return self.entity.attributes.get(fan.ATTR_DIRECTION) + + return None + + def configuration(self): + """Return configuration with modeResources.""" + return self.serialize_mode_resources() + + def capability_resources(self): + """Return capabilityResources object.""" + capability_resources = [] + + if self.instance == f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}": + capability_resources = [ + {"type": Catalog.LABEL_ASSET, "value": Catalog.SETTING_DIRECTION} + ] + + return 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 mode_resources + + 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) + + return {"ordered": ordered, "supportedModes": mode_resources} + + +class AlexaRangeController(AlexaCapability): + """Implements Alexa.RangeController. + + https://developer.amazon.com/docs/device-apis/alexa-rangecontroller.html + """ + + def __init__(self, entity, instance, non_controllable=False): + """Initialize the entity.""" + super().__init__(entity, instance) + self.properties_non_controllable = lambda: non_controllable + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.RangeController" + + def properties_supported(self): + """Return what properties this entity supports.""" + return [{"name": "rangeValue"}] + + def properties_proactively_reported(self): + """Return True if properties asynchronously reported.""" + return True + + 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 != "rangeValue": + raise UnsupportedProperty(name) + + if self.instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}": + speed = self.entity.attributes.get(fan.ATTR_SPEED) + return RANGE_FAN_MAP.get(speed, 0) + + return None + + def configuration(self): + """Return configuration with presetResources.""" + return self.serialize_preset_resources() + + def capability_resources(self): + """Return capabilityResources object.""" + capability_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"]) + }, + } + ) + + return { + "supportedRange": { + "minimumValue": resources["minimumValue"], + "maximumValue": resources["maximumValue"], + "precision": resources["precision"], + }, + "presets": preset_resources, + } + + +class AlexaToggleController(AlexaCapability): + """Implements Alexa.ToggleController. + + https://developer.amazon.com/docs/device-apis/alexa-togglecontroller.html + """ + + def __init__(self, entity, instance, non_controllable=False): + """Initialize the entity.""" + super().__init__(entity, instance) + self.properties_non_controllable = lambda: non_controllable + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.ToggleController" + + def properties_supported(self): + """Return what properties this entity supports.""" + return [{"name": "toggleState"}] + + def properties_proactively_reported(self): + """Return True if properties asynchronously reported.""" + return True + + 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 != "toggleState": + raise UnsupportedProperty(name) + + 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" + + return None + + def capability_resources(self): + """Return capabilityResources object.""" + capability_resources = [] + + 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"}, + ] + + return capability_resources + + +class AlexaChannelController(AlexaCapability): + """Implements Alexa.ChannelController. + + https://developer.amazon.com/docs/device-apis/alexa-channelcontroller.html + """ + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.ChannelController" + + +class AlexaDoorbellEventSource(AlexaCapability): + """Implements Alexa.DoorbellEventSource. + + https://developer.amazon.com/docs/device-apis/alexa-doorbelleventsource.html + """ + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.DoorbellEventSource" + + def capability_proactively_reported(self): + """Return True for proactively reported capability.""" + return True diff --git a/homeassistant/components/alexa/const.py b/homeassistant/components/alexa/const.py index 83c7da41c16..8d1f0ac95a5 100644 --- a/homeassistant/components/alexa/const.py +++ b/homeassistant/components/alexa/const.py @@ -5,7 +5,6 @@ from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.components.climate import const as climate from homeassistant.components import fan - DOMAIN = "alexa" # Flash briefing constants @@ -62,7 +61,26 @@ API_THERMOSTAT_MODES = OrderedDict( ) API_THERMOSTAT_PRESETS = {climate.PRESET_ECO: "ECO"} -PERCENTAGE_FAN_MAP = {fan.SPEED_LOW: 33, fan.SPEED_MEDIUM: 66, fan.SPEED_HIGH: 100} +PERCENTAGE_FAN_MAP = { + fan.SPEED_OFF: 0, + fan.SPEED_LOW: 33, + fan.SPEED_MEDIUM: 66, + fan.SPEED_HIGH: 100, +} + +RANGE_FAN_MAP = { + fan.SPEED_OFF: 0, + fan.SPEED_LOW: 1, + fan.SPEED_MEDIUM: 2, + fan.SPEED_HIGH: 3, +} + +SPEED_FAN_MAP = { + 0: fan.SPEED_OFF, + 1: fan.SPEED_LOW, + 2: fan.SPEED_MEDIUM, + 3: fan.SPEED_HIGH, +} class Cause: @@ -96,3 +114,160 @@ class Cause: # Indicates that the event was caused by a voice interaction with Alexa. # For example a user speaking to their Echo device. VOICE_INTERACTION = "VOICE_INTERACTION" + + +class Catalog: + """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. + """ + + LABEL_ASSET = "asset" + LABEL_TEXT = "text" + + # 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" diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index 55b5878f667..d84848e9aba 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -14,6 +14,7 @@ from homeassistant.const import ( 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, @@ -33,21 +34,28 @@ from homeassistant.components import ( from .const import CONF_DESCRIPTION, CONF_DISPLAY_CATEGORIES from .capabilities import ( AlexaBrightnessController, + AlexaChannelController, AlexaColorController, AlexaColorTemperatureController, AlexaContactSensor, + AlexaDoorbellEventSource, AlexaEndpointHealth, AlexaInputController, AlexaLockController, + AlexaModeController, AlexaMotionSensor, AlexaPercentageController, AlexaPlaybackController, AlexaPowerController, + AlexaPowerLevelController, + AlexaRangeController, AlexaSceneController, + AlexaSecurityPanelController, AlexaSpeaker, AlexaStepSpeaker, AlexaTemperatureSensor, AlexaThermostatController, + AlexaToggleController, ) ENTITY_ADAPTERS = Registry() @@ -77,7 +85,7 @@ class DisplayCategory: DOOR = "DOOR" # Indicates a doorbell. - DOOR_BELL = "DOORBELL" + DOORBELL = "DOORBELL" # Indicates a fan. FAN = "FAN" @@ -344,6 +352,20 @@ class FanCapabilities(AlexaEntity): supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if supported & fan.SUPPORT_SET_SPEED: yield AlexaPercentageController(self.entity) + yield AlexaPowerLevelController(self.entity) + 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}" + ) + if supported & fan.SUPPORT_DIRECTION: + yield AlexaModeController( + self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}" + ) + yield AlexaEndpointHealth(self.hass, self.entity) @@ -400,6 +422,9 @@ class MediaPlayerCapabilities(AlexaEntity): if supported & media_player.SUPPORT_SELECT_SOURCE: yield AlexaInputController(self.entity) + if supported & media_player.const.SUPPORT_PLAY_MEDIA: + yield AlexaChannelController(self.entity) + @ENTITY_ADAPTERS.register(scene.DOMAIN) class SceneCapabilities(AlexaEntity): @@ -476,6 +501,11 @@ class BinarySensorCapabilities(AlexaEntity): elif sensor_type is self.TYPE_MOTION: yield AlexaMotionSensor(self.hass, self.entity) + 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) + yield AlexaEndpointHealth(self.hass, self.entity) def get_type(self): @@ -485,3 +515,18 @@ class BinarySensorCapabilities(AlexaEntity): return self.TYPE_CONTACT if attrs.get(ATTR_DEVICE_CLASS) == "motion": return self.TYPE_MOTION + + +@ENTITY_ADAPTERS.register(alarm_control_panel.DOMAIN) +class AlarmControlPanelCapabilities(AlexaEntity): + """Class to represent Alarm capabilities.""" + + def default_display_categories(self): + """Return the display categories for this entity.""" + return [DisplayCategory.SECURITY_PANEL] + + def interfaces(self): + """Yield the supported interfaces.""" + if not self.entity.attributes.get("code_arm_required"): + yield AlexaSecurityPanelController(self.hass, self.entity) + yield AlexaEndpointHealth(self.hass, self.entity) diff --git a/homeassistant/components/alexa/errors.py b/homeassistant/components/alexa/errors.py index 8c2fa692267..b0600313fc2 100644 --- a/homeassistant/components/alexa/errors.py +++ b/homeassistant/components/alexa/errors.py @@ -83,3 +83,31 @@ class AlexaBridgeUnreachableError(AlexaError): namespace = "Alexa" error_type = "BRIDGE_UNREACHABLE" + + +class AlexaSecurityPanelUnauthorizedError(AlexaError): + """Class to represent SecurityPanelController Unauthorized errors.""" + + namespace = "Alexa.SecurityPanelController" + error_type = "UNAUTHORIZED" + + +class AlexaSecurityPanelAuthorizationRequired(AlexaError): + """Class to represent SecurityPanelController AuthorizationRequired errors.""" + + namespace = "Alexa.SecurityPanelController" + error_type = "AUTHORIZATION_REQUIRED" + + +class AlexaAlreadyInOperationError(AlexaError): + """Class to represent AlreadyInOperation errors.""" + + namespace = "Alexa" + error_type = "ALREADY_IN_OPERATION" + + +class AlexaInvalidDirectiveError(AlexaError): + """Class to represent InvalidDirective errors.""" + + namespace = "Alexa" + error_type = "INVALID_DIRECTIVE" diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index c72101460c4..331990dc4a4 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -9,6 +9,11 @@ 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, + SERVICE_ALARM_DISARM, SERVICE_LOCK, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, @@ -31,10 +36,21 @@ import homeassistant.util.dt as dt_util from homeassistant.util.decorator import Registry from homeassistant.util.temperature import convert as convert_temperature -from .const import API_TEMP_UNITS, API_THERMOSTAT_MODES, API_THERMOSTAT_PRESETS, Cause +from .const import ( + API_TEMP_UNITS, + API_THERMOSTAT_MODES, + API_THERMOSTAT_PRESETS, + Cause, + PERCENTAGE_FAN_MAP, + RANGE_FAN_MAP, + SPEED_FAN_MAP, +) from .entities import async_get_entities from .errors import ( + AlexaInvalidDirectiveError, AlexaInvalidValueError, + AlexaSecurityPanelAuthorizationRequired, + AlexaSecurityPanelUnauthorizedError, AlexaTempRangeError, AlexaUnsupportedThermostatModeError, ) @@ -349,15 +365,7 @@ async def async_api_adjust_percentage(hass, config, directive, context): if entity.domain == fan.DOMAIN: service = fan.SERVICE_SET_SPEED speed = entity.attributes.get(fan.ATTR_SPEED) - - if speed == "off": - current = 0 - elif speed == "low": - current = 33 - elif speed == "medium": - current = 66 - elif speed == "high": - current = 100 + current = PERCENTAGE_FAN_MAP.get(speed, 100) # set percentage percentage = max(0, percentage_delta + current) @@ -405,7 +413,6 @@ async def async_api_lock(hass, config, directive, context): return response -# Not supported by Alexa yet @HANDLERS.register(("Alexa.LockController", "Unlock")) async def async_api_unlock(hass, config, directive, context): """Process an unlock request.""" @@ -418,7 +425,12 @@ async def async_api_unlock(hass, config, directive, context): context=context, ) - return directive.response() + response = directive.response() + response.add_context_property( + {"namespace": "Alexa.LockController", "name": "lockState", "value": "UNLOCKED"} + ) + + return response @HANDLERS.register(("Alexa.Speaker", "SetVolume")) @@ -509,20 +521,28 @@ async def async_api_adjust_volume_step(hass, config, directive, context): """Process an adjust volume step request.""" # media_player volume up/down service does not support specifying steps # each component handles it differently e.g. via config. - # For now we use the volumeSteps returned to figure out if we - # should step up/down - volume_step = directive.payload["volumeSteps"] + # This workaround will simply call the volume up/Volume down the amount of steps asked for + # When no steps are called in the request, Alexa sends a default of 10 steps which for most + # purposes is too high. The default is set 1 in this case. entity = directive.entity + volume_int = int(directive.payload["volumeSteps"]) + is_default = bool(directive.payload["volumeStepsDefault"]) + default_steps = 1 + + if volume_int < 0: + service_volume = SERVICE_VOLUME_DOWN + if is_default: + volume_int = -default_steps + else: + service_volume = SERVICE_VOLUME_UP + if is_default: + volume_int = default_steps data = {ATTR_ENTITY_ID: entity.entity_id} - if volume_step > 0: + for _ in range(0, abs(volume_int)): await hass.services.async_call( - entity.domain, SERVICE_VOLUME_UP, data, blocking=False, context=context - ) - elif volume_step < 0: - await hass.services.async_call( - entity.domain, SERVICE_VOLUME_DOWN, data, blocking=False, context=context + entity.domain, service_volume, data, blocking=False, context=context ) return directive.response() @@ -534,7 +554,6 @@ async def async_api_set_mute(hass, config, directive, context): """Process a set mute request.""" mute = bool(directive.payload["mute"]) entity = directive.entity - data = { ATTR_ENTITY_ID: entity.entity_id, media_player.const.ATTR_MEDIA_VOLUME_MUTED: mute, @@ -779,3 +798,373 @@ async def async_api_set_thermostat_mode(hass, config, directive, context): async def async_api_reportstate(hass, config, directive, context): """Process a ReportState request.""" return directive.response(name="StateReport") + + +@HANDLERS.register(("Alexa.PowerLevelController", "SetPowerLevel")) +async def async_api_set_power_level(hass, config, directive, context): + """Process a SetPowerLevel request.""" + entity = directive.entity + percentage = int(directive.payload["powerLevel"]) + service = None + data = {ATTR_ENTITY_ID: entity.entity_id} + + if entity.domain == fan.DOMAIN: + service = fan.SERVICE_SET_SPEED + speed = "off" + + if percentage <= 33: + speed = "low" + elif percentage <= 66: + speed = "medium" + else: + speed = "high" + + data[fan.ATTR_SPEED] = speed + + await hass.services.async_call( + entity.domain, service, data, blocking=False, context=context + ) + + return directive.response() + + +@HANDLERS.register(("Alexa.PowerLevelController", "AdjustPowerLevel")) +async def async_api_adjust_power_level(hass, config, directive, context): + """Process an AdjustPowerLevel request.""" + entity = directive.entity + percentage_delta = int(directive.payload["powerLevelDelta"]) + service = None + data = {ATTR_ENTITY_ID: entity.entity_id} + + if entity.domain == fan.DOMAIN: + service = fan.SERVICE_SET_SPEED + speed = entity.attributes.get(fan.ATTR_SPEED) + current = PERCENTAGE_FAN_MAP.get(speed, 100) + + # set percentage + percentage = max(0, percentage_delta + current) + speed = "off" + + if percentage <= 33: + speed = "low" + elif percentage <= 66: + speed = "medium" + else: + speed = "high" + + data[fan.ATTR_SPEED] = speed + + await hass.services.async_call( + entity.domain, service, data, blocking=False, context=context + ) + + return directive.response() + + +@HANDLERS.register(("Alexa.SecurityPanelController", "Arm")) +async def async_api_arm(hass, config, directive, context): + """Process a Security Panel Arm request.""" + entity = directive.entity + service = None + arm_state = directive.payload["armState"] + data = {ATTR_ENTITY_ID: entity.entity_id} + + if entity.state != STATE_ALARM_DISARMED: + msg = "You must disarm the system before you can set the requested arm state." + raise AlexaSecurityPanelAuthorizationRequired(msg) + + if arm_state == "ARMED_AWAY": + service = SERVICE_ALARM_ARM_AWAY + if arm_state == "ARMED_STAY": + service = SERVICE_ALARM_ARM_HOME + if arm_state == "ARMED_NIGHT": + service = SERVICE_ALARM_ARM_NIGHT + + await hass.services.async_call( + entity.domain, service, data, blocking=False, context=context + ) + + response = directive.response( + name="Arm.Response", namespace="Alexa.SecurityPanelController" + ) + + response.add_context_property( + { + "name": "armState", + "namespace": "Alexa.SecurityPanelController", + "value": arm_state, + } + ) + + return response + + +@HANDLERS.register(("Alexa.SecurityPanelController", "Disarm")) +async def async_api_disarm(hass, config, directive, context): + """Process a Security Panel Disarm request.""" + entity = directive.entity + data = {ATTR_ENTITY_ID: entity.entity_id} + + payload = directive.payload + if "authorization" in payload: + value = payload["authorization"]["value"] + if payload["authorization"]["type"] == "FOUR_DIGIT_PIN": + data["code"] = value + + if not await hass.services.async_call( + entity.domain, SERVICE_ALARM_DISARM, data, blocking=True, context=context + ): + msg = "Invalid Code" + raise AlexaSecurityPanelUnauthorizedError(msg) + + response = directive.response() + response.add_context_property( + { + "name": "armState", + "namespace": "Alexa.SecurityPanelController", + "value": "DISARMED", + } + ) + + return response + + +@HANDLERS.register(("Alexa.ModeController", "SetMode")) +async def async_api_set_mode(hass, config, directive, context): + """Process a next request.""" + entity = directive.entity + instance = directive.instance + domain = entity.domain + service = None + data = {ATTR_ENTITY_ID: entity.entity_id} + mode = directive.payload["mode"] + + if domain != fan.DOMAIN: + msg = "Entity does not support directive" + raise AlexaInvalidDirectiveError(msg) + + if instance == f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}": + mode, direction = mode.split(".") + if direction in [fan.DIRECTION_REVERSE, fan.DIRECTION_FORWARD]: + service = fan.SERVICE_SET_DIRECTION + data[fan.ATTR_DIRECTION] = direction + + await hass.services.async_call( + domain, service, data, blocking=False, context=context + ) + + return directive.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. + """ + 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() + + +@HANDLERS.register(("Alexa.ToggleController", "TurnOn")) +async def async_api_toggle_on(hass, config, directive, context): + """Process a toggle on request.""" + entity = directive.entity + instance = directive.instance + domain = entity.domain + service = None + data = {ATTR_ENTITY_ID: entity.entity_id} + + if domain != fan.DOMAIN: + msg = "Entity does not support directive" + raise AlexaInvalidDirectiveError(msg) + + if instance == f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}": + service = fan.SERVICE_OSCILLATE + data[fan.ATTR_OSCILLATING] = True + + await hass.services.async_call( + domain, service, data, blocking=False, context=context + ) + + return directive.response() + + +@HANDLERS.register(("Alexa.ToggleController", "TurnOff")) +async def async_api_toggle_off(hass, config, directive, context): + """Process a toggle off request.""" + entity = directive.entity + instance = directive.instance + domain = entity.domain + service = None + data = {ATTR_ENTITY_ID: entity.entity_id} + + if domain != fan.DOMAIN: + msg = "Entity does not support directive" + raise AlexaInvalidDirectiveError(msg) + + if instance == f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}": + service = fan.SERVICE_OSCILLATE + data[fan.ATTR_OSCILLATING] = False + + await hass.services.async_call( + domain, service, data, blocking=False, context=context + ) + + return directive.response() + + +@HANDLERS.register(("Alexa.RangeController", "SetRangeValue")) +async def async_api_set_range(hass, config, directive, context): + """Process a next request.""" + entity = directive.entity + instance = directive.instance + 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) + + if instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}": + service = fan.SERVICE_SET_SPEED + speed = SPEED_FAN_MAP.get(range_value, None) + + if not speed: + msg = "Entity does not support value" + raise AlexaInvalidValueError(msg) + + if speed == fan.SPEED_OFF: + service = fan.SERVICE_TURN_OFF + + data[fan.ATTR_SPEED] = speed + + await hass.services.async_call( + domain, service, data, blocking=False, context=context + ) + + return directive.response() + + +@HANDLERS.register(("Alexa.RangeController", "AdjustRangeValue")) +async def async_api_adjust_range(hass, config, directive, context): + """Process a next request.""" + entity = directive.entity + instance = directive.instance + domain = entity.domain + service = None + data = {ATTR_ENTITY_ID: entity.entity_id} + range_delta = int(directive.payload["rangeValueDelta"]) + + if instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}": + 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) + + if speed == fan.SPEED_OFF: + service = fan.SERVICE_TURN_OFF + + data[fan.ATTR_SPEED] = speed + + await hass.services.async_call( + domain, service, data, blocking=False, context=context + ) + + return directive.response() + + +@HANDLERS.register(("Alexa.ChannelController", "ChangeChannel")) +async def async_api_changechannel(hass, config, directive, context): + """Process a change channel request.""" + channel = "0" + entity = directive.entity + payload = directive.payload["channel"] + payload_name = "number" + + if "number" in payload: + channel = payload["number"] + payload_name = "number" + elif "callSign" in payload: + channel = payload["callSign"] + payload_name = "callSign" + elif "affiliateCallSign" in payload: + channel = payload["affiliateCallSign"] + payload_name = "affiliateCallSign" + elif "uri" in payload: + channel = payload["uri"] + payload_name = "uri" + + data = { + ATTR_ENTITY_ID: entity.entity_id, + media_player.const.ATTR_MEDIA_CONTENT_ID: channel, + media_player.const.ATTR_MEDIA_CONTENT_TYPE: media_player.const.MEDIA_TYPE_CHANNEL, + } + + await hass.services.async_call( + entity.domain, + media_player.const.SERVICE_PLAY_MEDIA, + data, + blocking=False, + context=context, + ) + + response = directive.response() + + response.add_context_property( + { + "namespace": "Alexa.ChannelController", + "name": "channel", + "value": {payload_name: channel}, + } + ) + + return response + + +@HANDLERS.register(("Alexa.ChannelController", "SkipChannels")) +async def async_api_skipchannel(hass, config, directive, context): + """Process a skipchannel request.""" + channel = int(directive.payload["channelCount"]) + entity = directive.entity + + data = {ATTR_ENTITY_ID: entity.entity_id} + + if channel < 0: + service_media = SERVICE_MEDIA_PREVIOUS_TRACK + else: + service_media = SERVICE_MEDIA_NEXT_TRACK + + for _ in range(0, abs(channel)): + await hass.services.async_call( + entity.domain, service_media, data, blocking=False, context=context + ) + + response = directive.response() + + response.add_context_property( + { + "namespace": "Alexa.ChannelController", + "name": "channel", + "value": {"number": ""}, + } + ) + + return response diff --git a/homeassistant/components/alexa/manifest.json b/homeassistant/components/alexa/manifest.json index 9db7e270e61..ad0f1c33d49 100644 --- a/homeassistant/components/alexa/manifest.json +++ b/homeassistant/components/alexa/manifest.json @@ -4,5 +4,8 @@ "documentation": "https://www.home-assistant.io/integrations/alexa", "requirements": [], "dependencies": ["http"], - "codeowners": ["@home-assistant/cloud"] + "codeowners": [ + "@home-assistant/cloud", + "@ochlocracy" + ] } diff --git a/homeassistant/components/alexa/messages.py b/homeassistant/components/alexa/messages.py index 3195656ed09..cb78f269f8f 100644 --- a/homeassistant/components/alexa/messages.py +++ b/homeassistant/components/alexa/messages.py @@ -28,7 +28,7 @@ class AlexaDirective: self.payload = self._directive[API_PAYLOAD] self.has_endpoint = API_ENDPOINT in self._directive - self.entity = self.entity_id = self.endpoint = None + self.entity = self.entity_id = self.endpoint = self.instance = None def load_entity(self, hass, config): """Set attributes related to the entity for this request. @@ -38,6 +38,7 @@ class AlexaDirective: - entity - entity_id - endpoint + - instance (when header includes instance property) Behavior when self.has_endpoint is False is undefined. @@ -52,6 +53,8 @@ class AlexaDirective: raise AlexaInvalidEndpointError(_endpoint_id) self.endpoint = ENTITY_ADAPTERS[self.entity.domain](hass, config, self.entity) + if "instance" in self._directive[API_HEADER]: + self.instance = self._directive[API_HEADER]["instance"] def response(self, name="Response", namespace="Alexa", payload=None): """Create an API formatted response. diff --git a/homeassistant/components/alexa/state_report.py b/homeassistant/components/alexa/state_report.py index 42c16919a45..b5e1b741f0c 100644 --- a/homeassistant/components/alexa/state_report.py +++ b/homeassistant/components/alexa/state_report.py @@ -6,7 +6,8 @@ import logging import aiohttp import async_timeout -from homeassistant.const import MATCH_ALL +import homeassistant.util.dt as dt_util +from homeassistant.const import MATCH_ALL, STATE_ON from .const import API_CHANGE, Cause from .entities import ENTITY_ADAPTERS @@ -45,6 +46,14 @@ async def async_enable_proactive_mode(hass, smart_home_config): hass, smart_home_config, alexa_changed_entity ) return + if ( + interface.name() == "Alexa.DoorbellEventSource" + and new_state.state == STATE_ON + ): + await async_send_doorbell_event_message( + hass, smart_home_config, alexa_changed_entity + ) + return return hass.helpers.event.async_track_state_change( MATCH_ALL, async_entity_state_listener @@ -184,3 +193,58 @@ async def async_send_delete_message(hass, config, entity_ids): return await session.post( config.endpoint, headers=headers, json=message_serialized, allow_redirects=True ) + + +async def async_send_doorbell_event_message(hass, config, alexa_entity): + """Send a DoorbellPress event message for an Alexa entity. + + https://developer.amazon.com/docs/smarthome/send-events-to-the-alexa-event-gateway.html + """ + token = await config.async_get_access_token() + + headers = {"Authorization": f"Bearer {token}"} + + endpoint = alexa_entity.alexa_id() + + message = AlexaResponse( + name="DoorbellPress", + namespace="Alexa.DoorbellEventSource", + payload={ + "cause": {"type": Cause.PHYSICAL_INTERACTION}, + "timestamp": f"{dt_util.utcnow().replace(tzinfo=None).isoformat()}Z", + }, + ) + + message.set_endpoint_full(token, endpoint) + + message_serialized = message.serialize() + session = hass.helpers.aiohttp_client.async_get_clientsession() + + try: + with async_timeout.timeout(DEFAULT_TIMEOUT): + response = await session.post( + config.endpoint, + headers=headers, + json=message_serialized, + allow_redirects=True, + ) + + except (asyncio.TimeoutError, aiohttp.ClientError): + _LOGGER.error("Timeout sending report to Alexa.") + return + + response_text = await response.text() + + _LOGGER.debug("Sent: %s", json.dumps(message_serialized)) + _LOGGER.debug("Received (%s): %s", response.status, response_text) + + if response.status == 202: + return + + response_json = json.loads(response_text) + + _LOGGER.error( + "Error when sending DoorbellPress event to Alexa: %s: %s", + response_json["payload"]["code"], + response_json["payload"]["description"], + ) diff --git a/homeassistant/components/alpha_vantage/manifest.json b/homeassistant/components/alpha_vantage/manifest.json index 9ac8d1ea1e0..1213bb12e74 100644 --- a/homeassistant/components/alpha_vantage/manifest.json +++ b/homeassistant/components/alpha_vantage/manifest.json @@ -3,7 +3,7 @@ "name": "Alpha vantage", "documentation": "https://www.home-assistant.io/integrations/alpha_vantage", "requirements": [ - "alpha_vantage==2.1.0" + "alpha_vantage==2.1.1" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/alpha_vantage/sensor.py b/homeassistant/components/alpha_vantage/sensor.py index 188567e4cf4..da29e4e25e1 100644 --- a/homeassistant/components/alpha_vantage/sensor.py +++ b/homeassistant/components/alpha_vantage/sensor.py @@ -3,6 +3,8 @@ from datetime import timedelta import logging import voluptuous as vol +from alpha_vantage.timeseries import TimeSeries +from alpha_vantage.foreignexchange import ForeignExchange from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_CURRENCY, CONF_NAME @@ -62,15 +64,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Alpha Vantage sensor.""" - from alpha_vantage.timeseries import TimeSeries - from alpha_vantage.foreignexchange import ForeignExchange - api_key = config.get(CONF_API_KEY) symbols = config.get(CONF_SYMBOLS, []) conversions = config.get(CONF_FOREIGN_EXCHANGE, []) if not symbols and not conversions: - msg = "Warning: No symbols or currencies configured." + msg = "No symbols or currencies configured." hass.components.persistent_notification.create(msg, "Sensor alpha_vantage") _LOGGER.warning(msg) return diff --git a/homeassistant/components/amazon_polly/tts.py b/homeassistant/components/amazon_polly/tts.py index 64b8b71457c..3acfd472320 100644 --- a/homeassistant/components/amazon_polly/tts.py +++ b/homeassistant/components/amazon_polly/tts.py @@ -1,5 +1,6 @@ """Support for the Amazon Polly text to speech service.""" import logging +import boto3 import voluptuous as vol @@ -156,8 +157,6 @@ def get_engine(hass, config): config[CONF_SAMPLE_RATE] = sample_rate - import boto3 - profile = config.get(CONF_PROFILE_NAME) if profile is not None: diff --git a/homeassistant/components/ambiclimate/.translations/nn.json b/homeassistant/components/ambiclimate/.translations/nn.json new file mode 100644 index 00000000000..ce8a3ed9db6 --- /dev/null +++ b/homeassistant/components/ambiclimate/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Ambiclimate" + } +} \ No newline at end of file diff --git a/homeassistant/components/ambiclimate/.translations/ru.json b/homeassistant/components/ambiclimate/.translations/ru.json index 5a816bce140..ba667ea7b9a 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 Ambi Climate \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\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.", "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": { diff --git a/homeassistant/components/ambient_station/.translations/nn.json b/homeassistant/components/ambient_station/.translations/nn.json new file mode 100644 index 00000000000..0f878b363c9 --- /dev/null +++ b/homeassistant/components/ambient_station/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Ambient PWS" + } +} \ No newline at end of file diff --git a/homeassistant/components/ambient_station/.translations/ru.json b/homeassistant/components/ambient_station/.translations/ru.json index 2d7964f18eb..3a7c405ea4c 100644 --- a/homeassistant/components/ambient_station/.translations/ru.json +++ b/homeassistant/components/ambient_station/.translations/ru.json @@ -2,8 +2,8 @@ "config": { "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" + "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." }, "step": { "user": { diff --git a/homeassistant/components/amcrest/__init__.py b/homeassistant/components/amcrest/__init__.py index f915872abf0..d49104a0b26 100644 --- a/homeassistant/components/amcrest/__init__.py +++ b/homeassistant/components/amcrest/__init__.py @@ -167,6 +167,8 @@ class AmcrestChecker(Http): offline = not self.available if offline and was_online: _LOGGER.error("%s camera offline: Too many errors", self._wrap_name) + with self._token_lock: + self._token = None dispatcher_send( self._hass, service_signal(SERVICE_UPDATE, self._wrap_name) ) diff --git a/homeassistant/components/amcrest/camera.py b/homeassistant/components/amcrest/camera.py index f75a5adbe9c..e9e1e2b5f84 100644 --- a/homeassistant/components/amcrest/camera.py +++ b/homeassistant/components/amcrest/camera.py @@ -2,19 +2,20 @@ import asyncio from datetime import timedelta import logging -from urllib3.exceptions import HTTPError from amcrest import AmcrestError +from haffmpeg.camera import CameraMjpeg +from urllib3.exceptions import HTTPError import voluptuous as vol from homeassistant.components.camera import ( - Camera, CAMERA_SERVICE_SCHEMA, SUPPORT_ON_OFF, SUPPORT_STREAM, + Camera, ) from homeassistant.components.ffmpeg import DATA_FFMPEG -from homeassistant.const import CONF_NAME, STATE_ON, STATE_OFF +from homeassistant.const import CONF_NAME, STATE_OFF, STATE_ON from homeassistant.helpers.aiohttp_client import ( async_aiohttp_proxy_stream, async_aiohttp_proxy_web, @@ -159,7 +160,6 @@ class AmcrestCam(Camera): return await async_aiohttp_proxy_web(self.hass, request, stream_coro) # streaming via ffmpeg - from haffmpeg.camera import CameraMjpeg streaming_url = self._rtsp_url stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop) diff --git a/homeassistant/components/ampio/air_quality.py b/homeassistant/components/ampio/air_quality.py index e63f59839a8..c925909a9a8 100644 --- a/homeassistant/components/ampio/air_quality.py +++ b/homeassistant/components/ampio/air_quality.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +from asmog import AmpioSmog import voluptuous as vol from homeassistant.components.air_quality import PLATFORM_SCHEMA, AirQualityEntity @@ -23,7 +24,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Ampio Smog air quality platform.""" - from asmog import AmpioSmog name = config.get(CONF_NAME) station_id = config[CONF_STATION_ID] diff --git a/homeassistant/components/android_ip_webcam/__init__.py b/homeassistant/components/android_ip_webcam/__init__.py index 33362bd37cc..1f9df527c28 100644 --- a/homeassistant/components/android_ip_webcam/__init__.py +++ b/homeassistant/components/android_ip_webcam/__init__.py @@ -1,34 +1,35 @@ """Support for Android IP Webcam.""" import asyncio -import logging from datetime import timedelta +import logging +from pydroid_ipcam import PyDroidIPCam import voluptuous as vol -from homeassistant.core import callback +from homeassistant.components.mjpeg.camera import CONF_MJPEG_URL, CONF_STILL_IMAGE_URL from homeassistant.const import ( - CONF_NAME, CONF_HOST, - CONF_PORT, - CONF_USERNAME, + CONF_NAME, CONF_PASSWORD, + CONF_PLATFORM, + CONF_PORT, + CONF_SCAN_INTERVAL, CONF_SENSORS, CONF_SWITCHES, CONF_TIMEOUT, - CONF_SCAN_INTERVAL, - CONF_PLATFORM, + CONF_USERNAME, ) -from homeassistant.helpers.aiohttp_client import async_get_clientsession +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, async_dispatcher_connect, + async_dispatcher_send, ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util.dt import utcnow -from homeassistant.components.mjpeg.camera import CONF_MJPEG_URL, CONF_STILL_IMAGE_URL _LOGGER = logging.getLogger(__name__) @@ -187,7 +188,6 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass, config): """Set up the IP Webcam component.""" - from pydroid_ipcam import PyDroidIPCam webcams = hass.data[DATA_IP_WEBCAM] = {} websession = async_get_clientsession(hass) diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index e84ed35c763..9ec993b9f91 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.4", - "androidtv==0.0.30" + "adb-shell==0.0.7", + "androidtv==0.0.32" ], "dependencies": [], "codeowners": ["@JeffLIrion"] diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index fcf4950f5e2..62ae93f96e4 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -1,8 +1,10 @@ """Support for functionality to interact with Android TV / Fire TV devices.""" import functools import logging +import os import voluptuous as vol +from adb_shell.auth.keygen import keygen from adb_shell.exceptions import ( InvalidChecksumError, InvalidCommandError, @@ -40,6 +42,7 @@ from homeassistant.const import ( ) from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.storage import STORAGE_DIR ANDROIDTV_DOMAIN = "androidtv" @@ -133,27 +136,39 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if CONF_ADB_SERVER_IP not in config: # Use "adb_shell" (Python ADB implementation) - adb_log = "using Python ADB implementation " + ( - f"with adbkey='{config[CONF_ADBKEY]}'" - if CONF_ADBKEY in config - else "without adbkey authentication" - ) - if CONF_ADBKEY in config: + if CONF_ADBKEY not in config: + # Generate ADB key files (if they don't exist) + adbkey = hass.config.path(STORAGE_DIR, "androidtv_adbkey") + if not os.path.isfile(adbkey): + keygen(adbkey) + + adb_log = f"using Python ADB implementation with adbkey='{adbkey}'" + + aftv = setup( + host, + adbkey, + device_class=config[CONF_DEVICE_CLASS], + state_detection_rules=config[CONF_STATE_DETECTION_RULES], + auth_timeout_s=10.0, + ) + + else: + adb_log = ( + f"using Python ADB implementation with adbkey='{config[CONF_ADBKEY]}'" + ) + aftv = setup( host, config[CONF_ADBKEY], device_class=config[CONF_DEVICE_CLASS], state_detection_rules=config[CONF_STATE_DETECTION_RULES], + auth_timeout_s=10.0, ) - else: - aftv = setup( - host, - device_class=config[CONF_DEVICE_CLASS], - state_detection_rules=config[CONF_STATE_DETECTION_RULES], - ) else: # Use "pure-python-adb" (communicate with ADB server) + adb_log = f"using ADB server at {config[CONF_ADB_SERVER_IP]}:{config[CONF_ADB_SERVER_PORT]}" + aftv = setup( host, adb_server_ip=config[CONF_ADB_SERVER_IP], @@ -161,7 +176,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): device_class=config[CONF_DEVICE_CLASS], state_detection_rules=config[CONF_STATE_DETECTION_RULES], ) - adb_log = f"using ADB server at {config[CONF_ADB_SERVER_IP]}:{config[CONF_ADB_SERVER_PORT]}" if not aftv.available: # Determine the name that will be used for the device in the log @@ -257,7 +271,7 @@ def adb_decorator(override_available=False): "establishing attempt in the next update. Error: %s", err, ) - self.aftv.adb.close() + self.aftv.adb_close() self._available = False # pylint: disable=protected-access return None @@ -429,7 +443,7 @@ class AndroidTVDevice(ADBDevice): # Check if device is disconnected. if not self._available: # Try to connect - self._available = self.aftv.connect(always_log_errors=False) + self._available = self.aftv.adb_connect(always_log_errors=False) # To be safe, wait until the next update to run ADB commands if # using the Python ADB implementation. @@ -508,7 +522,7 @@ class FireTVDevice(ADBDevice): # Check if device is disconnected. if not self._available: # Try to connect - self._available = self.aftv.connect(always_log_errors=False) + self._available = self.aftv.adb_connect(always_log_errors=False) # To be safe, wait until the next update to run ADB commands if # using the Python ADB implementation. diff --git a/homeassistant/components/anel_pwrctrl/switch.py b/homeassistant/components/anel_pwrctrl/switch.py index 6184465ef16..3c181d7d04b 100644 --- a/homeassistant/components/anel_pwrctrl/switch.py +++ b/homeassistant/components/anel_pwrctrl/switch.py @@ -1,13 +1,14 @@ """Support for ANEL PwrCtrl switches.""" +from datetime import timedelta import logging import socket -from datetime import timedelta +from anel_pwrctrl import DeviceMaster import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -36,8 +37,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): port_recv = config.get(CONF_PORT_RECV) port_send = config.get(CONF_PORT_SEND) - from anel_pwrctrl import DeviceMaster - try: master = DeviceMaster( username=username, diff --git a/homeassistant/components/anthemav/media_player.py b/homeassistant/components/anthemav/media_player.py index a033470e5c9..d472af6104e 100644 --- a/homeassistant/components/anthemav/media_player.py +++ b/homeassistant/components/anthemav/media_player.py @@ -1,6 +1,8 @@ """Support for Anthem Network Receivers and Processors.""" import logging +import anthemav + import voluptuous as vol from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA @@ -46,7 +48,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up our socket to the AVR.""" - import anthemav host = config.get(CONF_HOST) port = config.get(CONF_PORT) diff --git a/homeassistant/components/apcupsd/__init__.py b/homeassistant/components/apcupsd/__init__.py index 512bd01b72a..71f25f04387 100644 --- a/homeassistant/components/apcupsd/__init__.py +++ b/homeassistant/components/apcupsd/__init__.py @@ -1,7 +1,8 @@ """Support for APCUPSd via its Network Information Server (NIS).""" -import logging from datetime import timedelta +import logging +from apcaccess import status import voluptuous as vol from homeassistant.const import CONF_HOST, CONF_PORT @@ -64,7 +65,6 @@ class APCUPSdData: def __init__(self, host, port): """Initialize the data object.""" - from apcaccess import status self._host = host self._port = port diff --git a/homeassistant/components/apcupsd/sensor.py b/homeassistant/components/apcupsd/sensor.py index 837e6e45c6c..255eb1624ff 100644 --- a/homeassistant/components/apcupsd/sensor.py +++ b/homeassistant/components/apcupsd/sensor.py @@ -1,12 +1,13 @@ """Support for APCUPSd sensors.""" import logging +from apcaccess.status import ALL_UNITS import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA -import homeassistant.helpers.config_validation as cv from homeassistant.components import apcupsd -from homeassistant.const import TEMP_CELSIUS, CONF_RESOURCES, POWER_WATT +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_RESOURCES, POWER_WATT, TEMP_CELSIUS +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -135,7 +136,6 @@ def infer_unit(value): Split the unit off the end of the value and return the value, unit tuple pair. Else return the original value and None as the unit. """ - from apcaccess.status import ALL_UNITS for unit in ALL_UNITS: if value.endswith(unit): diff --git a/homeassistant/components/apns/notify.py b/homeassistant/components/apns/notify.py index dbd45013a3c..c24c9cc1605 100644 --- a/homeassistant/components/apns/notify.py +++ b/homeassistant/components/apns/notify.py @@ -1,14 +1,11 @@ """APNS Notification platform.""" import logging +from apns2.client import APNsClient +from apns2.errors import Unregistered +from apns2.payload import Payload import voluptuous as vol -from homeassistant.config import load_yaml_config_file -from homeassistant.const import ATTR_NAME, CONF_NAME, CONF_PLATFORM -from homeassistant.helpers import template as template_helper -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import track_state_change - from homeassistant.components.notify import ( ATTR_DATA, ATTR_TARGET, @@ -16,6 +13,11 @@ from homeassistant.components.notify import ( PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.config import load_yaml_config_file +from homeassistant.const import ATTR_NAME, CONF_NAME, CONF_PLATFORM +from homeassistant.helpers import template as template_helper +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import track_state_change APNS_DEVICES = "apns.yaml" CONF_CERTFILE = "cert_file" @@ -213,9 +215,6 @@ class ApnsNotificationService(BaseNotificationService): def send_message(self, message=None, **kwargs): """Send push message to registered devices.""" - from apns2.client import APNsClient - from apns2.payload import Payload - from apns2.errors import Unregistered apns = APNsClient( self.certificate, use_sandbox=self.sandbox, use_alternative_port=False diff --git a/homeassistant/components/apple_tv/__init__.py b/homeassistant/components/apple_tv/__init__.py index 51c2ee7e1a5..38d520f73da 100644 --- a/homeassistant/components/apple_tv/__init__.py +++ b/homeassistant/components/apple_tv/__init__.py @@ -3,13 +3,15 @@ import asyncio import logging from typing import Sequence, TypeVar, Union +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__) @@ -80,7 +82,6 @@ def request_configuration(hass, config, atv, credentials): async def configuration_callback(callback_data): """Handle the submitted configuration.""" - from pyatv import exceptions pin = callback_data.get("pin") @@ -93,7 +94,7 @@ def request_configuration(hass, config, atv, credentials): title=NOTIFICATION_AUTH_TITLE, notification_id=NOTIFICATION_AUTH_ID, ) - except exceptions.DeviceAuthenticationError as ex: + except DeviceAuthenticationError as ex: hass.components.persistent_notification.async_create( "Authentication failed! Did you enter correct PIN?

" "Details: {0}".format(ex), @@ -112,11 +113,10 @@ def request_configuration(hass, config, atv, credentials): ) -async def scan_for_apple_tvs(hass): +async def scan_apple_tvs(hass): """Scan for devices and present a notification of the ones found.""" - import pyatv - atvs = await pyatv.scan_for_apple_tvs(hass.loop, timeout=3) + atvs = await scan_for_apple_tvs(hass.loop, timeout=3) devices = [] for atv in atvs: @@ -149,7 +149,7 @@ async def async_setup(hass, config): entity_ids = service.data.get(ATTR_ENTITY_ID) if service.service == SERVICE_SCAN: - hass.async_add_job(scan_for_apple_tvs, hass) + hass.async_add_job(scan_apple_tvs, hass) return if entity_ids: @@ -207,7 +207,6 @@ async def async_setup(hass, config): async def _setup_atv(hass, hass_config, atv_config): """Set up an Apple TV.""" - import pyatv name = atv_config.get(CONF_NAME) host = atv_config.get(CONF_HOST) @@ -218,9 +217,9 @@ async def _setup_atv(hass, hass_config, atv_config): if host in hass.data[DATA_APPLE_TV]: return - details = pyatv.AppleTVDevice(name, host, login_id) + details = AppleTVDevice(name, host, login_id) session = async_get_clientsession(hass) - atv = pyatv.connect_to_apple_tv(details, hass.loop, session=session) + atv = connect_to_apple_tv(details, hass.loop, session=session) if credentials: await atv.airplay.load_credentials(credentials) diff --git a/homeassistant/components/apple_tv/media_player.py b/homeassistant/components/apple_tv/media_player.py index 9ac5ba77f98..c816be52259 100644 --- a/homeassistant/components/apple_tv/media_player.py +++ b/homeassistant/components/apple_tv/media_player.py @@ -1,6 +1,8 @@ """Support for Apple TV media player.""" import logging +import pyatv.const as atv_const + from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, @@ -112,22 +114,21 @@ class AppleTvDevice(MediaPlayerDevice): return STATE_OFF if self._playing: - from pyatv import const state = self._playing.play_state if state in ( - const.PLAY_STATE_IDLE, - const.PLAY_STATE_NO_MEDIA, - const.PLAY_STATE_LOADING, + atv_const.PLAY_STATE_IDLE, + atv_const.PLAY_STATE_NO_MEDIA, + atv_const.PLAY_STATE_LOADING, ): return STATE_IDLE - if state == const.PLAY_STATE_PLAYING: + if state == atv_const.PLAY_STATE_PLAYING: return STATE_PLAYING if state in ( - const.PLAY_STATE_PAUSED, - const.PLAY_STATE_FAST_FORWARD, - const.PLAY_STATE_FAST_BACKWARD, - const.PLAY_STATE_STOPPED, + atv_const.PLAY_STATE_PAUSED, + atv_const.PLAY_STATE_FAST_FORWARD, + atv_const.PLAY_STATE_FAST_BACKWARD, + atv_const.PLAY_STATE_STOPPED, ): # Catch fast forward/backward here so "play" is default action return STATE_PAUSED @@ -156,14 +157,13 @@ class AppleTvDevice(MediaPlayerDevice): def media_content_type(self): """Content type of current playing media.""" if self._playing: - from pyatv import const media_type = self._playing.media_type - if media_type == const.MEDIA_TYPE_VIDEO: + if media_type == atv_const.MEDIA_TYPE_VIDEO: return MEDIA_TYPE_VIDEO - if media_type == const.MEDIA_TYPE_MUSIC: + if media_type == atv_const.MEDIA_TYPE_MUSIC: return MEDIA_TYPE_MUSIC - if media_type == const.MEDIA_TYPE_TV: + if media_type == atv_const.MEDIA_TYPE_TV: return MEDIA_TYPE_TVSHOW @property diff --git a/homeassistant/components/apprise/__init__.py b/homeassistant/components/apprise/__init__.py new file mode 100644 index 00000000000..6ffdaf690d9 --- /dev/null +++ b/homeassistant/components/apprise/__init__.py @@ -0,0 +1 @@ +"""The apprise component.""" diff --git a/homeassistant/components/apprise/manifest.json b/homeassistant/components/apprise/manifest.json new file mode 100644 index 00000000000..3e971a96e7e --- /dev/null +++ b/homeassistant/components/apprise/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "apprise", + "name": "Apprise", + "documentation": "https://www.home-assistant.io/components/apprise", + "requirements": [ + "apprise==0.8.1" + ], + "dependencies": [], + "codeowners": [ + "@caronc" + ] +} diff --git a/homeassistant/components/apprise/notify.py b/homeassistant/components/apprise/notify.py new file mode 100644 index 00000000000..662cc9c1ab6 --- /dev/null +++ b/homeassistant/components/apprise/notify.py @@ -0,0 +1,73 @@ +"""Apprise platform for notify component.""" +import logging + +import voluptuous as vol + +import apprise + +import homeassistant.helpers.config_validation as cv + +from homeassistant.components.notify import ( + ATTR_TARGET, + ATTR_TITLE, + ATTR_TITLE_DEFAULT, + PLATFORM_SCHEMA, + BaseNotificationService, +) + +_LOGGER = logging.getLogger(__name__) + +CONF_FILE = "config" +CONF_URL = "url" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_URL): vol.All(cv.ensure_list, [str]), + vol.Optional(CONF_FILE): cv.string, + } +) + + +def get_service(hass, config, discovery_info=None): + """Get the Apprise notification service.""" + + # Create our object + a_obj = apprise.Apprise() + + if config.get(CONF_FILE): + # Sourced from a Configuration File + a_config = apprise.AppriseConfig() + if not a_config.add(config[CONF_FILE]): + _LOGGER.error("Invalid Apprise config url provided") + return None + + if not a_obj.add(a_config): + _LOGGER.error("Invalid Apprise config url provided") + return None + + if config.get(CONF_URL): + # Ordered list of URLs + if not a_obj.add(config[CONF_URL]): + _LOGGER.error("Invalid Apprise URL(s) supplied") + return None + + return AppriseNotificationService(a_obj) + + +class AppriseNotificationService(BaseNotificationService): + """Implement the notification service for Apprise.""" + + def __init__(self, a_obj): + """Initialize the service.""" + self.apprise = a_obj + + def send_message(self, message="", **kwargs): + """Send a message to a specified target. + + If no target/tags are specified, then services are notified as is + However, if any tags are specified, then they will be applied + to the notification causing filtering (if set up that way). + """ + targets = kwargs.get(ATTR_TARGET) + title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) + self.apprise.notify(body=message, title=title, tag=targets) diff --git a/homeassistant/components/aprs/device_tracker.py b/homeassistant/components/aprs/device_tracker.py index 86b0b6f48af..0d23cedb4ee 100644 --- a/homeassistant/components/aprs/device_tracker.py +++ b/homeassistant/components/aprs/device_tracker.py @@ -3,6 +3,11 @@ import logging import threading +import geopy.distance +import aprslib +from aprslib import ConnectionError as AprsConnectionError +from aprslib import LoginError + import voluptuous as vol from homeassistant.components.device_tracker import PLATFORM_SCHEMA @@ -59,7 +64,6 @@ def make_filter(callsigns: list) -> str: def gps_accuracy(gps, posambiguity: int) -> int: """Calculate the GPS accuracy based on APRS posambiguity.""" - import geopy.distance pos_a_map = {0: 0, 1: 1 / 600, 2: 1 / 60, 3: 1 / 6, 4: 1} if posambiguity in pos_a_map: @@ -115,8 +119,6 @@ class AprsListenerThread(threading.Thread): """Initialize the class.""" super().__init__() - import aprslib - self.callsign = callsign self.host = host self.start_event = threading.Event() @@ -138,8 +140,6 @@ class AprsListenerThread(threading.Thread): def run(self): """Connect to APRS and listen for data.""" self.ais.set_filter(self.server_filter) - from aprslib import ConnectionError as AprsConnectionError - from aprslib import LoginError try: _LOGGER.info( diff --git a/homeassistant/components/aqualogic/__init__.py b/homeassistant/components/aqualogic/__init__.py index cabe00b6c6d..9f693966382 100644 --- a/homeassistant/components/aqualogic/__init__.py +++ b/homeassistant/components/aqualogic/__init__.py @@ -1,9 +1,10 @@ """Support for AquaLogic devices.""" from datetime import timedelta import logging -import time import threading +import time +from aqualogic.core import AquaLogic import voluptuous as vol from homeassistant.const import ( @@ -71,7 +72,6 @@ class AquaLogicProcessor(threading.Thread): def run(self): """Event thread.""" - from aqualogic.core import AquaLogic while True: self._panel = AquaLogic() diff --git a/homeassistant/components/aqualogic/switch.py b/homeassistant/components/aqualogic/switch.py index b5a7a409647..74f1a9d9f9a 100644 --- a/homeassistant/components/aqualogic/switch.py +++ b/homeassistant/components/aqualogic/switch.py @@ -1,6 +1,7 @@ """Support for AquaLogic switches.""" import logging +from aqualogic.core import States import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice @@ -50,7 +51,6 @@ class AquaLogicSwitch(SwitchDevice): def __init__(self, processor, switch_type): """Initialize switch.""" - from aqualogic.core import States self._processor = processor self._type = switch_type diff --git a/homeassistant/components/aquostv/media_player.py b/homeassistant/components/aquostv/media_player.py index 016db478fc9..d8770592c9f 100644 --- a/homeassistant/components/aquostv/media_player.py +++ b/homeassistant/components/aquostv/media_player.py @@ -1,6 +1,8 @@ """Support for interface with an Aquos TV.""" import logging +import sharp_aquos_rc + import voluptuous as vol from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA @@ -77,7 +79,6 @@ SOURCES = { def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Sharp Aquos TV platform.""" - import sharp_aquos_rc name = config.get(CONF_NAME) port = config.get(CONF_PORT) diff --git a/homeassistant/components/arduino/__init__.py b/homeassistant/components/arduino/__init__.py index 4dcde93e749..f973ec136e3 100644 --- a/homeassistant/components/arduino/__init__.py +++ b/homeassistant/components/arduino/__init__.py @@ -1,8 +1,11 @@ """Support for Arduino boards running with the Firmata firmware.""" import logging +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 import homeassistant.helpers.config_validation as cv @@ -20,7 +23,6 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): """Set up the Arduino component.""" - import serial port = config[DOMAIN][CONF_PORT] @@ -59,7 +61,6 @@ class ArduinoBoard: def __init__(self, port): """Initialize the board.""" - from PyMata.pymata import PyMata self._port = port self._board = PyMata(self._port, verbose=False) diff --git a/homeassistant/components/arduino/manifest.json b/homeassistant/components/arduino/manifest.json index 3567ce71cd1..a29f65700ff 100644 --- a/homeassistant/components/arduino/manifest.json +++ b/homeassistant/components/arduino/manifest.json @@ -3,7 +3,7 @@ "name": "Arduino", "documentation": "https://www.home-assistant.io/integrations/arduino", "requirements": [ - "PyMata==2.14" + "PyMata==2.20" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/arlo/__init__.py b/homeassistant/components/arlo/__init__.py index 80fa37b6787..df24bdd1a92 100644 --- a/homeassistant/components/arlo/__init__.py +++ b/homeassistant/components/arlo/__init__.py @@ -1,14 +1,15 @@ """Support for Netgear Arlo IP cameras.""" -import logging from datetime import timedelta +import logging +from pyarlo import PyArlo +from requests.exceptions import ConnectTimeout, HTTPError import voluptuous as vol -from requests.exceptions import HTTPError, ConnectTimeout +from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME from homeassistant.helpers import config_validation as cv -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_SCAN_INTERVAL -from homeassistant.helpers.event import track_time_interval from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.event import track_time_interval _LOGGER = logging.getLogger(__name__) @@ -47,7 +48,6 @@ def setup(hass, config): scan_interval = conf.get(CONF_SCAN_INTERVAL) try: - from pyarlo import PyArlo arlo = PyArlo(username, password, preload=False) if not arlo.is_connected: diff --git a/homeassistant/components/arlo/camera.py b/homeassistant/components/arlo/camera.py index a05dc40a9ef..958c383765a 100644 --- a/homeassistant/components/arlo/camera.py +++ b/homeassistant/components/arlo/camera.py @@ -1,6 +1,7 @@ """Support for Netgear Arlo IP cameras.""" import logging +from haffmpeg.camera import CameraMjpeg import voluptuous as vol from homeassistant.components.camera import PLATFORM_SCHEMA, Camera @@ -77,7 +78,6 @@ class ArloCam(Camera): async def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from the camera.""" - from haffmpeg.camera import CameraMjpeg video = self._camera.last_video if not video: diff --git a/homeassistant/components/aruba/device_tracker.py b/homeassistant/components/aruba/device_tracker.py index f93533b6beb..485c731ff6a 100644 --- a/homeassistant/components/aruba/device_tracker.py +++ b/homeassistant/components/aruba/device_tracker.py @@ -2,15 +2,16 @@ import logging import re +import pexpect 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__) @@ -82,7 +83,6 @@ class ArubaDeviceScanner(DeviceScanner): def get_aruba_data(self): """Retrieve data from Aruba Access Point and return parsed result.""" - import pexpect connect = "ssh {}@{}" ssh = pexpect.spawn(connect.format(self.username, self.host)) diff --git a/homeassistant/components/asterisk_mbox/__init__.py b/homeassistant/components/asterisk_mbox/__init__.py index 6c9412d07d8..1ecba9f4c8f 100644 --- a/homeassistant/components/asterisk_mbox/__init__.py +++ b/homeassistant/components/asterisk_mbox/__init__.py @@ -1,6 +1,12 @@ """Support for Asterisk Voicemail interface.""" import logging +from asterisk_mbox import Client as asteriskClient +from asterisk_mbox.commands import ( + CMD_MESSAGE_CDR, + CMD_MESSAGE_CDR_AVAILABLE, + CMD_MESSAGE_LIST, +) import voluptuous as vol from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT @@ -51,7 +57,6 @@ class AsteriskData: def __init__(self, hass, host, port, password, config): """Init the Asterisk data object.""" - from asterisk_mbox import Client as asteriskClient self.hass = hass self.config = config @@ -76,11 +81,6 @@ class AsteriskData: @callback def handle_data(self, command, msg): """Handle changes to the mailbox.""" - from asterisk_mbox.commands import ( - CMD_MESSAGE_LIST, - CMD_MESSAGE_CDR_AVAILABLE, - CMD_MESSAGE_CDR, - ) if command == CMD_MESSAGE_LIST: _LOGGER.debug("AsteriskVM sent updated message list: Len %d", len(msg)) diff --git a/homeassistant/components/asterisk_mbox/mailbox.py b/homeassistant/components/asterisk_mbox/mailbox.py index 4d3c255fd5b..3cd6fe059b6 100644 --- a/homeassistant/components/asterisk_mbox/mailbox.py +++ b/homeassistant/components/asterisk_mbox/mailbox.py @@ -1,6 +1,8 @@ """Support for the Asterisk Voicemail interface.""" import logging +from asterisk_mbox import ServerError + from homeassistant.components.mailbox import CONTENT_TYPE_MPEG, Mailbox, StreamError from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -50,7 +52,6 @@ class AsteriskMailbox(Mailbox): async def async_get_media(self, msgid): """Return the media blob for the msgid.""" - from asterisk_mbox import ServerError client = self.hass.data[ASTERISK_DOMAIN].client try: diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index 93b5ec6ec78..468e6e429a7 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -1,18 +1,20 @@ """Support for August devices.""" -import logging from datetime import timedelta +import logging +from august.api import Api +from august.authenticator import AuthenticationState, Authenticator, ValidationResult +from requests import RequestException, Session import voluptuous as vol -from requests import RequestException -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( CONF_PASSWORD, - CONF_USERNAME, CONF_TIMEOUT, + CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, ) from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -62,7 +64,6 @@ def request_configuration(hass, config, api, authenticator): def august_configuration_callback(data): """Run when the configuration callback is called.""" - from august.authenticator import ValidationResult result = authenticator.validate_verification_code(data.get("verification_code")) @@ -94,7 +95,6 @@ def request_configuration(hass, config, api, authenticator): def setup_august(hass, config, api, authenticator): """Set up the August component.""" - from august.authenticator import AuthenticationState authentication = None try: @@ -134,9 +134,6 @@ def setup_august(hass, config, api, authenticator): def setup(hass, config): """Set up the August component.""" - from august.api import Api - from august.authenticator import Authenticator - from requests import Session conf = config[DOMAIN] api_http_session = None diff --git a/homeassistant/components/august/binary_sensor.py b/homeassistant/components/august/binary_sensor.py index d68582d30c5..14d03189c92 100644 --- a/homeassistant/components/august/binary_sensor.py +++ b/homeassistant/components/august/binary_sensor.py @@ -2,6 +2,9 @@ from datetime import datetime, timedelta import logging +from august.activity import ActivityType +from august.lock import LockDoorStatus + from homeassistant.components.binary_sensor import BinarySensorDevice from . import DATA_AUGUST @@ -26,7 +29,6 @@ def _retrieve_online_state(data, doorbell): def _retrieve_motion_state(data, doorbell): - from august.activity import ActivityType return _activity_time_based_state( data, doorbell, [ActivityType.DOORBELL_MOTION, ActivityType.DOORBELL_DING] @@ -34,7 +36,6 @@ def _retrieve_motion_state(data, doorbell): def _retrieve_ding_state(data, doorbell): - from august.activity import ActivityType return _activity_time_based_state(data, doorbell, [ActivityType.DOORBELL_DING]) @@ -65,8 +66,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): data = hass.data[DATA_AUGUST] devices = [] - from august.lock import LockDoorStatus - for door in data.locks: for sensor_type in SENSOR_TYPES_DOOR: state_provider = SENSOR_TYPES_DOOR[sensor_type][2] @@ -136,8 +135,6 @@ class AugustDoorBinarySensor(BinarySensorDevice): self._state = state_provider(self._data, self._door) self._available = self._state is not None - from august.lock import LockDoorStatus - self._state = self._state == LockDoorStatus.OPEN @property diff --git a/homeassistant/components/august/lock.py b/homeassistant/components/august/lock.py index 8b8c019eb2d..a541be67097 100644 --- a/homeassistant/components/august/lock.py +++ b/homeassistant/components/august/lock.py @@ -2,6 +2,9 @@ from datetime import timedelta import logging +from august.activity import ActivityType +from august.lock import LockStatus + from homeassistant.components.lock import LockDevice from homeassistant.const import ATTR_BATTERY_LEVEL @@ -51,8 +54,6 @@ class AugustLock(LockDevice): self._lock_detail = self._data.get_lock_detail(self._lock.device_id) - from august.activity import ActivityType - activity = self._data.get_latest_device_activity( self._lock.device_id, ActivityType.LOCK_OPERATION ) @@ -73,7 +74,6 @@ class AugustLock(LockDevice): @property def is_locked(self): """Return true if device is on.""" - from august.lock import LockStatus return self._lock_status is LockStatus.LOCKED diff --git a/homeassistant/components/auth/.translations/ko.json b/homeassistant/components/auth/.translations/ko.json index 1cb70519b20..6c2e8988d83 100644 --- a/homeassistant/components/auth/.translations/ko.json +++ b/homeassistant/components/auth/.translations/ko.json @@ -25,7 +25,7 @@ }, "step": { "init": { - "description": "\uc2dc\uac04 \uae30\ubc18\uc758 \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638\ub97c \uc0ac\uc6a9\ud558\ub294 2\ub2e8\uacc4 \uc778\uc99d\uc744 \ud558\ub824\uba74 \uc778\uc99d\uc6a9 \uc571\uc744 \uc774\uc6a9\ud574\uc11c QR \ucf54\ub4dc\ub97c \uc2a4\uce94\ud574\uc8fc\uc138\uc694. \uc778\uc99d\uc6a9 \uc571\uc740 [\uad6c\uae00 OTP](https://support.google.com/accounts/answer/1066447) \ub610\ub294 [Authy](https://authy.com/) \ub97c \ucd94\ucc9c\ub4dc\ub9bd\ub2c8\ub2e4.\n\n{qr_code}\n\n\uc2a4\uce94 \ud6c4\uc5d0 \uc0dd\uc131\ub41c 6\uc790\ub9ac \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc11c \uc124\uc815\uc744 \ud655\uc778\ud558\uc138\uc694. QR \ucf54\ub4dc \uc2a4\uce94\uc5d0 \ubb38\uc81c\uac00 \uc788\ub2e4\uba74, **`{code}`** \ucf54\ub4dc\ub85c \uc9c1\uc811 \uc124\uc815\ud574\ubcf4\uc138\uc694.", + "description": "\uc2dc\uac04 \uae30\ubc18\uc758 \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638\ub97c \uc0ac\uc6a9\ud558\ub294 2\ub2e8\uacc4 \uc778\uc99d\uc744 \ud558\ub824\uba74 \uc778\uc99d\uc6a9 \uc571\uc744 \uc774\uc6a9\ud574\uc11c QR \ucf54\ub4dc\ub97c \uc2a4\uce94\ud574\uc8fc\uc138\uc694. \uc778\uc99d\uc6a9 \uc571\uc740 [Google OTP](https://support.google.com/accounts/answer/1066447) \ub610\ub294 [Authy](https://authy.com/) \ub97c \ucd94\ucc9c\ub4dc\ub9bd\ub2c8\ub2e4.\n\n{qr_code}\n\n\uc2a4\uce94 \ud6c4\uc5d0 \uc0dd\uc131\ub41c 6\uc790\ub9ac \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc11c \uc124\uc815\uc744 \ud655\uc778\ud558\uc138\uc694. QR \ucf54\ub4dc \uc2a4\uce94\uc5d0 \ubb38\uc81c\uac00 \uc788\ub2e4\uba74, **`{code}`** \ucf54\ub4dc\ub85c \uc9c1\uc811 \uc124\uc815\ud574\ubcf4\uc138\uc694.", "title": "TOTP 2\ub2e8\uacc4 \uc778\uc99d \uad6c\uc131" } }, diff --git a/homeassistant/components/auth/login_flow.py b/homeassistant/components/auth/login_flow.py index 0f5da5d7527..d6844396ce7 100644 --- a/homeassistant/components/auth/login_flow.py +++ b/homeassistant/components/auth/login_flow.py @@ -68,10 +68,15 @@ associate with an credential if "type" set to "link_user" in """ from aiohttp import web import voluptuous as vol +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, log_invalid_auth +from homeassistant.components.http.ban import ( + process_wrong_login, + process_success_login, + log_invalid_auth, +) from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.components.http.view import HomeAssistantView from . import indieauth @@ -120,8 +125,6 @@ def _prepare_result_json(result): if result["type"] != data_entry_flow.RESULT_TYPE_FORM: return result - import voluptuous_serialize - data = result.copy() schema = data["data_schema"] @@ -186,6 +189,7 @@ class LoginFlowIndexView(HomeAssistantView): return self.json_message("Handler does not support init", 400) if result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + await process_success_login(request) result.pop("data") result["result"] = self._store_result(data["client_id"], result["result"]) return self.json(result) diff --git a/homeassistant/components/auth/mfa_setup_flow.py b/homeassistant/components/auth/mfa_setup_flow.py index 42dab7ebb5a..271e9ae1634 100644 --- a/homeassistant/components/auth/mfa_setup_flow.py +++ b/homeassistant/components/auth/mfa_setup_flow.py @@ -2,6 +2,7 @@ import logging import voluptuous as vol +import voluptuous_serialize from homeassistant import data_entry_flow from homeassistant.components import websocket_api @@ -134,8 +135,6 @@ def _prepare_result_json(result): if result["type"] != data_entry_flow.RESULT_TYPE_FORM: return result - import voluptuous_serialize - data = result.copy() schema = data["data_schema"] diff --git a/homeassistant/components/automatic/device_tracker.py b/homeassistant/components/automatic/device_tracker.py index 09cf3f67114..fbb823dd329 100644 --- a/homeassistant/components/automatic/device_tracker.py +++ b/homeassistant/components/automatic/device_tracker.py @@ -6,6 +6,8 @@ import logging import os from aiohttp import web +import aioautomatic + import voluptuous as vol from homeassistant.components.device_tracker import ( @@ -82,7 +84,6 @@ def _write_refresh_token_to_file(hass, filename, refresh_token): @asyncio.coroutine def async_setup_scanner(hass, config, async_see, discovery_info=None): """Validate the configuration and return an Automatic scanner.""" - import aioautomatic hass.http.register_view(AutomaticAuthCallbackView()) @@ -215,7 +216,6 @@ class AutomaticData: @asyncio.coroutine def handle_event(self, name, event): """Coroutine to update state for a real time event.""" - import aioautomatic self.hass.bus.async_fire(EVENT_AUTOMATIC_UPDATE, event.data) @@ -261,7 +261,6 @@ class AutomaticData: @asyncio.coroutine def ws_connect(self, now=None): """Open the websocket connection.""" - import aioautomatic self.ws_close_requested = False @@ -321,7 +320,6 @@ class AutomaticData: @asyncio.coroutine def get_vehicle_info(self, vehicle): """Fetch the latest vehicle info from automatic.""" - import aioautomatic name = vehicle.display_name if name is None: diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index f669d415854..3409ce832dd 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -7,9 +7,6 @@ from typing import Any, Awaitable, Callable import voluptuous as vol -from homeassistant.components.device_automation.exceptions import ( - InvalidDeviceAutomationConfig, -) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_NAME, @@ -476,10 +473,7 @@ async def _async_process_trigger(hass, config, trigger_configs, name, action): for conf in trigger_configs: platform = importlib.import_module(".{}".format(conf[CONF_PLATFORM]), __name__) - try: - remove = await platform.async_attach_trigger(hass, conf, action, info) - except InvalidDeviceAutomationConfig: - remove = False + remove = await platform.async_attach_trigger(hass, conf, action, info) if not remove: _LOGGER.error("Error setting up trigger %s", name) diff --git a/homeassistant/components/automation/config.py b/homeassistant/components/automation/config.py index ebbd1771e84..5733cd2e83e 100644 --- a/homeassistant/components/automation/config.py +++ b/homeassistant/components/automation/config.py @@ -4,6 +4,9 @@ import importlib 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.exceptions import HomeAssistantError @@ -52,7 +55,12 @@ async def _try_async_validate_config_item(hass, config, full_config=None): """Validate config item.""" try: config = await async_validate_config_item(hass, config, full_config) - except (vol.Invalid, HomeAssistantError, IntegrationNotFound) as ex: + except ( + vol.Invalid, + HomeAssistantError, + IntegrationNotFound, + InvalidDeviceAutomationConfig, + ) as ex: async_log_exception(ex, DOMAIN, full_config or config, hass) return None diff --git a/homeassistant/components/automation/device.py b/homeassistant/components/automation/device.py index dc65008c3fb..ced8f65cbf5 100644 --- a/homeassistant/components/automation/device.py +++ b/homeassistant/components/automation/device.py @@ -18,6 +18,9 @@ async def async_validate_trigger_config(hass, config): platform = await async_get_device_automation_platform( hass, config[CONF_DOMAIN], "trigger" ) + if hasattr(platform, "async_validate_trigger_config"): + return await getattr(platform, "async_validate_trigger_config")(hass, config) + return platform.TRIGGER_SCHEMA(config) diff --git a/homeassistant/components/automation/reproduce_state.py b/homeassistant/components/automation/reproduce_state.py new file mode 100644 index 00000000000..553d6871087 --- /dev/null +++ b/homeassistant/components/automation/reproduce_state.py @@ -0,0 +1,61 @@ +"""Reproduce an Automation state.""" +import asyncio +import logging +from typing import Iterable, Optional + +from homeassistant.const import ( + ATTR_ENTITY_ID, + STATE_ON, + STATE_OFF, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.core import Context, State +from homeassistant.helpers.typing import HomeAssistantType + +from . import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +VALID_STATES = {STATE_ON, STATE_OFF} + + +async def _async_reproduce_state( + hass: HomeAssistantType, state: State, context: Optional[Context] = None +) -> None: + """Reproduce a single state.""" + cur_state = hass.states.get(state.entity_id) + + if cur_state is None: + _LOGGER.warning("Unable to find entity %s", state.entity_id) + return + + if state.state not in VALID_STATES: + _LOGGER.warning( + "Invalid state specified for %s: %s", state.entity_id, state.state + ) + return + + # Return if we are already at the right state. + if cur_state.state == state.state: + return + + service_data = {ATTR_ENTITY_ID: state.entity_id} + + if state.state == STATE_ON: + service = SERVICE_TURN_ON + elif state.state == STATE_OFF: + service = SERVICE_TURN_OFF + + await hass.services.async_call( + DOMAIN, service, service_data, context=context, blocking=True + ) + + +async def async_reproduce_states( + hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None +) -> None: + """Reproduce Automation states.""" + await asyncio.gather( + *(_async_reproduce_state(hass, state, context) for state in states) + ) diff --git a/homeassistant/components/awair/sensor.py b/homeassistant/components/awair/sensor.py index c899e009796..f15e4a80e36 100644 --- a/homeassistant/components/awair/sensor.py +++ b/homeassistant/components/awair/sensor.py @@ -4,6 +4,7 @@ from datetime import timedelta import logging import math +from python_awair import AwairClient import voluptuous as vol from homeassistant.const import ( @@ -105,7 +106,6 @@ PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend( # used at this time is the `uuid` value. async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Connect to the Awair API and find devices.""" - from python_awair import AwairClient token = config[CONF_ACCESS_TOKEN] client = AwairClient(token, session=async_get_clientsession(hass)) diff --git a/homeassistant/components/aws/__init__.py b/homeassistant/components/aws/__init__.py index 1959cc05e80..780a65b2d47 100644 --- a/homeassistant/components/aws/__init__.py +++ b/homeassistant/components/aws/__init__.py @@ -3,6 +3,8 @@ import asyncio import logging from collections import OrderedDict +import aiobotocore + import voluptuous as vol from homeassistant import config_entries @@ -151,7 +153,6 @@ async def async_setup_entry(hass, entry): async def _validate_aws_credentials(hass, credential): """Validate AWS credential config.""" - import aiobotocore aws_config = credential.copy() del aws_config[CONF_NAME] diff --git a/homeassistant/components/aws/notify.py b/homeassistant/components/aws/notify.py index fa1cf3fa363..2afa9a3a402 100644 --- a/homeassistant/components/aws/notify.py +++ b/homeassistant/components/aws/notify.py @@ -4,6 +4,8 @@ import base64 import json import logging +import aiobotocore + from homeassistant.components.notify import ( ATTR_TARGET, ATTR_TITLE, @@ -26,7 +28,6 @@ _LOGGER = logging.getLogger(__name__) async def get_available_regions(hass, service): """Get available regions for a service.""" - import aiobotocore session = aiobotocore.get_session() # get_available_regions is not a coroutine since it does not perform @@ -41,8 +42,6 @@ async def async_get_service(hass, config, discovery_info=None): _LOGGER.error("Please config aws notify platform in aws component") return None - import aiobotocore - session = None conf = discovery_info diff --git a/homeassistant/components/axis/.translations/ca.json b/homeassistant/components/axis/.translations/ca.json index 75dd89ef9c1..3458dcc4529 100644 --- a/homeassistant/components/axis/.translations/ca.json +++ b/homeassistant/components/axis/.translations/ca.json @@ -12,6 +12,7 @@ "device_unavailable": "El dispositiu no est\u00e0 disponible", "faulty_credentials": "Credencials d'usuari incorrectes" }, + "flow_title": "Dispositiu d'eix: {name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/axis/.translations/da.json b/homeassistant/components/axis/.translations/da.json index 2d728468fc7..c169f85f280 100644 --- a/homeassistant/components/axis/.translations/da.json +++ b/homeassistant/components/axis/.translations/da.json @@ -12,6 +12,7 @@ "device_unavailable": "Enheden er ikke tilg\u00e6ngelig", "faulty_credentials": "Ugyldige legitimationsoplysninger" }, + "flow_title": "Axis enhed: {name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/axis/.translations/en.json b/homeassistant/components/axis/.translations/en.json index 5fd5d9be565..c7d84aa8cc3 100644 --- a/homeassistant/components/axis/.translations/en.json +++ b/homeassistant/components/axis/.translations/en.json @@ -12,6 +12,7 @@ "device_unavailable": "Device is not available", "faulty_credentials": "Bad user credentials" }, + "flow_title": "Axis device: {name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/axis/.translations/es.json b/homeassistant/components/axis/.translations/es.json index d29481a3be9..3f7db674fdf 100644 --- a/homeassistant/components/axis/.translations/es.json +++ b/homeassistant/components/axis/.translations/es.json @@ -12,6 +12,7 @@ "device_unavailable": "El dispositivo no est\u00e1 disponible", "faulty_credentials": "Credenciales de usuario incorrectas" }, + "flow_title": "Dispositivo Axis: {name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/axis/.translations/fr.json b/homeassistant/components/axis/.translations/fr.json index 24afb4a226c..608e12d020a 100644 --- a/homeassistant/components/axis/.translations/fr.json +++ b/homeassistant/components/axis/.translations/fr.json @@ -12,6 +12,7 @@ "device_unavailable": "L'appareil n'est pas disponible", "faulty_credentials": "Mauvaises informations d'identification de l'utilisateur" }, + "flow_title": "Appareil Axis: {name} ( {host} )", "step": { "user": { "data": { diff --git a/homeassistant/components/axis/.translations/it.json b/homeassistant/components/axis/.translations/it.json index e979af08836..3f303140c68 100644 --- a/homeassistant/components/axis/.translations/it.json +++ b/homeassistant/components/axis/.translations/it.json @@ -12,6 +12,7 @@ "device_unavailable": "Il dispositivo non \u00e8 disponibile", "faulty_credentials": "Credenziali utente non valide" }, + "flow_title": "Dispositivo Axis: {name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/axis/.translations/ko.json b/homeassistant/components/axis/.translations/ko.json index 5ceaa082810..f02b7cdcefa 100644 --- a/homeassistant/components/axis/.translations/ko.json +++ b/homeassistant/components/axis/.translations/ko.json @@ -12,6 +12,7 @@ "device_unavailable": "\uae30\uae30\ub97c \uc0ac\uc6a9\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", "faulty_credentials": "\uc0ac\uc6a9\uc790 \uc774\ub984 \ud639\uc740 \ube44\ubc00\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, + "flow_title": "Axis \uae30\uae30: {name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/axis/.translations/lb.json b/homeassistant/components/axis/.translations/lb.json index 281eaa7c881..24ee0e24125 100644 --- a/homeassistant/components/axis/.translations/lb.json +++ b/homeassistant/components/axis/.translations/lb.json @@ -12,6 +12,7 @@ "device_unavailable": "Apparat ass net erreechbar", "faulty_credentials": "Ong\u00eblteg Login Informatioune" }, + "flow_title": "Axis Apparat: {name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/axis/.translations/nl.json b/homeassistant/components/axis/.translations/nl.json index 83395283404..10fc8c02d66 100644 --- a/homeassistant/components/axis/.translations/nl.json +++ b/homeassistant/components/axis/.translations/nl.json @@ -12,6 +12,7 @@ "device_unavailable": "Apparaat is niet beschikbaar", "faulty_credentials": "Ongeldige gebruikersreferenties" }, + "flow_title": "Axis apparaat: {naam} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/axis/.translations/no.json b/homeassistant/components/axis/.translations/no.json index 29022e39745..190737e5a76 100644 --- a/homeassistant/components/axis/.translations/no.json +++ b/homeassistant/components/axis/.translations/no.json @@ -12,6 +12,7 @@ "device_unavailable": "Enheten er ikke tilgjengelig", "faulty_credentials": "Ugyldig brukerlegitimasjon" }, + "flow_title": "Akse-enhet: {Name} ({Host})", "step": { "user": { "data": { diff --git a/homeassistant/components/axis/.translations/pl.json b/homeassistant/components/axis/.translations/pl.json index 88e80360536..4ca87310f48 100644 --- a/homeassistant/components/axis/.translations/pl.json +++ b/homeassistant/components/axis/.translations/pl.json @@ -12,6 +12,7 @@ "device_unavailable": "Urz\u0105dzenie jest niedost\u0119pne", "faulty_credentials": "B\u0142\u0119dne dane uwierzytelniaj\u0105ce" }, + "flow_title": "Urz\u0105dzenie Axis: {name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/axis/.translations/ru.json b/homeassistant/components/axis/.translations/ru.json index 951263d53f9..0345862b865 100644 --- a/homeassistant/components/axis/.translations/ru.json +++ b/homeassistant/components/axis/.translations/ru.json @@ -1,17 +1,18 @@ { "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.", - "bad_config_file": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438", - "link_local_address": "\u0421\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0435 \u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f", - "not_axis_device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c Axis" + "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.", + "bad_config_file": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438.", + "link_local_address": "\u0421\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0435 \u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f.", + "not_axis_device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c Axis." }, "error": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\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" + "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." }, + "flow_title": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Axis {name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/axis/.translations/sl.json b/homeassistant/components/axis/.translations/sl.json index 205e901553e..5ffa02e19f7 100644 --- a/homeassistant/components/axis/.translations/sl.json +++ b/homeassistant/components/axis/.translations/sl.json @@ -12,6 +12,7 @@ "device_unavailable": "Naprava ni na voljo", "faulty_credentials": "Napa\u010dni uporabni\u0161ki podatki" }, + "flow_title": "OS naprava: {Name} ({Host})", "step": { "user": { "data": { diff --git a/homeassistant/components/axis/.translations/zh-Hant.json b/homeassistant/components/axis/.translations/zh-Hant.json index c0d0df02135..6c78fc2166c 100644 --- a/homeassistant/components/axis/.translations/zh-Hant.json +++ b/homeassistant/components/axis/.translations/zh-Hant.json @@ -12,6 +12,7 @@ "device_unavailable": "\u8a2d\u5099\u7121\u6cd5\u4f7f\u7528", "faulty_credentials": "\u4f7f\u7528\u8005\u6191\u8b49\u7121\u6548" }, + "flow_title": "Axis \u8a2d\u5099\uff1a{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/axis/config_flow.py b/homeassistant/components/axis/config_flow.py index 3b5efe96760..5eb4f9daddd 100644 --- a/homeassistant/components/axis/config_flow.py +++ b/homeassistant/components/axis/config_flow.py @@ -171,7 +171,7 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if discovery_info[CONF_HOST].startswith("169.254"): return self.async_abort(reason="link_local_address") - # pylint: disable=unsupported-assignment-operation + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context["macaddress"] = serialnumber if any( @@ -191,6 +191,12 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): load_json, self.hass.config.path(CONFIG_FILE) ) + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + self.context["title_placeholders"] = { + "name": discovery_info["hostname"][:-7], + "host": discovery_info[CONF_HOST], + } + if serialnumber not in config_file: self.discovery_schema = { vol.Required(CONF_HOST, default=discovery_info[CONF_HOST]): str, @@ -198,6 +204,7 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): vol.Required(CONF_PASSWORD): str, vol.Required(CONF_PORT, default=discovery_info[CONF_PORT]): int, } + return await self.async_step_user() try: diff --git a/homeassistant/components/axis/device.py b/homeassistant/components/axis/device.py index 3b91f7e1474..e42a758f3c4 100644 --- a/homeassistant/components/axis/device.py +++ b/homeassistant/components/axis/device.py @@ -3,6 +3,9 @@ import asyncio import async_timeout +import axis +from axis.streammanager import SIGNAL_PLAYING + from homeassistant.const import ( CONF_DEVICE, CONF_HOST, @@ -140,7 +143,6 @@ class AxisNetworkDevice: This is called on every RTSP keep-alive message. Only signal state change if state change is true. """ - from axis.streammanager import SIGNAL_PLAYING if self.available != (status == SIGNAL_PLAYING): self.available = not self.available @@ -198,7 +200,6 @@ class AxisNetworkDevice: async def get_device(hass, config): """Create a Axis device.""" - import axis device = axis.AxisDevice( loop=hass.loop, diff --git a/homeassistant/components/axis/strings.json b/homeassistant/components/axis/strings.json index 29fe09b7e5b..2dc23f3e466 100644 --- a/homeassistant/components/axis/strings.json +++ b/homeassistant/components/axis/strings.json @@ -1,6 +1,7 @@ { "config": { "title": "Axis device", + "flow_title": "Axis device: {name} ({host})", "step": { "user": { "title": "Set up Axis device", diff --git a/homeassistant/components/baidu/tts.py b/homeassistant/components/baidu/tts.py index 85737d1affd..8d753753e5a 100644 --- a/homeassistant/components/baidu/tts.py +++ b/homeassistant/components/baidu/tts.py @@ -1,6 +1,7 @@ """Support for Baidu speech service.""" import logging +from aip import AipSpeech import voluptuous as vol from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider @@ -106,7 +107,6 @@ class BaiduTTSProvider(Provider): def get_tts_audio(self, message, language, options=None): """Load TTS from BaiduTTS.""" - from aip import AipSpeech aip_speech = AipSpeech( self._app_data["appid"], diff --git a/homeassistant/components/bayesian/binary_sensor.py b/homeassistant/components/bayesian/binary_sensor.py index acefc5a3b26..ffa13a6288c 100644 --- a/homeassistant/components/bayesian/binary_sensor.py +++ b/homeassistant/components/bayesian/binary_sensor.py @@ -250,7 +250,7 @@ class BayesianBinarySensor(BinarySensorDevice): def device_state_attributes(self): """Return the state attributes of the sensor.""" return { - ATTR_OBSERVATIONS: [val for val in self.current_obs.values()], + ATTR_OBSERVATIONS: list(self.current_obs.values()), ATTR_PROBABILITY: round(self.probability, 2), ATTR_PROBABILITY_THRESHOLD: self._probability_threshold, } diff --git a/homeassistant/components/bbb_gpio/__init__.py b/homeassistant/components/bbb_gpio/__init__.py index bfaa2a7c50d..e68633c0688 100644 --- a/homeassistant/components/bbb_gpio/__init__.py +++ b/homeassistant/components/bbb_gpio/__init__.py @@ -1,6 +1,8 @@ """Support for controlling GPIO pins of a Beaglebone Black.""" import logging +from Adafruit_BBIO import GPIO # pylint: disable=import-error + from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP _LOGGER = logging.getLogger(__name__) @@ -11,7 +13,6 @@ DOMAIN = "bbb_gpio" def setup(hass, config): """Set up the BeagleBone Black GPIO component.""" # pylint: disable=import-error - from Adafruit_BBIO import GPIO def cleanup_gpio(event): """Stuff to do before stopping.""" @@ -27,39 +28,29 @@ def setup(hass, config): def setup_output(pin): """Set up a GPIO as output.""" - # pylint: disable=import-error - from Adafruit_BBIO import GPIO GPIO.setup(pin, GPIO.OUT) def setup_input(pin, pull_mode): """Set up a GPIO as input.""" - # pylint: disable=import-error - from Adafruit_BBIO import GPIO GPIO.setup(pin, GPIO.IN, GPIO.PUD_DOWN if pull_mode == "DOWN" else GPIO.PUD_UP) def write_output(pin, value): """Write a value to a GPIO.""" - # pylint: disable=import-error - from Adafruit_BBIO import GPIO GPIO.output(pin, value) def read_input(pin): """Read a value from a GPIO.""" - # pylint: disable=import-error - from Adafruit_BBIO import GPIO return GPIO.input(pin) is GPIO.HIGH def edge_detect(pin, event_callback, bounce): """Add detection for RISING and FALLING events.""" - # pylint: disable=import-error - from Adafruit_BBIO import GPIO GPIO.add_event_detect(pin, GPIO.BOTH, callback=event_callback, bouncetime=bounce) diff --git a/homeassistant/components/bbox/device_tracker.py b/homeassistant/components/bbox/device_tracker.py index 89449aeab45..122016ecf96 100644 --- a/homeassistant/components/bbox/device_tracker.py +++ b/homeassistant/components/bbox/device_tracker.py @@ -4,6 +4,8 @@ from datetime import timedelta import logging from typing import List +import pybbox + import voluptuous as vol from homeassistant.components.device_tracker import ( @@ -75,8 +77,6 @@ class BboxDeviceScanner(DeviceScanner): """ _LOGGER.info("Scanning...") - import pybbox - box = pybbox.Bbox(ip=self.host) result = box.get_all_connected_devices() diff --git a/homeassistant/components/bbox/sensor.py b/homeassistant/components/bbox/sensor.py index ba38f8d2607..ad6bcc39796 100644 --- a/homeassistant/components/bbox/sensor.py +++ b/homeassistant/components/bbox/sensor.py @@ -3,6 +3,8 @@ import logging from datetime import timedelta import requests +import pybbox + import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -136,7 +138,6 @@ class BboxData: @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data from the Bbox.""" - import pybbox try: box = pybbox.Bbox() diff --git a/homeassistant/components/bh1750/sensor.py b/homeassistant/components/bh1750/sensor.py index 0a305c21adb..cc91fa48bae 100644 --- a/homeassistant/components/bh1750/sensor.py +++ b/homeassistant/components/bh1750/sensor.py @@ -2,6 +2,9 @@ from functools import partial import logging +import smbus # pylint: disable=import-error +from i2csense.bh1750 import BH1750 # pylint: disable=import-error + import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -60,8 +63,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the BH1750 sensor.""" - import smbus # pylint: disable=import-error - from i2csense.bh1750 import BH1750 # pylint: disable=import-error name = config.get(CONF_NAME) bus_number = config.get(CONF_I2C_BUS) diff --git a/homeassistant/components/binary_sensor/.translations/ca.json b/homeassistant/components/binary_sensor/.translations/ca.json index de7d837b12c..8bbd19a0d45 100644 --- a/homeassistant/components/binary_sensor/.translations/ca.json +++ b/homeassistant/components/binary_sensor/.translations/ca.json @@ -53,6 +53,7 @@ "hot": "{entity_name} es torna calent", "light": "{entity_name} ha comen\u00e7at a detectar llum", "locked": "{entity_name} est\u00e0 bloquejat", + "moist": "{entity_name} es torna humit", "moist\u00a7": "{entity_name} es torna humit", "motion": "{entity_name} ha comen\u00e7at a detectar moviment", "moving": "{entity_name} ha comen\u00e7at a moure's", @@ -71,6 +72,7 @@ "not_moist": "{entity_name} es torna sec", "not_moving": "{entity_name} ha parat de moure's", "not_occupied": "{entity_name} es desocupa", + "not_opened": "{entity_name} es tanca", "not_plugged_in": "{entity_name} desendollat", "not_powered": "{entity_name} no est\u00e0 alimentat", "not_present": "{entity_name} no est\u00e0 present", diff --git a/homeassistant/components/binary_sensor/.translations/da.json b/homeassistant/components/binary_sensor/.translations/da.json index 56822c2365c..f7bd834561c 100644 --- a/homeassistant/components/binary_sensor/.translations/da.json +++ b/homeassistant/components/binary_sensor/.translations/da.json @@ -39,6 +39,7 @@ "closed": "{entity_name} lukket", "cold": "{entity_name} blev kold", "connected": "{entity_name} tilsluttet", + "moist": "{entity_name} blev fugtig", "moist\u00a7": "{entity_name} blev fugtig", "motion": "{entity_name} begyndte at registrere bev\u00e6gelse", "moving": "{entity_name} begyndte at bev\u00e6ge sig", @@ -53,6 +54,7 @@ "not_hot": "{entity_name} blev ikke varm", "not_locked": "{entity_name} l\u00e5st op", "not_moist": "{entity_name} blev t\u00f8r", + "not_opened": "{entity_name} lukket", "not_present": "{entity_name} ikke til stede", "not_unsafe": "{entity_name} blev sikker", "occupied": "{entity_name} blev optaget", diff --git a/homeassistant/components/binary_sensor/.translations/de.json b/homeassistant/components/binary_sensor/.translations/de.json new file mode 100644 index 00000000000..e246198864b --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/de.json @@ -0,0 +1,94 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} Batterie ist schwach", + "is_cold": "{entity_name} ist kalt", + "is_connected": "{entity_name} ist verbunden", + "is_gas": "{entity_name} erkennt Gas", + "is_hot": "{entity_name} ist hei\u00df", + "is_light": "{entity_name} erkennt Licht", + "is_locked": "{entity_name} ist gesperrt", + "is_moist": "{entity_name} ist feucht", + "is_motion": "{entity_name} erkennt Bewegung", + "is_moving": "{entity_name} bewegt sich", + "is_no_gas": "{entity_name} erkennt kein Gas", + "is_no_light": "{entity_name} erkennt kein Licht", + "is_no_motion": "{entity_name} erkennt keine Bewegung", + "is_no_problem": "{entity_name} erkennt kein Problem", + "is_no_smoke": "{entity_name} erkennt keinen Rauch", + "is_no_sound": "{entity_name} erkennt keine Ger\u00e4usche", + "is_no_vibration": "{entity_name} erkennt keine Vibrationen", + "is_not_bat_low": "{entity_name} Batterie ist normal", + "is_not_cold": "{entity_name} ist nicht kalt", + "is_not_connected": "{entity_name} ist nicht verbunden", + "is_not_hot": "{entity_name} ist nicht hei\u00df", + "is_not_locked": "{entity_name} ist entsperrt", + "is_not_moist": "{entity_name} ist trocken", + "is_not_moving": "{entity_name} bewegt sich nicht", + "is_not_occupied": "{entity_name} ist nicht besch\u00e4ftigt / besetzt", + "is_not_open": "{entity_name} ist geschlossen", + "is_not_plugged_in": "{entity_name} ist nicht angeschlossen", + "is_not_powered": "{entity_name} wird nicht mit Strom versorgt", + "is_not_present": "{entity_name} ist nicht vorhanden", + "is_not_unsafe": "{entity_name} ist sicher", + "is_occupied": "{entity_name} ist besch\u00e4ftigt / besetzt", + "is_off": "{entity_name} ist ausgeschaltet", + "is_on": "{entity_name} ist eingeschaltet", + "is_open": "{entity_name} ist offen", + "is_plugged_in": "{entity_name} ist eingesteckt", + "is_powered": "{entity_name} wird mit Strom versorgt", + "is_present": "{entity_name} ist vorhanden", + "is_problem": "{entity_name} hat ein Problem festgestellt", + "is_smoke": "{entity_name} hat Rauch detektiert", + "is_sound": "{entity_name} hat Ger\u00e4usche detektiert", + "is_unsafe": "{entity_name} ist unsicher", + "is_vibration": "{entity_name} erkennt Vibrationen." + }, + "trigger_type": { + "bat_low": "{entity_name} Batterie schwach", + "closed": "{entity_name} geschlossen", + "cold": "{entity_name} wurde kalt", + "connected": "{entity_name} verbunden", + "gas": "{entity_name} hat Gas detektiert", + "hot": "{entity_name} wurde hei\u00df", + "light": "{entity_name} hat Licht detektiert", + "locked": "{entity_name} gesperrt", + "moist": "{entity_name} wurde feucht", + "moist\u00a7": "{entity_name} wurde feucht", + "motion": "{entity_name} hat Bewegungen detektiert", + "moving": "{entity_name} hat angefangen sich zu bewegen", + "no_gas": "{entity_name} hat kein Gas mehr erkannt", + "no_light": "{entity_name} hat kein Licht mehr erkannt", + "no_motion": "{entity_name} hat keine Bewegung mehr erkannt", + "no_problem": "{entity_name} hat kein Problem mehr erkannt", + "no_smoke": "{entity_name} hat keinen Rauch mehr erkannt", + "no_sound": "{entity_name} hat keine Ger\u00e4usche mehr erkannt", + "no_vibration": "{entity_name}hat keine Vibrationen mehr erkannt", + "not_bat_low": "{entity_name} Batterie normal", + "not_cold": "{entity_name} w\u00e4rmte auf", + "not_connected": "{entity_name} getrennt", + "not_hot": "{entity_name} k\u00fchlte ab", + "not_locked": "{entity_name} entsperrt", + "not_moist": "{entity_name} wurde trocken", + "not_moving": "{entity_name} bewegt sich nicht mehr", + "not_occupied": "{entity_name} wurde frei / inaktiv", + "not_opened": "{entity_name} geschlossen", + "not_plugged_in": "{entity_name} ist nicht angeschlossen", + "not_powered": "{entity_name} nicht mit Strom versorgt", + "not_present": "{entity_name} nicht anwesend", + "not_unsafe": "{entity_name} wurde sicher", + "occupied": "{entity_name} wurde besch\u00e4ftigt / besetzt", + "opened": "{entity_name} ge\u00f6ffnet", + "plugged_in": "{entity_name} eingesteckt", + "powered": "{entity_name} wird mit Strom versorgt", + "present": "{entity_name} anwesend", + "problem": "{entity_name} hat ein Problem festgestellt", + "smoke": "{entity_name} detektiert Rauch", + "sound": "{entity_name} detektiert Ger\u00e4usche", + "turned_off": "{entity_name} ausgeschaltet", + "turned_on": "{entity_name} eingeschaltet", + "unsafe": "{entity_name} ist unsicher", + "vibration": "{entity_name} detektiert Vibrationen" + } + } +} \ 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 8e2d326d9d3..756a370ca3c 100644 --- a/homeassistant/components/binary_sensor/.translations/es.json +++ b/homeassistant/components/binary_sensor/.translations/es.json @@ -53,6 +53,7 @@ "hot": "{entity_name} se est\u00e1 calentando", "light": "{entity_name} empez\u00f3 a detectar la luz", "locked": "{entity_name} bloqueado", + "moist": "{entity_name} se humedece", "moist\u00a7": "{entity_name} se humedeci\u00f3", "motion": "{entity_name} comenz\u00f3 a detectar movimiento", "moving": "{entity_name} empez\u00f3 a moverse", @@ -71,6 +72,7 @@ "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_plugged_in": "{entity_name} desconectado", "not_powered": "{entity_name} no est\u00e1 activado", "not_present": "{entity_name} no est\u00e1 presente", diff --git a/homeassistant/components/binary_sensor/.translations/fr.json b/homeassistant/components/binary_sensor/.translations/fr.json index 80792f16635..4d9bcefbe66 100644 --- a/homeassistant/components/binary_sensor/.translations/fr.json +++ b/homeassistant/components/binary_sensor/.translations/fr.json @@ -9,7 +9,7 @@ "is_light": "{entity_name} d\u00e9tecte de la lumi\u00e8re", "is_locked": "{entity_name} est verrouill\u00e9", "is_moist": "{entity_name} est humide", - "is_motion": "{entity_name} d\u00e9tecte un mouvement", + "is_motion": "{entity_name} d\u00e9tecte du mouvement", "is_moving": "{entity_name} se d\u00e9place", "is_no_gas": "{entity_name} ne d\u00e9tecte pas de gaz", "is_no_light": "{entity_name} ne d\u00e9tecte pas de lumi\u00e8re", @@ -40,12 +40,52 @@ "is_present": "{entity_name} est pr\u00e9sent", "is_problem": "{entity_name} d\u00e9tecte un probl\u00e8me", "is_smoke": "{entity_name} d\u00e9tecte de la fum\u00e9e", - "is_sound": "{entity_name} d\u00e9tecte du son" + "is_sound": "{entity_name} d\u00e9tecte du son", + "is_unsafe": "{entity_name} est dangereux", + "is_vibration": "{entity_name} d\u00e9tecte des vibrations" }, "trigger_type": { + "bat_low": "{entity_name} batterie faible", + "closed": "{entity_name} ferm\u00e9", + "cold": "{entity_name} est devenu froid", + "connected": "{entity_name} connect\u00e9", + "gas": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter du gaz", + "hot": "{entity_name} est devenu chaud", + "light": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter la lumi\u00e8re", + "locked": "{entity_name} verrouill\u00e9", + "moist": "{entity_name} est devenu humide", + "moist\u00a7": "{entity_name} est devenu humide", + "motion": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter du mouvement", + "moving": "{entity_name} a commenc\u00e9 \u00e0 se d\u00e9placer", + "no_gas": "{entity_name} a arr\u00eat\u00e9 de d\u00e9tecter le gaz", + "no_light": "{entity_name} a arr\u00eat\u00e9 de d\u00e9tecter la lumi\u00e8re", + "no_motion": "{entity_name} a arr\u00eat\u00e9 de d\u00e9tecter le mouvement", + "no_problem": "{entity_name} a cess\u00e9 de d\u00e9tecter un probl\u00e8me", + "no_smoke": "{entity_name} a cess\u00e9 de d\u00e9tecter de la fum\u00e9e", + "no_sound": "{entity_name} a cess\u00e9 de d\u00e9tecter du bruit", + "no_vibration": "{entity_name} a cess\u00e9 de d\u00e9tecter des vibrations", + "not_bat_low": "{entity_name} batterie normale", + "not_cold": "{entity_name} n'est plus froid", + "not_connected": "{entity_name} d\u00e9connect\u00e9", + "not_hot": "{entity_name} n'est plus chaud", + "not_locked": "{entity_name} d\u00e9verrouill\u00e9", + "not_moist": "{entity_name} est devenu sec", + "not_moving": "{entity_name} a cess\u00e9 de bouger", + "not_occupied": "{entity_name} est devenu non occup\u00e9", + "not_opened": "{entity_name} ferm\u00e9", + "not_plugged_in": "{entity_name} d\u00e9branch\u00e9", + "not_powered": "{entity_name} non aliment\u00e9", + "not_present": "{entity_name} non pr\u00e9sent", + "not_unsafe": "{entity_name} est devenu s\u00fbr", + "occupied": "{entity_name} est devenu occup\u00e9", + "opened": "{entity_name} ouvert", + "plugged_in": "{entity_name} branch\u00e9", + "powered": "{entity_name} aliment\u00e9", + "present": "{entity_name} pr\u00e9sent", + "problem": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter un probl\u00e8me", "smoke": "{entity_name} commenc\u00e9 \u00e0 d\u00e9tecter la fum\u00e9e", "sound": "{entity_name} commenc\u00e9 \u00e0 d\u00e9tecter le son", - "turned_off": "{entity_name} d\u00e9sactiv\u00e9", + "turned_off": "{entity_name} est d\u00e9sactiv\u00e9", "turned_on": "{entity_name} activ\u00e9", "unsafe": "{entity_name} est devenu dangereux", "vibration": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter les vibrations" diff --git a/homeassistant/components/binary_sensor/.translations/it.json b/homeassistant/components/binary_sensor/.translations/it.json index 0583a4d4f74..c69f5a07a41 100644 --- a/homeassistant/components/binary_sensor/.translations/it.json +++ b/homeassistant/components/binary_sensor/.translations/it.json @@ -53,6 +53,7 @@ "hot": "{entity_name} \u00e8 diventato caldo", "light": "{entity_name} ha iniziato a rilevare la luce", "locked": "{entity_name} bloccato", + "moist": "{entity_name} diventato umido", "moist\u00a7": "{entity_name} \u00e8 diventato umido", "motion": "{entity_name} ha iniziato a rilevare il movimento", "moving": "{entity_name} ha iniziato a muoversi", @@ -71,6 +72,7 @@ "not_moist": "{entity_name} \u00e8 diventato asciutto", "not_moving": "{entity_name} ha smesso di muoversi", "not_occupied": "{entity_name} non \u00e8 occupato", + "not_opened": "{entity_name} chiuso", "not_plugged_in": "{entity_name} \u00e8 scollegato", "not_powered": "{entity_name} non \u00e8 alimentato", "not_present": "{entity_name} non \u00e8 presente", diff --git a/homeassistant/components/binary_sensor/.translations/ko.json b/homeassistant/components/binary_sensor/.translations/ko.json index 3c12eabe8ff..167708c2cf1 100644 --- a/homeassistant/components/binary_sensor/.translations/ko.json +++ b/homeassistant/components/binary_sensor/.translations/ko.json @@ -1,7 +1,7 @@ { "device_automation": { "condition_type": { - "is_bat_low": "{entity_name} \ubc30\ud130\ub9ac \uc794\ub7c9\uc774 \ubd80\uc871\ud569\ub2c8\ub2e4", + "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", @@ -18,20 +18,20 @@ "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} \ubc30\ud130\ub9ac\uac00 \uc815\uc0c1\uc785\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_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_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", @@ -45,8 +45,50 @@ "is_vibration": "{entity_name} \uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud588\uc2b5\ub2c8\ub2e4" }, "trigger_type": { - "bat_low": "{entity_name} \ubc30\ud130\ub9ac \uc794\ub7c9 \ubd80\uc871", - "closed": "{entity_name} \ub2eb\ud798" + "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" } } } \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/lb.json b/homeassistant/components/binary_sensor/.translations/lb.json index 0b10e1f51a5..c65ae94396b 100644 --- a/homeassistant/components/binary_sensor/.translations/lb.json +++ b/homeassistant/components/binary_sensor/.translations/lb.json @@ -53,6 +53,7 @@ "hot": "{entity_name} gouf waarm", "light": "{entity_name} huet ugefange Luucht z'entdecken", "locked": "{entity_name} gespaart", + "moist": "{entity_name} gouf fiicht", "moist\u00a7": "{entity_name} gouf fiicht", "motion": "{entity_name} huet ugefaange Beweegung z'entdecken", "moving": "{entity_name} huet ugefaangen sech ze beweegen", @@ -71,6 +72,7 @@ "not_moist": "{entity_name} gouf dr\u00e9chen", "not_moving": "{entity_name} huet opgehale sech ze beweegen", "not_occupied": "{entity_name} gouf fr\u00e4i", + "not_opened": "{entity_name} gouf zougemaach", "not_plugged_in": "{entity_name} net ugeschloss", "not_powered": "{entity_name} net aliment\u00e9iert", "not_present": "{entity_name} net pr\u00e4sent", diff --git a/homeassistant/components/binary_sensor/.translations/lv.json b/homeassistant/components/binary_sensor/.translations/lv.json new file mode 100644 index 00000000000..7668dfa5ac8 --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/lv.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "trigger_type": { + "turned_off": "{entity_name} tika izsl\u0113gta", + "turned_on": "{entity_name} tika iesl\u0113gta" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/nl.json b/homeassistant/components/binary_sensor/.translations/nl.json new file mode 100644 index 00000000000..508a06b38a2 --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/nl.json @@ -0,0 +1,94 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} batterij is bijna leeg", + "is_cold": "{entity_name} is koud", + "is_connected": "{entity_name} is verbonden", + "is_gas": "{entity_name} detecteert gas", + "is_hot": "{entity_name} is hot", + "is_light": "{entity_name} detecteert licht", + "is_locked": "{entity_name} is vergrendeld", + "is_moist": "{entity_name} is vochtig", + "is_motion": "{entity_name} detecteert beweging", + "is_moving": "{entity_name} is in beweging", + "is_no_gas": "{entity_name} detecteert geen gas", + "is_no_light": "{entity_name} detecteert geen licht", + "is_no_motion": "{entity_name} detecteert geen beweging", + "is_no_problem": "{entity_name} detecteert geen probleem", + "is_no_smoke": "{entity_name} detecteert geen rook", + "is_no_sound": "{entity_name} detecteert geen geluid", + "is_no_vibration": "{entity_name} detecteert geen trillingen", + "is_not_bat_low": "{entity_name} batterij is normaal", + "is_not_cold": "{entity_name} is niet koud", + "is_not_connected": "{entity_name} is niet verbonden", + "is_not_hot": "{entity_name} is niet heet", + "is_not_locked": "{entity_name} is ontgrendeld", + "is_not_moist": "{entity_name} is droog", + "is_not_moving": "{entity_name} beweegt niet", + "is_not_occupied": "{entity_name} is niet bezet", + "is_not_open": "{entity_name} is gesloten", + "is_not_plugged_in": "{entity_name} is niet aangesloten", + "is_not_powered": "{entity_name} is niet van stroom voorzien...", + "is_not_present": "{entity_name} is niet aanwezig", + "is_not_unsafe": "{entity_name} is veilig", + "is_occupied": "{entity_name} bezet is", + "is_off": "{entity_name} is uitgeschakeld", + "is_on": "{entity_name} is ingeschakeld", + "is_open": "{entity_name} is open", + "is_plugged_in": "{entity_name} is aangesloten", + "is_powered": "{entity_name} is van stroom voorzien....", + "is_present": "{entity_name} is aanwezig", + "is_problem": "{entity_name} detecteert een probleem", + "is_smoke": "{entity_name} detecteert rook", + "is_sound": "{entity_name} detecteert geluid", + "is_unsafe": "{entity_name} is onveilig", + "is_vibration": "{entity_name} detecteert trillingen" + }, + "trigger_type": { + "bat_low": "{entity_name} batterij bijna leeg", + "closed": "{entity_name} gesloten", + "cold": "{entity_name} werd koud", + "connected": "{entity_name} verbonden", + "gas": "{entity_name} begon gas te detecteren", + "hot": "{entity_name} werd heet", + "light": "{entity_name} begon licht te detecteren", + "locked": "{entity_name} vergrendeld", + "moist": "{entity_name} werd vochtig", + "moist\u00a7": "{entity_name} werd vochtig", + "motion": "{entity_name} begon beweging te detecteren", + "moving": "{entity_name} begon te bewegen", + "no_gas": "{entity_name} is gestopt met het detecteren van gas", + "no_light": "{entity_name} gestopt met het detecteren van licht", + "no_motion": "{entity_name} gestopt met het detecteren van beweging", + "no_problem": "{entity_name} gestopt met het detecteren van het probleem", + "no_smoke": "{entity_name} gestopt met het detecteren van rook", + "no_sound": "{entity_name} gestopt met het detecteren van geluid", + "no_vibration": "{entity_name} gestopt met het detecteren van trillingen", + "not_bat_low": "{entity_name} batterij normaal", + "not_cold": "{entity_name} werd niet koud", + "not_connected": "{entity_name} verbroken", + "not_hot": "{entity_name} werd niet warm", + "not_locked": "{entity_name} ontgrendeld", + "not_moist": "{entity_name} werd droog", + "not_moving": "{entity_name} gestopt met bewegen", + "not_occupied": "{entity_name} werd niet bezet", + "not_opened": "{entity_name} gesloten", + "not_plugged_in": "{entity_name} niet verbonden", + "not_powered": "{entity_name} niet ingeschakeld", + "not_present": "{entity_name} is niet aanwezig", + "not_unsafe": "{entity_name} werd veilig", + "occupied": "{entity_name} werd bezet", + "opened": "{entity_name} geopend", + "plugged_in": "{entity_name} aangesloten", + "powered": "{entity_name} heeft vermogen", + "present": "{entity_name} aanwezig", + "problem": "{entity_name} begonnen met het detecteren van een probleem", + "smoke": "{entity_name} begon rook te detecteren", + "sound": "{entity_name} begon geluid te detecteren", + "turned_off": "{entity_name} uitgeschakeld", + "turned_on": "{entity_name} ingeschakeld", + "unsafe": "{entity_name} werd onveilig", + "vibration": "{entity_name} begon trillingen te detecteren" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/no.json b/homeassistant/components/binary_sensor/.translations/no.json index 5a1916bce59..4194102948b 100644 --- a/homeassistant/components/binary_sensor/.translations/no.json +++ b/homeassistant/components/binary_sensor/.translations/no.json @@ -53,6 +53,7 @@ "hot": "{entity_name} ble varm", "light": "{entity_name} begynte \u00e5 registrere lys", "locked": "{entity_name} l\u00e5st", + "moist": "{entity_name} ble fuktig", "moist\u00a7": "{entity_name} ble fuktig", "motion": "{entity_name} begynte \u00e5 registrere bevegelse", "moving": "{entity_name} begynte \u00e5 bevege seg", @@ -71,6 +72,7 @@ "not_moist": "{entity_name} ble t\u00f8rr", "not_moving": "{entity_name} sluttet \u00e5 bevege seg", "not_occupied": "{entity_name} ble ledig", + "not_opened": "{entity_name} stengt", "not_plugged_in": "{entity_name} koblet fra", "not_powered": "{entity_name} spenningsl\u00f8s", "not_present": "{entity_name} ikke til stede", diff --git a/homeassistant/components/binary_sensor/.translations/pl.json b/homeassistant/components/binary_sensor/.translations/pl.json index a7f0bd516a0..bc474e3d514 100644 --- a/homeassistant/components/binary_sensor/.translations/pl.json +++ b/homeassistant/components/binary_sensor/.translations/pl.json @@ -45,14 +45,15 @@ "is_vibration": "sensor {entity_name} wykrywa wibracje" }, "trigger_type": { - "bat_low": "bateria {entity_name} stanie si\u0119 roz\u0142adowana", - "closed": "zamkni\u0119cie {entity_name}", + "bat_low": "nast\u0105pi roz\u0142adowanie baterii {entity_name}", + "closed": "nast\u0105pi zamkni\u0119cie {entity_name}", "cold": "sensor {entity_name} wykryje zimno", - "connected": "pod\u0142\u0105czenie {entity_name}", + "connected": "nast\u0105pi pod\u0142\u0105czenie {entity_name}", "gas": "sensor {entity_name} wykryje gaz", "hot": "sensor {entity_name} wykryje gor\u0105co", "light": "sensor {entity_name} wykryje \u015bwiat\u0142o", - "locked": "zamkni\u0119cie {entity_name}", + "locked": "nast\u0105pi zamkni\u0119cie {entity_name}", + "moist": "nast\u0105pi wykrycie wilgoci {entity_name}", "moist\u00a7": "sensor {entity_name} wykryje wilgo\u0107", "motion": "sensor {entity_name} wykryje ruch", "moving": "sensor {entity_name} zacznie porusza\u0107 si\u0119", @@ -63,28 +64,29 @@ "no_smoke": "sensor {entity_name} przestanie wykrywa\u0107 dym", "no_sound": "sensor {entity_name} przestanie wykrywa\u0107 d\u017awi\u0119k", "no_vibration": "sensor {entity_name} przestanie wykrywa\u0107 wibracje", - "not_bat_low": "bateria {entity_name} staje si\u0119 na\u0142adowana", + "not_bat_low": "nast\u0105pi na\u0142adowanie baterii {entity_name}", "not_cold": "sensor {entity_name} przestanie wykrywa\u0107 zimno", - "not_connected": "roz\u0142\u0105czenie {entity_name}", + "not_connected": "nast\u0105pi roz\u0142\u0105czenie {entity_name}", "not_hot": "sensor {entity_name} przestanie wykrywa\u0107 gor\u0105co", - "not_locked": "otwarcie {entity_name}", + "not_locked": "nast\u0105pi otwarcie {entity_name}", "not_moist": "sensor {entity_name} przestanie wykrywa\u0107 wilgo\u0107", "not_moving": "sensor {entity_name} przestanie porusza\u0107 si\u0119", - "not_occupied": "sensor {entity_name} przesta\u0142 by\u0107 zaj\u0119ty", - "not_plugged_in": "od\u0142\u0105czenie {entity_name}", - "not_powered": "od\u0142\u0105czenie zasilania {entity_name}", + "not_occupied": "sensor {entity_name} przestanie by\u0107 zaj\u0119ty", + "not_opened": "nast\u0105pi zamkni\u0119cie {entity_name}", + "not_plugged_in": "nast\u0105pi od\u0142\u0105czenie {entity_name}", + "not_powered": "nast\u0105pi od\u0142\u0105czenie zasilania {entity_name}", "not_present": "sensor {entity_name} przestanie wykrywa\u0107 obecno\u015b\u0107", "not_unsafe": "sensor {entity_name} przestanie wykrywa\u0107 niebezpiecze\u0144stwo", - "occupied": "sensor {entity_name} sta\u0142 si\u0119 zaj\u0119ty", - "opened": "otwarcie {entity_name}", - "plugged_in": "pod\u0142\u0105czenie {entity_name}", - "powered": "pod\u0142\u0105czenie zasilenia {entity_name}", + "occupied": "sensor {entity_name} stanie si\u0119 zaj\u0119ty", + "opened": "nast\u0105pi otwarcie {entity_name}", + "plugged_in": "nast\u0105pi pod\u0142\u0105czenie {entity_name}", + "powered": "nast\u0105pi pod\u0142\u0105czenie zasilenia {entity_name}", "present": "sensor {entity_name} wykryje obecno\u015b\u0107", "problem": "sensor {entity_name} wykryje problem", "smoke": "sensor {entity_name} wykryje dym", "sound": "sensor {entity_name} wykryje d\u017awi\u0119k", - "turned_off": "wy\u0142\u0105czenie {entity_name}", - "turned_on": "w\u0142\u0105czenie {entity_name}", + "turned_off": "nast\u0105pi wy\u0142\u0105czenie {entity_name}", + "turned_on": "nast\u0105pi w\u0142\u0105czenie {entity_name}", "unsafe": "sensor {entity_name} wykryje niebezpiecze\u0144stwo", "vibration": "sensor {entity_name} wykryje wibracje" } diff --git a/homeassistant/components/binary_sensor/.translations/pt.json b/homeassistant/components/binary_sensor/.translations/pt.json new file mode 100644 index 00000000000..aa16576d2c1 --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/pt.json @@ -0,0 +1,41 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "a bateria {entity_name} est\u00e1 baixa", + "is_cold": "{entity_name} est\u00e1 frio", + "is_connected": "{entity_name} est\u00e1 ligado", + "is_gas": "{entity_name} est\u00e1 a detectar g\u00e1s", + "is_hot": "{entity_name} est\u00e1 quente", + "is_light": "{entity_name} est\u00e1 a detectar luz", + "is_locked": "{entity_name} est\u00e1 fechado", + "is_moist": "{entity_name} est\u00e1 h\u00famido", + "is_motion": "{entity_name} est\u00e1 a detectar movimento", + "is_moving": "{entity_name} est\u00e1 a mexer", + "is_not_open": "{entity_name} est\u00e1 fechada", + "is_off": "{entity_name} est\u00e1 desligado", + "is_on": "{entity_name} est\u00e1 ligado", + "is_vibration": "{entity_name} est\u00e1 a detectar vibra\u00e7\u00f5es" + }, + "trigger_type": { + "closed": "{entity_name} est\u00e1 fechado", + "moist": "ficou h\u00famido {entity_name}", + "not_opened": "fechado {entity_name}", + "not_plugged_in": "{entity_name} desligado", + "not_powered": "{entity_name} n\u00e3o alimentado", + "not_present": "ausente {entity_name}", + "not_unsafe": "ficou seguro {entity_name}", + "occupied": "ficou ocupado {entity_name}", + "opened": "{entity_name} aberto", + "plugged_in": "{entity_name} ligado", + "powered": "{entity_name} alimentado", + "present": "{entity_name} presente", + "problem": "foi detectado problema em {entity_name}", + "smoke": "foi detectado fumo em {entity_name}", + "sound": "foram detectadas sons em {entity_name}", + "turned_off": "foi desligado {entity_name}", + "turned_on": "foi ligado {entity_name}", + "unsafe": "ficou inseguro {entity_name}", + "vibration": "foram detectadas vibra\u00e7\u00f5es em {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/ru.json b/homeassistant/components/binary_sensor/.translations/ru.json index 7d73cb8d4aa..cce765c8d84 100644 --- a/homeassistant/components/binary_sensor/.translations/ru.json +++ b/homeassistant/components/binary_sensor/.translations/ru.json @@ -1,15 +1,94 @@ { "device_automation": { "condition_type": { - "is_bat_low": "{entity_name}: \u043d\u0438\u0437\u043a\u0438\u0439 \u0437\u0430\u0440\u044f\u0434", - "is_cold": "{entity_name}: \u0445\u043e\u043b\u043e\u0434\u043d\u043e", - "is_connected": "{entity_name}: \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e", - "is_gas": "{entity_name}: \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d \u0433\u0430\u0437", - "is_hot": "{entity_name}: \u0433\u043e\u0440\u044f\u0447\u043e", - "is_light": "{entity_name}: \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d \u0441\u0432\u0435\u0442", - "is_locked": "{entity_name}: \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043e", - "is_moist": "{entity_name}: \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0430 \u0432\u043b\u0430\u0433\u0430", - "is_motion": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435" + "is_bat_low": "{entity_name} \u0432 \u0440\u0430\u0437\u0440\u044f\u0436\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_cold": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043e\u0445\u043b\u0430\u0436\u0434\u0435\u043d\u0438\u0435", + "is_connected": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435", + "is_gas": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0433\u0430\u0437", + "is_hot": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043d\u0430\u0433\u0440\u0435\u0432", + "is_light": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0441\u0432\u0435\u0442", + "is_locked": "{entity_name} \u0432 \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_moist": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0432\u043b\u0430\u0433\u0443", + "is_motion": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435", + "is_moving": "{entity_name} \u043f\u0435\u0440\u0435\u043c\u0435\u0449\u0430\u0435\u0442\u0441\u044f", + "is_no_gas": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0433\u0430\u0437", + "is_no_light": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0441\u0432\u0435\u0442", + "is_no_motion": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435", + "is_no_problem": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443", + "is_no_smoke": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0434\u044b\u043c", + "is_no_sound": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0437\u0432\u0443\u043a", + "is_no_vibration": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0432\u0438\u0431\u0440\u0430\u0446\u0438\u044e", + "is_not_bat_low": "{entity_name} \u0432 \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_not_cold": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043e\u0445\u043b\u0430\u0436\u0434\u0435\u043d\u0438\u0435", + "is_not_connected": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435", + "is_not_hot": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043d\u0430\u0433\u0440\u0435\u0432", + "is_not_locked": "{entity_name} \u0432 \u0440\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_not_moist": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0432\u043b\u0430\u0433\u0443", + "is_not_moving": "{entity_name} \u043d\u0435 \u043f\u0435\u0440\u0435\u043c\u0435\u0449\u0430\u0435\u0442\u0441\u044f", + "is_not_occupied": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", + "is_not_open": "{entity_name} \u0432 \u0437\u0430\u043a\u0440\u044b\u0442\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_not_plugged_in": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435", + "is_not_powered": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u044d\u043d\u0435\u0440\u0433\u0438\u044e", + "is_not_present": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", + "is_not_unsafe": "{entity_name} \u0432 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_occupied": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", + "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", + "is_open": "{entity_name} \u0432 \u043e\u0442\u043a\u0440\u044b\u0442\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_plugged_in": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435", + "is_powered": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u044d\u043d\u0435\u0440\u0433\u0438\u044e", + "is_present": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", + "is_problem": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443", + "is_smoke": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0434\u044b\u043c", + "is_sound": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0437\u0432\u0443\u043a", + "is_unsafe": "{entity_name} \u0432 \u043d\u0435\u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_vibration": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0432\u0438\u0431\u0440\u0430\u0446\u0438\u044e" + }, + "trigger_type": { + "bat_low": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043d\u0438\u0437\u043a\u0438\u0439 \u0437\u0430\u0440\u044f\u0434", + "closed": "{entity_name} \u0437\u0430\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", + "cold": "{entity_name} \u043e\u0445\u043b\u0430\u0436\u0434\u0430\u0435\u0442\u0441\u044f", + "connected": "{entity_name} \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f", + "gas": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0433\u0430\u0437", + "hot": "{entity_name} \u043d\u0430\u0433\u0440\u0435\u0432\u0430\u0435\u0442\u0441\u044f", + "light": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0441\u0432\u0435\u0442", + "locked": "{entity_name} \u0431\u043b\u043e\u043a\u0438\u0440\u0443\u0435\u0442\u0441\u044f", + "moist": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0432\u043b\u0430\u0433\u0443", + "moist\u00a7": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0432\u043b\u0430\u0433\u0443", + "motion": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435", + "moving": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043f\u0435\u0440\u0435\u043c\u0435\u0449\u0435\u043d\u0438\u0435", + "no_gas": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0433\u0430\u0437", + "no_light": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0441\u0432\u0435\u0442", + "no_motion": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435", + "no_problem": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443", + "no_smoke": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0434\u044b\u043c", + "no_sound": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0437\u0432\u0443\u043a", + "no_vibration": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0432\u0438\u0431\u0440\u0430\u0446\u0438\u044e", + "not_bat_low": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u044b\u0439 \u0437\u0430\u0440\u044f\u0434", + "not_cold": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0445\u043b\u0430\u0436\u0434\u0430\u0442\u044c\u0441\u044f", + "not_connected": "{entity_name} \u043e\u0442\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f", + "not_hot": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043d\u0430\u0433\u0440\u0435\u0432\u0430\u0442\u044c\u0441\u044f", + "not_locked": "{entity_name} \u0440\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u0443\u0435\u0442\u0441\u044f", + "not_moist": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0432\u043b\u0430\u0433\u0443", + "not_moving": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043f\u0435\u0440\u0435\u043c\u0435\u0449\u0435\u043d\u0438\u0435", + "not_occupied": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", + "not_opened": "{entity_name} \u0437\u0430\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", + "not_plugged_in": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435", + "not_powered": "{entity_name} \u043d\u0435 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043d\u0430\u043b\u0438\u0447\u0438\u0435 \u043f\u0438\u0442\u0430\u043d\u0438\u044f", + "not_present": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", + "not_unsafe": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u044c", + "occupied": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", + "opened": "{entity_name} \u043e\u0442\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", + "plugged_in": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435", + "powered": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043d\u0430\u043b\u0438\u0447\u0438\u0435 \u043f\u0438\u0442\u0430\u043d\u0438\u044f", + "present": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", + "problem": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443", + "smoke": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0434\u044b\u043c", + "sound": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0437\u0432\u0443\u043a", + "turned_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f", + "turned_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f", + "unsafe": "{entity_name} \u043d\u0435 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u044c", + "vibration": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0432\u0438\u0431\u0440\u0430\u0446\u0438\u044e" } } } \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/sl.json b/homeassistant/components/binary_sensor/.translations/sl.json index 6b4e144d9a6..2004caeb342 100644 --- a/homeassistant/components/binary_sensor/.translations/sl.json +++ b/homeassistant/components/binary_sensor/.translations/sl.json @@ -53,6 +53,7 @@ "hot": "{entity_name} je postal vro\u010d", "light": "{entity_name} za\u010del zaznavati svetlobo", "locked": "{entity_name} zaklenjen", + "moist": "{entity_name} postal vla\u017een", "moist\u00a7": "{entity_name} postal vla\u017een", "motion": "{entity_name} za\u010del zaznavati gibanje", "moving": "{entity_name} se je za\u010del premikati", @@ -71,6 +72,7 @@ "not_moist": "{entity_name} je postalo suh", "not_moving": "{entity_name} se je prenehal premikati", "not_occupied": "{entity_name} ni zaseden", + "not_opened": "{entity_name} zaprto", "not_plugged_in": "{entity_name} odklopljen", "not_powered": "{entity_name} ni napajan", "not_present": "{entity_name} ni prisoten", diff --git a/homeassistant/components/binary_sensor/.translations/zh-Hant.json b/homeassistant/components/binary_sensor/.translations/zh-Hant.json index 36c72dcb9e6..046b999cb8c 100644 --- a/homeassistant/components/binary_sensor/.translations/zh-Hant.json +++ b/homeassistant/components/binary_sensor/.translations/zh-Hant.json @@ -53,6 +53,7 @@ "hot": "{entity_name} \u5df2\u8b8a\u71b1", "light": "{entity_name} \u5df2\u958b\u59cb\u5075\u6e2c\u5149\u7dda", "locked": "{entity_name} \u5df2\u4e0a\u9396", + "moist": "{entity_name} \u5df2\u8b8a\u6f6e\u6fd5", "moist\u00a7": "{entity_name} \u5df2\u8b8a\u6f6e\u6fd5", "motion": "{entity_name} \u5df2\u5075\u6e2c\u5230\u52d5\u4f5c", "moving": "{entity_name} \u958b\u59cb\u79fb\u52d5", @@ -71,6 +72,7 @@ "not_moist": "{entity_name} \u5df2\u8b8a\u4e7e", "not_moving": "{entity_name} \u505c\u6b62\u79fb\u52d5", "not_occupied": "{entity_name} \u672a\u6709\u4eba", + "not_opened": "{entity_name} \u5df2\u95dc\u9589", "not_plugged_in": "{entity_name} \u672a\u63d2\u5165", "not_powered": "{entity_name} \u672a\u901a\u96fb", "not_present": "{entity_name} \u672a\u51fa\u73fe", diff --git a/homeassistant/components/binary_sensor/device_condition.py b/homeassistant/components/binary_sensor/device_condition.py index 1749ea91c5b..0766d82c727 100644 --- a/homeassistant/components/binary_sensor/device_condition.py +++ b/homeassistant/components/binary_sensor/device_condition.py @@ -1,10 +1,10 @@ """Implemenet device conditions for binary sensor.""" -from typing import List +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_TYPE +from homeassistant.const import ATTR_DEVICE_CLASS, CONF_ENTITY_ID, CONF_FOR, CONF_TYPE from homeassistant.helpers import condition, config_validation as cv from homeassistant.helpers.entity_registry import ( async_entries_for_device, @@ -188,13 +188,16 @@ CONDITION_SCHEMA = cv.DEVICE_CONDITION_BASE_SCHEMA.extend( { vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_TYPE): vol.In(IS_OFF + IS_ON), + vol.Optional(CONF_FOR): cv.positive_time_period_dict, } ) -async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[dict]: +async def async_get_conditions( + hass: HomeAssistant, device_id: str +) -> List[Dict[str, str]]: """List device conditions.""" - conditions: List[dict] = [] + conditions: List[Dict[str, str]] = [] entity_registry = await async_get_registry(hass) entries = [ entry @@ -244,5 +247,16 @@ def async_condition_from_config( condition.CONF_ENTITY_ID: config[CONF_ENTITY_ID], condition.CONF_STATE: stat, } + if CONF_FOR in config: + state_config[CONF_FOR] = config[CONF_FOR] - return condition.state_from_config(state_config, config_validation) + return condition.state_from_config(state_config) + + +async def async_get_condition_capabilities(hass: HomeAssistant, config: dict) -> dict: + """List condition capabilities.""" + return { + "extra_fields": vol.Schema( + {vol.Optional(CONF_FOR): cv.positive_time_period_dict} + ) + } diff --git a/homeassistant/components/binary_sensor/device_trigger.py b/homeassistant/components/binary_sensor/device_trigger.py index f138bcfd5a8..c51b9749288 100644 --- a/homeassistant/components/binary_sensor/device_trigger.py +++ b/homeassistant/components/binary_sensor/device_trigger.py @@ -191,6 +191,7 @@ async def async_attach_trigger(hass, config, action, automation_info): to_state = "off" state_config = { + state_automation.CONF_PLATFORM: "state", state_automation.CONF_ENTITY_ID: config[CONF_ENTITY_ID], state_automation.CONF_FROM: from_state, state_automation.CONF_TO: to_state, @@ -198,6 +199,7 @@ async def async_attach_trigger(hass, config, action, automation_info): if CONF_FOR in config: state_config[CONF_FOR] = config[CONF_FOR] + state_config = state_automation.TRIGGER_SCHEMA(state_config) return await state_automation.async_attach_trigger( hass, state_config, action, automation_info, platform_type="device" ) @@ -240,7 +242,7 @@ async def async_get_triggers(hass, device_id): return triggers -async def async_get_trigger_capabilities(hass, trigger): +async def async_get_trigger_capabilities(hass, config): """List trigger capabilities.""" return { "extra_fields": vol.Schema( diff --git a/homeassistant/components/bitcoin/sensor.py b/homeassistant/components/bitcoin/sensor.py index 4d8d5643826..b62bb434e85 100644 --- a/homeassistant/components/bitcoin/sensor.py +++ b/homeassistant/components/bitcoin/sensor.py @@ -1,11 +1,12 @@ """Bitcoin information service that uses blockchain.info.""" -import logging from datetime import timedelta +import logging +from blockchain import exchangerates, statistics import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_DISPLAY_OPTIONS, ATTR_ATTRIBUTION, CONF_CURRENCY +from homeassistant.const import ATTR_ATTRIBUTION, CONF_CURRENCY, CONF_DISPLAY_OPTIONS import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -55,7 +56,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Bitcoin sensors.""" - from blockchain import exchangerates currency = config.get(CONF_CURRENCY) @@ -169,7 +169,6 @@ class BitcoinData: def update(self): """Get the latest data from blockchain.info.""" - from blockchain import statistics, exchangerates self.stats = statistics.get() self.ticker = exchangerates.get_ticker() diff --git a/homeassistant/components/blackbird/media_player.py b/homeassistant/components/blackbird/media_player.py index eca7fa84f50..e1aa7200c07 100644 --- a/homeassistant/components/blackbird/media_player.py +++ b/homeassistant/components/blackbird/media_player.py @@ -2,9 +2,11 @@ import logging import socket +from pyblackbird import get_blackbird +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, @@ -72,9 +74,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): port = config.get(CONF_PORT) host = config.get(CONF_HOST) - from pyblackbird import get_blackbird - from serial import SerialException - connection = None if port is not None: try: diff --git a/homeassistant/components/blink/__init__.py b/homeassistant/components/blink/__init__.py index bd11572ba1c..e233a8b21d8 100644 --- a/homeassistant/components/blink/__init__.py +++ b/homeassistant/components/blink/__init__.py @@ -1,22 +1,24 @@ """Support for Blink Home Camera System.""" -import logging from datetime import timedelta +import logging + +from blinkpy import blinkpy import voluptuous as vol -from homeassistant.helpers import config_validation as cv, discovery from homeassistant.const import ( - CONF_USERNAME, - CONF_PASSWORD, - CONF_NAME, - CONF_SCAN_INTERVAL, CONF_BINARY_SENSORS, - CONF_SENSORS, CONF_FILENAME, - CONF_MONITORED_CONDITIONS, CONF_MODE, + CONF_MONITORED_CONDITIONS, + CONF_NAME, CONF_OFFSET, + CONF_PASSWORD, + CONF_SCAN_INTERVAL, + CONF_SENSORS, + CONF_USERNAME, TEMP_FAHRENHEIT, ) +from homeassistant.helpers import config_validation as cv, discovery _LOGGER = logging.getLogger(__name__) @@ -97,7 +99,6 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): """Set up Blink System.""" - from blinkpy import blinkpy conf = config[BLINK_DATA] username = conf[CONF_USERNAME] diff --git a/homeassistant/components/blink/manifest.json b/homeassistant/components/blink/manifest.json index a38ba0bd613..47cded00cc0 100644 --- a/homeassistant/components/blink/manifest.json +++ b/homeassistant/components/blink/manifest.json @@ -3,7 +3,7 @@ "name": "Blink", "documentation": "https://www.home-assistant.io/integrations/blink", "requirements": [ - "blinkpy==0.14.1" + "blinkpy==0.14.2" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/blinksticklight/light.py b/homeassistant/components/blinksticklight/light.py index 5f3cb7ebfd1..197213f7473 100644 --- a/homeassistant/components/blinksticklight/light.py +++ b/homeassistant/components/blinksticklight/light.py @@ -1,15 +1,16 @@ """Support for Blinkstick lights.""" import logging +from blinkstick import blinkstick import voluptuous as vol from homeassistant.components.light import ( ATTR_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 @@ -33,7 +34,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up Blinkstick device specified by serial number.""" - from blinkstick import blinkstick name = config.get(CONF_NAME) serial = config.get(CONF_SERIAL) diff --git a/homeassistant/components/blockchain/sensor.py b/homeassistant/components/blockchain/sensor.py index c95ccb3fed3..6d17484bdd7 100644 --- a/homeassistant/components/blockchain/sensor.py +++ b/homeassistant/components/blockchain/sensor.py @@ -1,12 +1,13 @@ """Support for Blockchain.info sensors.""" -import logging from datetime import timedelta +import logging +from pyblockchain import get_balance, validate_address 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__) @@ -31,7 +32,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Blockchain.info sensors.""" - from pyblockchain import validate_address addresses = config.get(CONF_ADDRESSES) name = config.get(CONF_NAME) @@ -81,6 +81,5 @@ class BlockchainSensor(Entity): def update(self): """Get the latest state of the sensor.""" - from pyblockchain import get_balance self._state = get_balance(self.addresses) diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index bf0568aed16..702cf5ddc30 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -3,14 +3,16 @@ import asyncio from asyncio.futures import CancelledError from datetime import timedelta import logging +from urllib import parse import aiohttp from aiohttp.client_exceptions import ClientError from aiohttp.hdrs import CONNECTION, KEEP_ALIVE import async_timeout import voluptuous as vol +import xmltodict -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_MEDIA_ENQUEUE, DOMAIN, @@ -329,7 +331,6 @@ class BluesoundPlayer(MediaPlayerDevice): self, method, raise_timeout=False, allow_offline=False ): """Send command to the player.""" - import xmltodict if not self._is_online and not allow_offline: return @@ -370,7 +371,6 @@ class BluesoundPlayer(MediaPlayerDevice): async def async_update_status(self): """Use the poll session to always get the status of the player.""" - import xmltodict response = None @@ -690,7 +690,6 @@ class BluesoundPlayer(MediaPlayerDevice): @property def source(self): """Name of the current input source.""" - from urllib import parse if self._status is None or (self.is_grouped and not self.is_master): return None diff --git a/homeassistant/components/bluetooth_le_tracker/device_tracker.py b/homeassistant/components/bluetooth_le_tracker/device_tracker.py index 29eecdfd077..18edd750639 100644 --- a/homeassistant/components/bluetooth_le_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_le_tracker/device_tracker.py @@ -2,6 +2,8 @@ import asyncio import logging +import pygatt # pylint: disable=import-error + from homeassistant.helpers.event import track_point_in_utc_time from homeassistant.components.device_tracker.legacy import ( YAML_DEVICES, @@ -26,8 +28,6 @@ MIN_SEEN_NEW = 5 def setup_scanner(hass, config, see, discovery_info=None): """Set up the Bluetooth LE Scanner.""" - # pylint: disable=import-error - import pygatt new_devices = {} hass.data.setdefault(DATA_BLE, {DATA_BLE_ADAPTER: None}) diff --git a/homeassistant/components/bluetooth_le_tracker/manifest.json b/homeassistant/components/bluetooth_le_tracker/manifest.json index 30ed924a9dc..d9f4cb0a2b5 100644 --- a/homeassistant/components/bluetooth_le_tracker/manifest.json +++ b/homeassistant/components/bluetooth_le_tracker/manifest.json @@ -3,7 +3,7 @@ "name": "Bluetooth le tracker", "documentation": "https://www.home-assistant.io/integrations/bluetooth_le_tracker", "requirements": [ - "pygatt[GATTTOOL]==4.0.1" + "pygatt[GATTTOOL]==4.0.5" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/bme280/sensor.py b/homeassistant/components/bme280/sensor.py index ee4e1731156..b9bc18e6abf 100644 --- a/homeassistant/components/bme280/sensor.py +++ b/homeassistant/components/bme280/sensor.py @@ -3,6 +3,9 @@ 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 voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -76,8 +79,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the BME280 sensor.""" - import smbus # pylint: disable=import-error - from i2csense.bme280 import BME280 # pylint: disable=import-error SENSOR_TYPES[SENSOR_TEMP][1] = hass.config.units.temperature_unit name = config.get(CONF_NAME) diff --git a/homeassistant/components/bme680/sensor.py b/homeassistant/components/bme680/sensor.py index a36b35ea9d4..5a1e9fd120f 100644 --- a/homeassistant/components/bme680/sensor.py +++ b/homeassistant/components/bme680/sensor.py @@ -1,14 +1,15 @@ """Support for BME680 Sensor over SMBus.""" -import importlib import logging +import threading +from time import sleep, time -from time import time, sleep - +from smbus import SMBus # pylint: disable=import-error +import bme680 # 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.temperature import celsius_to_fahrenheit @@ -121,9 +122,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= def _setup_bme680(config): """Set up and configure the BME680 sensor.""" - from smbus import SMBus # pylint: disable=import-error - - bme680 = importlib.import_module("bme680") sensor_handler = None sensor = None @@ -224,7 +222,6 @@ class BME680Handler: self._gas_baseline = None if gas_measurement: - import threading threading.Thread( target=self._run_gas_sensor, diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index 8e67da86dc3..455d821e669 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -1,12 +1,14 @@ """Reads vehicle status from BMW connected drive portal.""" import logging +from bimmer_connected.account import ConnectedDriveAccount +from bimmer_connected.country_selector import get_region_from_name import voluptuous as vol -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers import discovery -from homeassistant.helpers.event import track_utc_time_change import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import track_utc_time_change import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -118,8 +120,6 @@ class BMWConnectedDriveAccount: self, username: str, password: str, region_str: str, name: str, read_only ) -> None: """Constructor.""" - from bimmer_connected.account import ConnectedDriveAccount - from bimmer_connected.country_selector import get_region_from_name 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 c13de455984..8163ae4eae3 100644 --- a/homeassistant/components/bmw_connected_drive/binary_sensor.py +++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py @@ -1,6 +1,8 @@ """Reads vehicle status from BMW connected drive portal.""" import logging +from bimmer_connected.state import ChargingState, LockState + from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.const import LENGTH_KILOMETERS @@ -141,8 +143,6 @@ class BMWConnectedDriveSensor(BinarySensorDevice): def update(self): """Read new state data from the library.""" - from bimmer_connected.state import LockState - from bimmer_connected.state import ChargingState vehicle_state = self._vehicle.state diff --git a/homeassistant/components/bmw_connected_drive/lock.py b/homeassistant/components/bmw_connected_drive/lock.py index 2055b442dcd..5323e94c1c3 100644 --- a/homeassistant/components/bmw_connected_drive/lock.py +++ b/homeassistant/components/bmw_connected_drive/lock.py @@ -1,6 +1,8 @@ """Support for BMW car locks with BMW ConnectedDrive.""" import logging +from bimmer_connected.state import LockState + from homeassistant.components.lock import LockDevice from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED @@ -87,7 +89,6 @@ class BMWLock(LockDevice): def update(self): """Update state of the lock.""" - from bimmer_connected.state import LockState _LOGGER.debug("%s: updating data for %s", self._vehicle.name, self._attribute) vehicle_state = self._vehicle.state diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index 28a4e853f2c..f919bba6b95 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -1,6 +1,8 @@ """Support for reading vehicle status from BMW connected drive portal.""" import logging +from bimmer_connected.state import ChargingState + from homeassistant.const import ( CONF_UNIT_SYSTEM_IMPERIAL, LENGTH_KILOMETERS, @@ -97,7 +99,6 @@ class BMWConnectedDriveSensor(Entity): @property def icon(self): """Icon to use in the frontend, if any.""" - from bimmer_connected.state import ChargingState vehicle_state = self._vehicle.state charging_state = vehicle_state.charging_status in [ChargingState.CHARGING] diff --git a/homeassistant/components/bom/camera.py b/homeassistant/components/bom/camera.py index f417cf769a4..7460b84f734 100644 --- a/homeassistant/components/bom/camera.py +++ b/homeassistant/components/bom/camera.py @@ -1,4 +1,5 @@ """Provide animated GIF loops of BOM radar imagery.""" +from bomradarloop import BOMRadarLoop import voluptuous as vol from homeassistant.components.camera import PLATFORM_SCHEMA, Camera @@ -119,7 +120,6 @@ class BOMRadarCam(Camera): def __init__(self, name, location, radar_id, delta, frames, outfile): """Initialize the component.""" - from bomradarloop import BOMRadarLoop super().__init__() self._name = name diff --git a/homeassistant/components/broadlink/manifest.json b/homeassistant/components/broadlink/manifest.json index d77c32966b1..c2c128909cc 100644 --- a/homeassistant/components/broadlink/manifest.json +++ b/homeassistant/components/broadlink/manifest.json @@ -3,7 +3,7 @@ "name": "Broadlink", "documentation": "https://www.home-assistant.io/integrations/broadlink", "requirements": [ - "broadlink==0.11.1" + "broadlink==0.12.0" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/broadlink/sensor.py b/homeassistant/components/broadlink/sensor.py index 98988965ca0..6374f35c503 100644 --- a/homeassistant/components/broadlink/sensor.py +++ b/homeassistant/components/broadlink/sensor.py @@ -3,6 +3,8 @@ import binascii import logging from datetime import timedelta +import broadlink + import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -128,7 +130,6 @@ class BroadlinkData: _LOGGER.warning("Failed to connect to device") def _connect(self): - import broadlink self._device = broadlink.a1((self.ip_addr, 80), self.mac_addr, None) self._device.timeout = self.timeout diff --git a/homeassistant/components/broadlink/switch.py b/homeassistant/components/broadlink/switch.py index d60331aaa44..bfb6dc4f42e 100644 --- a/homeassistant/components/broadlink/switch.py +++ b/homeassistant/components/broadlink/switch.py @@ -4,6 +4,8 @@ from datetime import timedelta import logging import socket +import broadlink + import voluptuous as vol from homeassistant.components.switch import ( @@ -91,7 +93,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Broadlink switches.""" - import broadlink devices = config.get(CONF_SWITCHES) slots = config.get("slots", {}) diff --git a/homeassistant/components/brottsplatskartan/sensor.py b/homeassistant/components/brottsplatskartan/sensor.py index 5a3f72c3ef2..d8592f44fff 100644 --- a/homeassistant/components/brottsplatskartan/sensor.py +++ b/homeassistant/components/brottsplatskartan/sensor.py @@ -4,6 +4,8 @@ from datetime import timedelta import logging import uuid +import brottsplatskartan + import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -60,7 +62,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Brottsplatskartan platform.""" - import brottsplatskartan area = config.get(CONF_AREA) latitude = config.get(CONF_LATITUDE, hass.config.latitude) @@ -105,7 +106,6 @@ class BrottsplatskartanSensor(Entity): def update(self): """Update device state.""" - import brottsplatskartan incident_counts = defaultdict(int) incidents = self._brottsplatskartan.get_incidents() diff --git a/homeassistant/components/browser/__init__.py b/homeassistant/components/browser/__init__.py index b163f16a5c4..b7612def701 100644 --- a/homeassistant/components/browser/__init__.py +++ b/homeassistant/components/browser/__init__.py @@ -1,4 +1,5 @@ """Support for launching a web browser on the host machine.""" +import webbrowser import voluptuous as vol ATTR_URL = "url" @@ -18,7 +19,6 @@ SERVICE_BROWSE_URL_SCHEMA = vol.Schema( def setup(hass, config): """Listen for browse_url events.""" - import webbrowser hass.services.register( DOMAIN, diff --git a/homeassistant/components/browser/services.yaml b/homeassistant/components/browser/services.yaml index e69de29bb2d..460def22dc1 100644 --- a/homeassistant/components/browser/services.yaml +++ b/homeassistant/components/browser/services.yaml @@ -0,0 +1,6 @@ +browse_url: + description: Open a URL in the default browser on the host machine of Home Assistant. + fields: + url: + description: The URL to open. + example: "https://www.home-assistant.io" diff --git a/homeassistant/components/brunt/cover.py b/homeassistant/components/brunt/cover.py index af809cc7878..7d4279cf5b2 100644 --- a/homeassistant/components/brunt/cover.py +++ b/homeassistant/components/brunt/cover.py @@ -2,17 +2,18 @@ import logging +from brunt import BruntAPI import voluptuous as vol -from homeassistant.const import ATTR_ATTRIBUTION, CONF_PASSWORD, CONF_USERNAME from homeassistant.components.cover import ( ATTR_POSITION, - CoverDevice, PLATFORM_SCHEMA, SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION, + CoverDevice, ) +from homeassistant.const import ATTR_ATTRIBUTION, CONF_PASSWORD, CONF_USERNAME import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -36,7 +37,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the brunt platform.""" # pylint: disable=no-name-in-module - from brunt import BruntAPI username = config[CONF_USERNAME] password = config[CONF_PASSWORD] diff --git a/homeassistant/components/bt_home_hub_5/device_tracker.py b/homeassistant/components/bt_home_hub_5/device_tracker.py index 0a068a3981f..20ad909c44e 100644 --- a/homeassistant/components/bt_home_hub_5/device_tracker.py +++ b/homeassistant/components/bt_home_hub_5/device_tracker.py @@ -1,6 +1,8 @@ """Support for BT Home Hub 5.""" import logging +import bthomehub5_devicelist + import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -32,7 +34,6 @@ class BTHomeHub5DeviceScanner(DeviceScanner): def __init__(self, config): """Initialise the scanner.""" - import bthomehub5_devicelist _LOGGER.info("Initialising BT Home Hub 5") self.host = config[CONF_HOST] @@ -61,7 +62,6 @@ class BTHomeHub5DeviceScanner(DeviceScanner): def update_info(self): """Ensure the information from the BT Home Hub 5 is up to date.""" - import bthomehub5_devicelist _LOGGER.info("Scanning") diff --git a/homeassistant/components/bt_smarthub/device_tracker.py b/homeassistant/components/bt_smarthub/device_tracker.py index 58f409c2d4b..45b18b963c5 100644 --- a/homeassistant/components/bt_smarthub/device_tracker.py +++ b/homeassistant/components/bt_smarthub/device_tracker.py @@ -1,15 +1,16 @@ """Support for BT Smart Hub (Sometimes referred to as BT Home Hub 6).""" import logging +import btsmarthub_devicelist 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__) @@ -69,12 +70,11 @@ class BTSmartHubScanner(DeviceScanner): _LOGGER.warning("Error scanning devices") return - clients = [client for client in data.values()] + clients = list(data.values()) self.last_results = clients def get_bt_smarthub_data(self): """Retrieve data from BT Smart Hub and return parsed result.""" - import btsmarthub_devicelist # Request data from bt smarthub into a list of dicts. data = btsmarthub_devicelist.get_devicelist( diff --git a/homeassistant/components/buienradar/const.py b/homeassistant/components/buienradar/const.py new file mode 100644 index 00000000000..b91d2497d77 --- /dev/null +++ b/homeassistant/components/buienradar/const.py @@ -0,0 +1,7 @@ +"""Constants for buienradar component.""" +DEFAULT_TIMEFRAME = 60 + +"""Schedule next call after (minutes).""" +SCHEDULE_OK = 10 +"""When an error occurred, new call after (minutes).""" +SCHEDULE_NOK = 2 diff --git a/homeassistant/components/buienradar/sensor.py b/homeassistant/components/buienradar/sensor.py index ef65db74f16..5fe97b6fb38 100644 --- a/homeassistant/components/buienradar/sensor.py +++ b/homeassistant/components/buienradar/sensor.py @@ -1,10 +1,23 @@ """Support for Buienradar.nl weather service.""" -import asyncio -from datetime import datetime, timedelta import logging -import aiohttp -import async_timeout +from buienradar.constants import ( + ATTRIBUTION, + CONDCODE, + CONDITION, + DETAILED, + EXACT, + EXACTNL, + FORECAST, + IMAGE, + MEASURED, + PRECIPITATION_FORECAST, + STATIONNAME, + TIMEFRAME, + VISIBILITY, + WINDGUST, + WINDSPEED, +) import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -16,12 +29,15 @@ from homeassistant.const import ( CONF_NAME, 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_point_in_utc_time from homeassistant.util import dt as dt_util + +from .const import DEFAULT_TIMEFRAME +from .util import BrData + + _LOGGER = logging.getLogger(__name__) MEASURED_LABEL = "Measured" @@ -183,7 +199,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Create the buienradar sensor.""" - from .weather import DEFAULT_TIMEFRAME latitude = config.get(CONF_LATITUDE, hass.config.latitude) longitude = config.get(CONF_LONGITUDE, hass.config.longitude) @@ -216,7 +231,6 @@ class BrSensor(Entity): def __init__(self, sensor_type, client_name, coordinates): """Initialize the sensor.""" - from buienradar.constants import PRECIPITATION_FORECAST, CONDITION self.client_name = client_name self._name = SENSOR_TYPES[sensor_type][0] @@ -247,23 +261,6 @@ class BrSensor(Entity): def load_data(self, data): """Load the sensor with relevant data.""" # Find sensor - from buienradar.constants import ( - ATTRIBUTION, - CONDITION, - CONDCODE, - DETAILED, - EXACT, - EXACTNL, - FORECAST, - IMAGE, - MEASURED, - PRECIPITATION_FORECAST, - STATIONNAME, - TIMEFRAME, - VISIBILITY, - WINDGUST, - WINDSPEED, - ) # Check if we have a new measurement, # otherwise we do not have to update the sensor @@ -421,7 +418,6 @@ class BrSensor(Entity): @property def device_state_attributes(self): """Return the state attributes.""" - from buienradar.constants import PRECIPITATION_FORECAST if self.type.startswith(PRECIPITATION_FORECAST): result = {ATTR_ATTRIBUTION: self._attribution} @@ -455,208 +451,3 @@ class BrSensor(Entity): def force_update(self): """Return true for continuous sensors, false for discrete sensors.""" return self._force_update - - -class BrData: - """Get the latest data and updates the states.""" - - def __init__(self, hass, coordinates, timeframe, devices): - """Initialize the data object.""" - self.devices = devices - self.data = {} - self.hass = hass - self.coordinates = coordinates - self.timeframe = timeframe - - async def update_devices(self): - """Update all devices/sensors.""" - if self.devices: - tasks = [] - # Update all devices - for dev in self.devices: - if dev.load_data(self.data): - tasks.append(dev.async_update_ha_state()) - - if tasks: - await asyncio.wait(tasks) - - async def schedule_update(self, minute=1): - """Schedule an update after minute minutes.""" - _LOGGER.debug("Scheduling next update in %s minutes.", minute) - nxt = dt_util.utcnow() + timedelta(minutes=minute) - async_track_point_in_utc_time(self.hass, self.async_update, nxt) - - async def get_data(self, url): - """Load data from specified url.""" - from buienradar.constants import CONTENT, MESSAGE, STATUS_CODE, SUCCESS - - _LOGGER.debug("Calling url: %s...", url) - result = {SUCCESS: False, MESSAGE: None} - resp = None - try: - websession = async_get_clientsession(self.hass) - with async_timeout.timeout(10): - resp = await websession.get(url) - - result[STATUS_CODE] = resp.status - result[CONTENT] = await resp.text() - if resp.status == 200: - result[SUCCESS] = True - else: - result[MESSAGE] = "Got http statuscode: %d" % (resp.status) - - return result - except (asyncio.TimeoutError, aiohttp.ClientError) as err: - result[MESSAGE] = "%s" % err - return result - finally: - if resp is not None: - await resp.release() - - async def async_update(self, *_): - """Update the data from buienradar.""" - from buienradar.constants import CONTENT, DATA, MESSAGE, STATUS_CODE, SUCCESS - from buienradar.buienradar import parse_data - from buienradar.urls import JSON_FEED_URL, json_precipitation_forecast_url - - content = await self.get_data(JSON_FEED_URL) - - if content.get(SUCCESS) is not True: - # unable to get the data - _LOGGER.warning( - "Unable to retrieve json data from Buienradar." - "(Msg: %s, status: %s,)", - content.get(MESSAGE), - content.get(STATUS_CODE), - ) - # schedule new call - await self.schedule_update(SCHEDULE_NOK) - return - - # rounding coordinates prevents unnecessary redirects/calls - lat = self.coordinates[CONF_LATITUDE] - lon = self.coordinates[CONF_LONGITUDE] - rainurl = json_precipitation_forecast_url(lat, lon) - raincontent = await self.get_data(rainurl) - - if raincontent.get(SUCCESS) is not True: - # unable to get the data - _LOGGER.warning( - "Unable to retrieve raindata from Buienradar." "(Msg: %s, status: %s,)", - raincontent.get(MESSAGE), - raincontent.get(STATUS_CODE), - ) - # schedule new call - await self.schedule_update(SCHEDULE_NOK) - return - - result = parse_data( - content.get(CONTENT), - raincontent.get(CONTENT), - self.coordinates[CONF_LATITUDE], - self.coordinates[CONF_LONGITUDE], - self.timeframe, - False, - ) - - _LOGGER.debug("Buienradar parsed data: %s", result) - if result.get(SUCCESS) is not True: - if int(datetime.now().strftime("%H")) > 0: - _LOGGER.warning( - "Unable to parse data from Buienradar." "(Msg: %s)", - result.get(MESSAGE), - ) - await self.schedule_update(SCHEDULE_NOK) - return - - self.data = result.get(DATA) - await self.update_devices() - await self.schedule_update(SCHEDULE_OK) - - @property - def attribution(self): - """Return the attribution.""" - from buienradar.constants import ATTRIBUTION - - return self.data.get(ATTRIBUTION) - - @property - def stationname(self): - """Return the name of the selected weatherstation.""" - from buienradar.constants import STATIONNAME - - return self.data.get(STATIONNAME) - - @property - def condition(self): - """Return the condition.""" - from buienradar.constants import CONDITION - - return self.data.get(CONDITION) - - @property - def temperature(self): - """Return the temperature, or None.""" - from buienradar.constants import TEMPERATURE - - try: - return float(self.data.get(TEMPERATURE)) - except (ValueError, TypeError): - return None - - @property - def pressure(self): - """Return the pressure, or None.""" - from buienradar.constants import PRESSURE - - try: - return float(self.data.get(PRESSURE)) - except (ValueError, TypeError): - return None - - @property - def humidity(self): - """Return the humidity, or None.""" - from buienradar.constants import HUMIDITY - - try: - return int(self.data.get(HUMIDITY)) - except (ValueError, TypeError): - return None - - @property - def visibility(self): - """Return the visibility, or None.""" - from buienradar.constants import VISIBILITY - - try: - return int(self.data.get(VISIBILITY)) - except (ValueError, TypeError): - return None - - @property - def wind_speed(self): - """Return the windspeed, or None.""" - from buienradar.constants import WINDSPEED - - try: - return float(self.data.get(WINDSPEED)) - except (ValueError, TypeError): - return None - - @property - def wind_bearing(self): - """Return the wind bearing, or None.""" - from buienradar.constants import WINDAZIMUTH - - try: - return int(self.data.get(WINDAZIMUTH)) - except (ValueError, TypeError): - return None - - @property - def forecast(self): - """Return the forecast data.""" - from buienradar.constants import FORECAST - - return self.data.get(FORECAST) diff --git a/homeassistant/components/buienradar/util.py b/homeassistant/components/buienradar/util.py new file mode 100644 index 00000000000..579b3418271 --- /dev/null +++ b/homeassistant/components/buienradar/util.py @@ -0,0 +1,228 @@ +"""Shared utilities for different supported platforms.""" +import asyncio +from datetime import datetime, timedelta +import logging + +import aiohttp +import async_timeout + +from buienradar.buienradar import parse_data +from buienradar.constants import ( + ATTRIBUTION, + CONDITION, + CONTENT, + DATA, + FORECAST, + HUMIDITY, + MESSAGE, + PRESSURE, + STATIONNAME, + STATUS_CODE, + SUCCESS, + TEMPERATURE, + VISIBILITY, + WINDAZIMUTH, + WINDSPEED, +) +from buienradar.urls import JSON_FEED_URL, json_precipitation_forecast_url + +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.event import async_track_point_in_utc_time +from homeassistant.util import dt as dt_util + + +from .const import SCHEDULE_OK, SCHEDULE_NOK + + +_LOGGER = logging.getLogger(__name__) + + +class BrData: + """Get the latest data and updates the states.""" + + def __init__(self, hass, coordinates, timeframe, devices): + """Initialize the data object.""" + self.devices = devices + self.data = {} + self.hass = hass + self.coordinates = coordinates + self.timeframe = timeframe + + async def update_devices(self): + """Update all devices/sensors.""" + if self.devices: + tasks = [] + # Update all devices + for dev in self.devices: + if dev.load_data(self.data): + tasks.append(dev.async_update_ha_state()) + + if tasks: + await asyncio.wait(tasks) + + async def schedule_update(self, minute=1): + """Schedule an update after minute minutes.""" + _LOGGER.debug("Scheduling next update in %s minutes.", minute) + nxt = dt_util.utcnow() + timedelta(minutes=minute) + async_track_point_in_utc_time(self.hass, self.async_update, nxt) + + async def get_data(self, url): + """Load data from specified url.""" + _LOGGER.debug("Calling url: %s...", url) + result = {SUCCESS: False, MESSAGE: None} + resp = None + try: + websession = async_get_clientsession(self.hass) + with async_timeout.timeout(10): + resp = await websession.get(url) + + result[STATUS_CODE] = resp.status + result[CONTENT] = await resp.text() + if resp.status == 200: + result[SUCCESS] = True + else: + result[MESSAGE] = "Got http statuscode: %d" % (resp.status) + + return result + except (asyncio.TimeoutError, aiohttp.ClientError) as err: + result[MESSAGE] = "%s" % err + return result + finally: + if resp is not None: + await resp.release() + + async def async_update(self, *_): + """Update the data from buienradar.""" + + content = await self.get_data(JSON_FEED_URL) + + if content.get(SUCCESS) is not True: + # unable to get the data + _LOGGER.warning( + "Unable to retrieve json data from Buienradar." + "(Msg: %s, status: %s,)", + content.get(MESSAGE), + content.get(STATUS_CODE), + ) + # schedule new call + await self.schedule_update(SCHEDULE_NOK) + return + + # rounding coordinates prevents unnecessary redirects/calls + lat = self.coordinates[CONF_LATITUDE] + lon = self.coordinates[CONF_LONGITUDE] + rainurl = json_precipitation_forecast_url(lat, lon) + raincontent = await self.get_data(rainurl) + + if raincontent.get(SUCCESS) is not True: + # unable to get the data + _LOGGER.warning( + "Unable to retrieve raindata from Buienradar." "(Msg: %s, status: %s,)", + raincontent.get(MESSAGE), + raincontent.get(STATUS_CODE), + ) + # schedule new call + await self.schedule_update(SCHEDULE_NOK) + return + + result = parse_data( + content.get(CONTENT), + raincontent.get(CONTENT), + self.coordinates[CONF_LATITUDE], + self.coordinates[CONF_LONGITUDE], + self.timeframe, + False, + ) + + _LOGGER.debug("Buienradar parsed data: %s", result) + if result.get(SUCCESS) is not True: + if int(datetime.now().strftime("%H")) > 0: + _LOGGER.warning( + "Unable to parse data from Buienradar." "(Msg: %s)", + result.get(MESSAGE), + ) + await self.schedule_update(SCHEDULE_NOK) + return + + self.data = result.get(DATA) + await self.update_devices() + await self.schedule_update(SCHEDULE_OK) + + @property + def attribution(self): + """Return the attribution.""" + + return self.data.get(ATTRIBUTION) + + @property + def stationname(self): + """Return the name of the selected weatherstation.""" + + return self.data.get(STATIONNAME) + + @property + def condition(self): + """Return the condition.""" + + return self.data.get(CONDITION) + + @property + def temperature(self): + """Return the temperature, or None.""" + + try: + return float(self.data.get(TEMPERATURE)) + except (ValueError, TypeError): + return None + + @property + def pressure(self): + """Return the pressure, or None.""" + + try: + return float(self.data.get(PRESSURE)) + except (ValueError, TypeError): + return None + + @property + def humidity(self): + """Return the humidity, or None.""" + + try: + return int(self.data.get(HUMIDITY)) + except (ValueError, TypeError): + return None + + @property + def visibility(self): + """Return the visibility, or None.""" + + try: + return int(self.data.get(VISIBILITY)) + except (ValueError, TypeError): + return None + + @property + def wind_speed(self): + """Return the windspeed, or None.""" + + try: + return float(self.data.get(WINDSPEED)) + except (ValueError, TypeError): + return None + + @property + def wind_bearing(self): + """Return the wind bearing, or None.""" + + try: + return int(self.data.get(WINDAZIMUTH)) + except (ValueError, TypeError): + return None + + @property + def forecast(self): + """Return the forecast data.""" + + return self.data.get(FORECAST) diff --git a/homeassistant/components/buienradar/weather.py b/homeassistant/components/buienradar/weather.py index d8ae448c981..c95e57807c4 100644 --- a/homeassistant/components/buienradar/weather.py +++ b/homeassistant/components/buienradar/weather.py @@ -1,30 +1,40 @@ """Support for Buienradar.nl weather service.""" import logging +from buienradar.constants import ( + CONDCODE, + CONDITION, + DATETIME, + MAX_TEMP, + MIN_TEMP, + RAIN, + WINDAZIMUTH, + WINDSPEED, +) import voluptuous as vol from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, + ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, - PLATFORM_SCHEMA, - WeatherEntity, - ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_WIND_BEARING, ATTR_FORECAST_WIND_SPEED, + PLATFORM_SCHEMA, + WeatherEntity, ) from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_CELSIUS from homeassistant.helpers import config_validation as cv # Reuse data and API logic from the sensor implementation -from .sensor import BrData +from .util import BrData +from .const import DEFAULT_TIMEFRAME _LOGGER = logging.getLogger(__name__) DATA_CONDITION = "buienradar_condition" -DEFAULT_TIMEFRAME = 60 CONF_FORECAST = "forecast" @@ -110,7 +120,6 @@ class BrWeather(WeatherEntity): @property def condition(self): """Return the current condition.""" - from buienradar.constants import CONDCODE if self._data and self._data.condition: ccode = self._data.condition.get(CONDCODE) @@ -161,16 +170,6 @@ class BrWeather(WeatherEntity): @property def forecast(self): """Return the forecast array.""" - from buienradar.constants import ( - CONDITION, - CONDCODE, - RAIN, - DATETIME, - MIN_TEMP, - MAX_TEMP, - WINDAZIMUTH, - WINDSPEED, - ) if not self._forecast: return None diff --git a/homeassistant/components/caldav/calendar.py b/homeassistant/components/caldav/calendar.py index 6251679b225..ad9dac1f727 100644 --- a/homeassistant/components/caldav/calendar.py +++ b/homeassistant/components/caldav/calendar.py @@ -4,6 +4,7 @@ from datetime import datetime, timedelta import logging import re +import caldav import voluptuous as vol from homeassistant.components.calendar import ( @@ -62,8 +63,6 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15) def setup_platform(hass, config, add_entities, disc_info=None): """Set up the WebDav Calendar platform.""" - import caldav - url = config[CONF_URL] username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) @@ -279,6 +278,10 @@ class WebDavCalendarData: def to_datetime(obj): """Return a datetime.""" if isinstance(obj, datetime): + if obj.tzinfo is None: + # floating value, not bound to any time zone in particular + # represent same time regardless of which time zone is currently being observed + return obj.replace(tzinfo=dt.DEFAULT_TIME_ZONE) return obj return dt.as_local(dt.dt.datetime.combine(obj, dt.dt.time.min)) diff --git a/homeassistant/components/canary/__init__.py b/homeassistant/components/canary/__init__.py index f23b6ad46c9..a8a45f5b946 100644 --- a/homeassistant/components/canary/__init__.py +++ b/homeassistant/components/canary/__init__.py @@ -1,13 +1,14 @@ """Support for Canary devices.""" -import logging from datetime import timedelta +import logging -import voluptuous as vol +from canary.api import Api from requests import ConnectTimeout, HTTPError +import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT +from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -67,7 +68,6 @@ class CanaryData: def __init__(self, username, password, timeout): """Init the Canary data object.""" - from canary.api import Api self._api = Api(username, password, timeout) diff --git a/homeassistant/components/canary/alarm_control_panel.py b/homeassistant/components/canary/alarm_control_panel.py index 42b5048bc1d..856ecb9f3a2 100644 --- a/homeassistant/components/canary/alarm_control_panel.py +++ b/homeassistant/components/canary/alarm_control_panel.py @@ -1,6 +1,8 @@ """Support for Canary alarm.""" 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.const import ( STATE_ALARM_ARMED_AWAY, @@ -42,11 +44,6 @@ class CanaryAlarm(AlarmControlPanel): @property def state(self): """Return the state of the device.""" - from canary.api import ( - LOCATION_MODE_AWAY, - LOCATION_MODE_HOME, - LOCATION_MODE_NIGHT, - ) location = self._data.get_location(self._location_id) @@ -75,18 +72,15 @@ class CanaryAlarm(AlarmControlPanel): def alarm_arm_home(self, code=None): """Send arm home command.""" - from canary.api import LOCATION_MODE_HOME self._data.set_location_mode(self._location_id, LOCATION_MODE_HOME) def alarm_arm_away(self, code=None): """Send arm away command.""" - from canary.api import LOCATION_MODE_AWAY self._data.set_location_mode(self._location_id, LOCATION_MODE_AWAY) def alarm_arm_night(self, code=None): """Send arm night command.""" - from canary.api import LOCATION_MODE_NIGHT self._data.set_location_mode(self._location_id, LOCATION_MODE_NIGHT) diff --git a/homeassistant/components/canary/camera.py b/homeassistant/components/canary/camera.py index 8a6d27b8916..7ed1e62ab8a 100644 --- a/homeassistant/components/canary/camera.py +++ b/homeassistant/components/canary/camera.py @@ -3,6 +3,8 @@ 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 @@ -81,8 +83,6 @@ class CanaryCamera(Camera): """Return a still image response from the camera.""" self.renew_live_stream_session() - from haffmpeg.tools import ImageFrame, IMAGE_JPEG - ffmpeg = ImageFrame(self._ffmpeg.binary, loop=self.hass.loop) image = await asyncio.shield( ffmpeg.get_image( @@ -98,8 +98,6 @@ class CanaryCamera(Camera): if self._live_stream_session is None: return - from haffmpeg.camera import CameraMjpeg - stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop) await stream.open_camera( self._live_stream_session.live_stream_url, extra_cmd=self._ffmpeg_arguments diff --git a/homeassistant/components/cast/.translations/ko.json b/homeassistant/components/cast/.translations/ko.json index 71dee3afec5..1374372aa24 100644 --- a/homeassistant/components/cast/.translations/ko.json +++ b/homeassistant/components/cast/.translations/ko.json @@ -1,15 +1,15 @@ { "config": { "abort": { - "no_devices_found": "\uad6c\uae00 \uce90\uc2a4\ud2b8 \uae30\uae30\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", - "single_instance_allowed": "\ud558\ub098\uc758 \uad6c\uae00 \uce90\uc2a4\ud2b8\ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." + "no_devices_found": "Google \uce90\uc2a4\ud2b8 \uae30\uae30\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", + "single_instance_allowed": "\ud558\ub098\uc758 Google \uce90\uc2a4\ud2b8\ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." }, "step": { "confirm": { - "description": "\uad6c\uae00 \uce90\uc2a4\ud2b8\ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", - "title": "\uad6c\uae00 \uce90\uc2a4\ud2b8" + "description": "Google \uce90\uc2a4\ud2b8\ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "Google \uce90\uc2a4\ud2b8" } }, - "title": "\uad6c\uae00 \uce90\uc2a4\ud2b8" + "title": "Google \uce90\uc2a4\ud2b8" } } \ No newline at end of file diff --git a/homeassistant/components/cast/config_flow.py b/homeassistant/components/cast/config_flow.py index c3c21944d02..5c2b6dca932 100644 --- a/homeassistant/components/cast/config_flow.py +++ b/homeassistant/components/cast/config_flow.py @@ -1,12 +1,14 @@ """Config flow for Cast.""" -from homeassistant.helpers import config_entry_flow +from pychromecast.discovery import discover_chromecasts + from homeassistant import config_entries +from homeassistant.helpers import config_entry_flow + from .const import DOMAIN async def _async_has_devices(hass): """Return if there are devices that can be discovered.""" - from pychromecast.discovery import discover_chromecasts return await hass.async_add_executor_job(discover_chromecasts) diff --git a/homeassistant/components/cert_expiry/.translations/ca.json b/homeassistant/components/cert_expiry/.translations/ca.json index 25c0b26fafc..f1df9a06be1 100644 --- a/homeassistant/components/cert_expiry/.translations/ca.json +++ b/homeassistant/components/cert_expiry/.translations/ca.json @@ -4,15 +4,17 @@ "host_port_exists": "Aquesta combinaci\u00f3 d'amfitri\u00f3 i port ja est\u00e0 configurada" }, "error": { + "certificate_error": "El certificat no ha pogut ser validat", "certificate_fetch_failed": "No s'ha pogut obtenir el certificat des d'aquesta combinaci\u00f3 d'amfitri\u00f3 i port", "connection_timeout": "S'ha acabat el temps d'espera durant la connexi\u00f3 amb l'amfitri\u00f3.", "host_port_exists": "Aquesta combinaci\u00f3 d'amfitri\u00f3 i port ja est\u00e0 configurada", - "resolve_failed": "No s'ha pogut resoldre l'amfitri\u00f3" + "resolve_failed": "No s'ha pogut resoldre l'amfitri\u00f3", + "wrong_host": "El certificat no coincideix amb el nom de l'amfitri\u00f3" }, "step": { "user": { "data": { - "host": "Nom d'amfitri\u00f3 del certificat", + "host": "Nom de l'amfitri\u00f3 del certificat", "name": "Nom del certificat", "port": "Port del certificat" }, diff --git a/homeassistant/components/cert_expiry/.translations/da.json b/homeassistant/components/cert_expiry/.translations/da.json index 667ab5fa4e3..c95a56320c9 100644 --- a/homeassistant/components/cert_expiry/.translations/da.json +++ b/homeassistant/components/cert_expiry/.translations/da.json @@ -4,10 +4,12 @@ "host_port_exists": "Denne v\u00e6rt- og portkombination er allerede konfigureret" }, "error": { + "certificate_error": "Certifikatet kunne ikke valideres", "certificate_fetch_failed": "Kan ikke hente certifikat fra denne v\u00e6rt- og portkombination", "connection_timeout": "Timeout ved tilslutning til denne v\u00e6rt", "host_port_exists": "Denne v\u00e6rt- og portkombination er allerede konfigureret", - "resolve_failed": "V\u00e6rten kunne ikke findes" + "resolve_failed": "V\u00e6rten kunne ikke findes", + "wrong_host": "Certifikatet stemmer ikke overens med v\u00e6rtsnavnet" }, "step": { "user": { diff --git a/homeassistant/components/cert_expiry/.translations/en.json b/homeassistant/components/cert_expiry/.translations/en.json index 873dfee9a92..19e237a6d05 100644 --- a/homeassistant/components/cert_expiry/.translations/en.json +++ b/homeassistant/components/cert_expiry/.translations/en.json @@ -4,10 +4,12 @@ "host_port_exists": "This host and port combination is already configured" }, "error": { + "certificate_error": "Certificate could not be validated", "certificate_fetch_failed": "Can not fetch certificate from this host and port combination", "connection_timeout": "Timeout when connecting to this host", "host_port_exists": "This host and port combination is already configured", - "resolve_failed": "This host can not be resolved" + "resolve_failed": "This host can not be resolved", + "wrong_host": "Certificate does not match hostname" }, "step": { "user": { diff --git a/homeassistant/components/cert_expiry/.translations/fr.json b/homeassistant/components/cert_expiry/.translations/fr.json index a3536902c76..9e7df5564a2 100644 --- a/homeassistant/components/cert_expiry/.translations/fr.json +++ b/homeassistant/components/cert_expiry/.translations/fr.json @@ -4,10 +4,12 @@ "host_port_exists": "Cette combinaison h\u00f4te / port est d\u00e9j\u00e0 configur\u00e9e" }, "error": { + "certificate_error": "Le certificat n'a pas pu \u00eatre valid\u00e9", "certificate_fetch_failed": "Impossible de r\u00e9cup\u00e9rer le certificat de cette combinaison h\u00f4te / port", "connection_timeout": "D\u00e9lai d'attente lors de la connexion \u00e0 cet h\u00f4te", "host_port_exists": "Cette combinaison h\u00f4te / port est d\u00e9j\u00e0 configur\u00e9e", - "resolve_failed": "Cet h\u00f4te ne peut pas \u00eatre r\u00e9solu" + "resolve_failed": "Cet h\u00f4te ne peut pas \u00eatre r\u00e9solu", + "wrong_host": "Le certificat ne correspond pas au nom d'h\u00f4te" }, "step": { "user": { diff --git a/homeassistant/components/cert_expiry/.translations/nl.json b/homeassistant/components/cert_expiry/.translations/nl.json index d2fe3c76e85..0544c8c02c1 100644 --- a/homeassistant/components/cert_expiry/.translations/nl.json +++ b/homeassistant/components/cert_expiry/.translations/nl.json @@ -4,10 +4,12 @@ "host_port_exists": "Deze combinatie van host en poort is al geconfigureerd" }, "error": { + "certificate_error": "Certificaat kon niet worden gevalideerd", "certificate_fetch_failed": "Kan certificaat niet ophalen van deze combinatie van host en poort", - "connection_timeout": "Timeout bij verbinding maken met deze host", + "connection_timeout": "Time-out bij verbinding maken met deze host", "host_port_exists": "Deze combinatie van host en poort is al geconfigureerd", - "resolve_failed": "Deze host kon niet gevonden worden" + "resolve_failed": "Deze host kon niet gevonden worden", + "wrong_host": "Certificaat komt niet overeen met hostnaam" }, "step": { "user": { diff --git a/homeassistant/components/cert_expiry/.translations/no.json b/homeassistant/components/cert_expiry/.translations/no.json index 73e899106c1..fc2e98b725d 100644 --- a/homeassistant/components/cert_expiry/.translations/no.json +++ b/homeassistant/components/cert_expiry/.translations/no.json @@ -4,10 +4,12 @@ "host_port_exists": "Denne verts- og portkombinasjonen er allerede konfigurert" }, "error": { + "certificate_error": "Sertifikatet kunne ikke valideres", "certificate_fetch_failed": "Kan ikke hente sertifikat fra denne verts- og portkombinasjonen", "connection_timeout": "Tidsavbrudd n\u00e5r du kobler til denne verten", "host_port_exists": "Denne verts- og portkombinasjonen er allerede konfigurert", - "resolve_failed": "Denne verten kan ikke l\u00f8ses" + "resolve_failed": "Denne verten kan ikke l\u00f8ses", + "wrong_host": "Sertifikatet samsvarer ikke med vertsnavn" }, "step": { "user": { diff --git a/homeassistant/components/cert_expiry/.translations/ru.json b/homeassistant/components/cert_expiry/.translations/ru.json index d962c793121..8c0f230382a 100644 --- a/homeassistant/components/cert_expiry/.translations/ru.json +++ b/homeassistant/components/cert_expiry/.translations/ru.json @@ -4,19 +4,21 @@ "host_port_exists": "\u042d\u0442\u0430 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430." }, "error": { - "certificate_fetch_failed": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u0441 \u044d\u0442\u043e\u0439 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u0438 \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430", - "connection_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0445\u043e\u0441\u0442\u0443", + "certificate_error": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442.", + "certificate_fetch_failed": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u0441 \u044d\u0442\u043e\u0439 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u0438 \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430.", + "connection_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0445\u043e\u0441\u0442\u0443.", "host_port_exists": "\u042d\u0442\u0430 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430.", - "resolve_failed": "\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u0442\u044c \u0445\u043e\u0441\u0442" + "resolve_failed": "\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u0442\u044c \u0445\u043e\u0441\u0442.", + "wrong_host": "\u0421\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u043d\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u043c\u0443 \u0438\u043c\u0435\u043d\u0438." }, "step": { "user": { "data": { - "host": "\u0418\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430", - "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430", - "port": "\u041f\u043e\u0440\u0442 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430" + "host": "\u0414\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "port": "\u041f\u043e\u0440\u0442" }, - "title": "C\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u0434\u043b\u044f \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f" + "title": "\u0421\u0440\u043e\u043a \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430" } }, "title": "\u0421\u0440\u043e\u043a \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430" diff --git a/homeassistant/components/cert_expiry/__init__.py b/homeassistant/components/cert_expiry/__init__.py index 7c7efea7333..28a79a3e505 100644 --- a/homeassistant/components/cert_expiry/__init__.py +++ b/homeassistant/components/cert_expiry/__init__.py @@ -15,3 +15,8 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): hass.config_entries.async_forward_entry_setup(entry, "sensor") ) return True + + +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + return await hass.config_entries.async_forward_entry_unload(entry, "sensor") diff --git a/homeassistant/components/cert_expiry/config_flow.py b/homeassistant/components/cert_expiry/config_flow.py index d73762ce882..78450d247b9 100644 --- a/homeassistant/components/cert_expiry/config_flow.py +++ b/homeassistant/components/cert_expiry/config_flow.py @@ -1,15 +1,18 @@ """Config flow for the Cert Expiry platform.""" +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.core import HomeAssistant, callback -from homeassistant.util import slugify from .const import DOMAIN, DEFAULT_PORT, DEFAULT_NAME from .helper import get_cert +_LOGGER = logging.getLogger(__name__) + @callback def certexpiry_entries(hass: HomeAssistant): @@ -40,17 +43,28 @@ class CertexpiryConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def _test_connection(self, user_input=None): """Test connection to the server and try to get the certtificate.""" + host = user_input[CONF_HOST] try: await self.hass.async_add_executor_job( - get_cert, user_input[CONF_HOST], user_input.get(CONF_PORT, DEFAULT_PORT) + get_cert, host, user_input.get(CONF_PORT, DEFAULT_PORT) ) return True except socket.gaierror: + _LOGGER.error("Host cannot be resolved: %s", host) self._errors[CONF_HOST] = "resolve_failed" except socket.timeout: + _LOGGER.error("Timed out connecting to %s", host) self._errors[CONF_HOST] = "connection_timeout" - except OSError: - self._errors[CONF_HOST] = "certificate_fetch_failed" + except ssl.CertificateError as err: + if "doesn't match" in err.args[0]: + _LOGGER.error("Certificate does not match host: %s", host) + self._errors[CONF_HOST] = "wrong_host" + else: + _LOGGER.error("Certificate could not be validated: %s", host) + self._errors[CONF_HOST] = "certificate_error" + except ssl.SSLError: + _LOGGER.error("Certificate could not be validated: %s", host) + self._errors[CONF_HOST] = "certificate_error" return False async def async_step_user(self, user_input=None): @@ -62,11 +76,12 @@ class CertexpiryConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._errors[CONF_HOST] = "host_port_exists" else: if await self._test_connection(user_input): - host = user_input[CONF_HOST] - name = slugify(user_input.get(CONF_NAME, DEFAULT_NAME)) - prt = user_input.get(CONF_PORT, DEFAULT_PORT) return self.async_create_entry( - title=name, data={CONF_HOST: host, CONF_PORT: prt} + title=user_input.get(CONF_NAME, DEFAULT_NAME), + data={ + CONF_HOST: user_input[CONF_HOST], + CONF_PORT: user_input.get(CONF_PORT, DEFAULT_PORT), + }, ) else: user_input = {} diff --git a/homeassistant/components/cert_expiry/helper.py b/homeassistant/components/cert_expiry/helper.py index 9c10887293a..cd49588ec89 100644 --- a/homeassistant/components/cert_expiry/helper.py +++ b/homeassistant/components/cert_expiry/helper.py @@ -11,5 +11,6 @@ def get_cert(host, port): address = (host, port) with socket.create_connection(address, timeout=TIMEOUT) as sock: with ctx.wrap_socket(sock, server_hostname=address[0]) as ssock: - cert = ssock.getpeercert() + # pylint disable: https://github.com/PyCQA/pylint/issues/3166 + cert = ssock.getpeercert() # pylint: disable=no-member return cert diff --git a/homeassistant/components/cert_expiry/manifest.json b/homeassistant/components/cert_expiry/manifest.json index 97f72f2ad11..48816809bbd 100644 --- a/homeassistant/components/cert_expiry/manifest.json +++ b/homeassistant/components/cert_expiry/manifest.json @@ -5,5 +5,8 @@ "requirements": [], "config_flow": true, "dependencies": [], - "codeowners": ["@cereal2nd"] + "codeowners": [ + "@Cereal2nd", + "@jjlawren" + ] } diff --git a/homeassistant/components/cert_expiry/sensor.py b/homeassistant/components/cert_expiry/sensor.py index a5b879e5661..3022c7bd42b 100644 --- a/homeassistant/components/cert_expiry/sensor.py +++ b/homeassistant/components/cert_expiry/sensor.py @@ -70,12 +70,18 @@ class SSLCertificate(Entity): self._name = sensor_name self._state = None self._available = False + self._valid = False @property def name(self): """Return the name of the sensor.""" return self._name + @property + def unique_id(self): + """Return a unique id for the sensor.""" + return f"{self.server_name}:{self.server_port}" + @property def unit_of_measurement(self): """Return the unit this state is expressed in.""" @@ -117,16 +123,17 @@ class SSLCertificate(Entity): except socket.gaierror: _LOGGER.error("Cannot resolve hostname: %s", self.server_name) self._available = False + self._valid = False return except socket.timeout: _LOGGER.error("Connection timeout with server: %s", self.server_name) self._available = False + self._valid = False return - except OSError: - _LOGGER.error( - "Cannot fetch certificate from %s", self.server_name, exc_info=1 - ) - self._available = False + except (ssl.CertificateError, ssl.SSLError): + self._available = True + self._state = 0 + self._valid = False return ts_seconds = ssl.cert_time_to_seconds(cert["notAfter"]) @@ -134,3 +141,11 @@ class SSLCertificate(Entity): expiry = timestamp - datetime.today() self._available = True self._state = expiry.days + self._valid = True + + @property + def device_state_attributes(self): + """Return additional sensor state attributes.""" + attr = {"is_valid": self._valid} + + return attr diff --git a/homeassistant/components/cert_expiry/strings.json b/homeassistant/components/cert_expiry/strings.json index 3e2fea2342e..e5e670d214f 100644 --- a/homeassistant/components/cert_expiry/strings.json +++ b/homeassistant/components/cert_expiry/strings.json @@ -15,7 +15,8 @@ "host_port_exists": "This host and port combination is already configured", "resolve_failed": "This host can not be resolved", "connection_timeout": "Timeout when connecting to this host", - "certificate_fetch_failed": "Can not fetch certificate from this host and port combination" + "certificate_error": "Certificate could not be validated", + "wrong_host": "Certificate does not match hostname" }, "abort": { "host_port_exists": "This host and port combination is already configured" diff --git a/homeassistant/components/channels/media_player.py b/homeassistant/components/channels/media_player.py index 6c3e18cdb05..6d978a5451e 100644 --- a/homeassistant/components/channels/media_player.py +++ b/homeassistant/components/channels/media_player.py @@ -1,9 +1,10 @@ """Support for interfacing with an instance of getchannels.com.""" import logging +from pychannels import Channels 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_CHANNEL, @@ -124,7 +125,6 @@ class ChannelsPlayer(MediaPlayerDevice): def __init__(self, name, host, port): """Initialize the Channels app.""" - from pychannels import Channels self._name = name self._host = host diff --git a/homeassistant/components/cisco_ios/device_tracker.py b/homeassistant/components/cisco_ios/device_tracker.py index b442b24feb4..5a42ef1c8b8 100644 --- a/homeassistant/components/cisco_ios/device_tracker.py +++ b/homeassistant/components/cisco_ios/device_tracker.py @@ -1,15 +1,17 @@ """Support for Cisco IOS Routers.""" import logging +import re +from pexpect import 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__) @@ -100,8 +102,6 @@ class CiscoDeviceScanner(DeviceScanner): def _get_arp_data(self): """Open connection to the router and get arp entries.""" - from pexpect import pxssh - import re try: cisco_ssh = pxssh.pxssh() diff --git a/homeassistant/components/cisco_mobility_express/device_tracker.py b/homeassistant/components/cisco_mobility_express/device_tracker.py index ca24fcb5c52..702ebdfa611 100644 --- a/homeassistant/components/cisco_mobility_express/device_tracker.py +++ b/homeassistant/components/cisco_mobility_express/device_tracker.py @@ -1,9 +1,9 @@ """Support for Cisco Mobility Express.""" import logging +from ciscomobilityexpress.ciscome import CiscoMobilityExpress import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, @@ -11,11 +11,12 @@ from homeassistant.components.device_tracker import ( ) from homeassistant.const import ( CONF_HOST, - CONF_USERNAME, CONF_PASSWORD, CONF_SSL, + CONF_USERNAME, CONF_VERIFY_SSL, ) +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -35,7 +36,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def get_scanner(hass, config): """Validate the configuration and return a Cisco ME scanner.""" - from ciscomobilityexpress.ciscome import CiscoMobilityExpress config = config[DOMAIN] diff --git a/homeassistant/components/cisco_webex_teams/notify.py b/homeassistant/components/cisco_webex_teams/notify.py index a77f5673df7..6f80fa138d4 100644 --- a/homeassistant/components/cisco_webex_teams/notify.py +++ b/homeassistant/components/cisco_webex_teams/notify.py @@ -2,11 +2,12 @@ import logging import voluptuous as vol +from webexteamssdk import ApiError, WebexTeamsAPI, exceptions from homeassistant.components.notify import ( + ATTR_TITLE, PLATFORM_SCHEMA, BaseNotificationService, - ATTR_TITLE, ) from homeassistant.const import CONF_TOKEN import homeassistant.helpers.config_validation as cv @@ -22,7 +23,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def get_service(hass, config, discovery_info=None): """Get the CiscoWebexTeams notification service.""" - from webexteamssdk import WebexTeamsAPI, exceptions client = WebexTeamsAPI(access_token=config[CONF_TOKEN]) try: @@ -45,7 +45,6 @@ class CiscoWebexTeamsNotificationService(BaseNotificationService): def send_message(self, message="", **kwargs): """Send a message to a user.""" - from webexteamssdk import ApiError title = "" if kwargs.get(ATTR_TITLE) is not None: diff --git a/homeassistant/components/ciscospark/notify.py b/homeassistant/components/ciscospark/notify.py index 67609766366..e765aff05f6 100644 --- a/homeassistant/components/ciscospark/notify.py +++ b/homeassistant/components/ciscospark/notify.py @@ -1,16 +1,16 @@ """Cisco Spark platform for notify component.""" import logging +from ciscosparkapi import CiscoSparkAPI, SparkApiError import voluptuous as vol -from homeassistant.const import CONF_TOKEN -import homeassistant.helpers.config_validation as cv - from homeassistant.components.notify import ( ATTR_TITLE, PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.const import CONF_TOKEN +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -33,7 +33,6 @@ class CiscoSparkNotificationService(BaseNotificationService): def __init__(self, token, default_room): """Initialize the service.""" - from ciscosparkapi import CiscoSparkAPI self._default_room = default_room self._token = token @@ -41,7 +40,6 @@ class CiscoSparkNotificationService(BaseNotificationService): def send_message(self, message="", **kwargs): """Send a message to a user.""" - from ciscosparkapi import SparkApiError try: title = "" diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index 71550fc37b1..a2c79fdc0a7 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -1,6 +1,7 @@ """Component to integrate the Home Assistant cloud.""" import logging +from hass_nabucasa import Cloud import voluptuous as vol from homeassistant.auth.const import GROUP_ID_ADMIN @@ -20,25 +21,26 @@ from homeassistant.loader import bind_hass from homeassistant.util.aiohttp import MockRequest from . import http_api +from .client import CloudClient from .const import ( CONF_ACME_DIRECTORY_SERVER, CONF_ALEXA, + CONF_ALEXA_ACCESS_TOKEN_URL, CONF_ALIASES, CONF_CLOUDHOOK_CREATE_URL, CONF_COGNITO_CLIENT_ID, CONF_ENTITY_CONFIG, CONF_FILTER, CONF_GOOGLE_ACTIONS, + CONF_GOOGLE_ACTIONS_REPORT_STATE_URL, CONF_GOOGLE_ACTIONS_SYNC_URL, CONF_RELAYER, CONF_REMOTE_API_URL, CONF_SUBSCRIPTION_INFO_URL, CONF_USER_POOL_ID, - CONF_GOOGLE_ACTIONS_REPORT_STATE_URL, DOMAIN, MODE_DEV, MODE_PROD, - CONF_ALEXA_ACCESS_TOKEN_URL, ) from .prefs import CloudPreferences @@ -166,8 +168,6 @@ def is_cloudhook_request(request): async def async_setup(hass, config): """Initialize the Home Assistant cloud.""" - from hass_nabucasa import Cloud - from .client import CloudClient # Process configs if DOMAIN in config: diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index 38ae09ced93..c7626777943 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -110,14 +110,17 @@ class CloudClient(Interface): if not self.cloud.is_logged_in: return - if self.alexa_config.should_report_state: + if self.alexa_config.enabled and self.alexa_config.should_report_state: try: await self.alexa_config.async_enable_proactive_mode() except alexa_errors.NoTokenAvailable: pass - if self.google_config.should_report_state: - self.google_config.async_enable_report_state() + if self.google_config.enabled: + self.google_config.async_enable_local_sdk() + + if self.google_config.should_report_state: + self.google_config.async_enable_report_state() async def cleanups(self) -> None: """Cleanup some stuff after logout.""" diff --git a/homeassistant/components/cloud/const.py b/homeassistant/components/cloud/const.py index e28d75f017d..6495cba23b7 100644 --- a/homeassistant/components/cloud/const.py +++ b/homeassistant/components/cloud/const.py @@ -16,6 +16,7 @@ PREF_OVERRIDE_NAME = "override_name" PREF_DISABLE_2FA = "disable_2fa" PREF_ALIASES = "aliases" PREF_SHOULD_EXPOSE = "should_expose" +PREF_GOOGLE_LOCAL_WEBHOOK_ID = "google_local_webhook_id" 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 38e4aec56e0..582fa007550 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -63,6 +63,19 @@ class CloudGoogleConfig(AbstractConfig): """Return if states should be proactively reported.""" return self._prefs.google_report_state + @property + def local_sdk_webhook_id(self): + """Return the local SDK webhook. + + Return None to disable the local SDK. + """ + return self._prefs.google_local_webhook_id + + @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 + def should_expose(self, state): """If a state object should be exposed.""" return self._should_expose_entity_id(state.entity_id) @@ -131,17 +144,19 @@ 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() - return # If entity prefs are the same or we have filter in config.yaml, # don't sync. - if ( - self._cur_entity_prefs is prefs.google_entity_configs - or not self._config["filter"].empty_filter + elif ( + self._cur_entity_prefs is not prefs.google_entity_configs + and self._config["filter"].empty_filter ): - return + self.async_schedule_google_sync() - self.async_schedule_google_sync() + if self.enabled and not self.is_local_sdk_active: + self.async_enable_local_sdk() + elif not self.enabled and self.is_local_sdk_active: + self.async_disable_local_sdk() async def _handle_entity_registry_updated(self, event): """Handle when entity registry updated.""" diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index f243eab8fd0..97c96b0a3e8 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -3,33 +3,34 @@ import asyncio from functools import wraps import logging -import attr import aiohttp import async_timeout +import attr +from hass_nabucasa import Cloud, auth +from hass_nabucasa.const import STATE_DISCONNECTED import voluptuous as vol -from hass_nabucasa import Cloud -from homeassistant.core import callback -from homeassistant.components.http import HomeAssistantView -from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.components import websocket_api -from homeassistant.components.websocket_api import const as ws_const from homeassistant.components.alexa import ( entities as alexa_entities, errors as alexa_errors, ) from homeassistant.components.google_assistant import helpers as google_helpers +from homeassistant.components.http import HomeAssistantView +from homeassistant.components.http.data_validator import RequestDataValidator +from homeassistant.components.websocket_api import const as ws_const +from homeassistant.core import callback from .const import ( DOMAIN, - REQUEST_TIMEOUT, + PREF_ALEXA_REPORT_STATE, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE, + PREF_GOOGLE_REPORT_STATE, PREF_GOOGLE_SECURE_DEVICES_PIN, + REQUEST_TIMEOUT, InvalidTrustedNetworks, InvalidTrustedProxies, - PREF_ALEXA_REPORT_STATE, - PREF_GOOGLE_REPORT_STATE, RequireRelink, ) @@ -104,8 +105,6 @@ async def async_setup(hass): hass.http.register_view(CloudResendConfirmView) hass.http.register_view(CloudForgotPasswordView) - from hass_nabucasa import auth - _CLOUD_ERRORS.update( { auth.UserNotFound: (400, "User does not exist."), @@ -320,7 +319,6 @@ def _require_cloud_login(handler): @websocket_api.async_response async def websocket_subscription(hass, connection, msg): """Handle request for account info.""" - from hass_nabucasa.const import STATE_DISCONNECTED cloud = hass.data[DOMAIN] @@ -417,7 +415,6 @@ async def websocket_hook_delete(hass, connection, msg): def _account_data(cloud): """Generate the auth data JSON response.""" - from hass_nabucasa.const import STATE_DISCONNECTED if not cloud.is_logged_in: return {"logged_in": False, "cloud": STATE_DISCONNECTED} diff --git a/homeassistant/components/cloud/prefs.py b/homeassistant/components/cloud/prefs.py index a8ff775a227..0599b00a8bd 100644 --- a/homeassistant/components/cloud/prefs.py +++ b/homeassistant/components/cloud/prefs.py @@ -21,6 +21,7 @@ from .const import ( PREF_ALEXA_REPORT_STATE, DEFAULT_ALEXA_REPORT_STATE, PREF_GOOGLE_REPORT_STATE, + PREF_GOOGLE_LOCAL_WEBHOOK_ID, DEFAULT_GOOGLE_REPORT_STATE, InvalidTrustedNetworks, InvalidTrustedProxies, @@ -59,6 +60,14 @@ class CloudPreferences: self._prefs = prefs + if PREF_GOOGLE_LOCAL_WEBHOOK_ID not in self._prefs: + await self._save_prefs( + { + **self._prefs, + PREF_GOOGLE_LOCAL_WEBHOOK_ID: self._hass.components.webhook.async_generate_id(), + } + ) + @callback def async_listen_updates(self, listener): """Listen for updates to the preferences.""" @@ -79,6 +88,8 @@ class CloudPreferences: google_report_state=_UNDEF, ): """Update user preferences.""" + prefs = {**self._prefs} + for key, value in ( (PREF_ENABLE_GOOGLE, google_enabled), (PREF_ENABLE_ALEXA, alexa_enabled), @@ -92,20 +103,17 @@ class CloudPreferences: (PREF_GOOGLE_REPORT_STATE, google_report_state), ): if value is not _UNDEF: - self._prefs[key] = value + prefs[key] = value if remote_enabled is True and self._has_local_trusted_network: - self._prefs[PREF_ENABLE_REMOTE] = False + prefs[PREF_ENABLE_REMOTE] = False raise InvalidTrustedNetworks if remote_enabled is True and self._has_local_trusted_proxies: - self._prefs[PREF_ENABLE_REMOTE] = False + prefs[PREF_ENABLE_REMOTE] = False raise InvalidTrustedProxies - await self._store.async_save(self._prefs) - - for listener in self._listeners: - self._hass.async_create_task(async_create_catching_coro(listener(self))) + await self._save_prefs(prefs) async def async_update_google_entity_config( self, @@ -216,6 +224,11 @@ class CloudPreferences: """Return Google Entity configurations.""" return self._prefs.get(PREF_GOOGLE_ENTITY_CONFIGS, {}) + @property + def google_local_webhook_id(self): + """Return Google webhook ID to receive local messages.""" + return self._prefs[PREF_GOOGLE_LOCAL_WEBHOOK_ID] + @property def alexa_entity_configs(self): """Return Alexa Entity configurations.""" @@ -262,3 +275,11 @@ class CloudPreferences: return True return False + + async def _save_prefs(self, prefs): + """Save preferences to disk.""" + self._prefs = prefs + await self._store.async_save(self._prefs) + + for listener in self._listeners: + self._hass.async_create_task(async_create_catching_coro(listener(self))) diff --git a/homeassistant/components/cloudflare/__init__.py b/homeassistant/components/cloudflare/__init__.py index 26feff069da..265621b6250 100644 --- a/homeassistant/components/cloudflare/__init__.py +++ b/homeassistant/components/cloudflare/__init__.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +from pycfdns import CloudflareUpdater import voluptuous as vol from homeassistant.const import CONF_API_KEY, CONF_EMAIL, CONF_ZONE @@ -33,7 +34,6 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): """Set up the Cloudflare component.""" - from pycfdns import CloudflareUpdater cfupdate = CloudflareUpdater() email = config[DOMAIN][CONF_EMAIL] diff --git a/homeassistant/components/cmus/media_player.py b/homeassistant/components/cmus/media_player.py index dbaa763c461..3daf0bac828 100644 --- a/homeassistant/components/cmus/media_player.py +++ b/homeassistant/components/cmus/media_player.py @@ -1,9 +1,10 @@ """Support for interacting with and controlling the cmus music player.""" import logging +from pycmus import exceptions, remote 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, @@ -57,7 +58,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discover_info=None): """Set up the CMUS platform.""" - from pycmus import exceptions host = config.get(CONF_HOST) password = config.get(CONF_PASSWORD) @@ -78,7 +78,6 @@ class CmusDevice(MediaPlayerDevice): # pylint: disable=no-member def __init__(self, server, password, port, name): """Initialize the CMUS device.""" - from pycmus import remote if server: self.cmus = remote.PyCmus(server=server, password=password, port=port) diff --git a/homeassistant/components/co2signal/sensor.py b/homeassistant/components/co2signal/sensor.py index 9098a053fff..7160d140b3f 100644 --- a/homeassistant/components/co2signal/sensor.py +++ b/homeassistant/components/co2signal/sensor.py @@ -1,16 +1,17 @@ """Support for the CO2signal platform.""" import logging +import CO2Signal 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_TOKEN, CONF_LATITUDE, CONF_LONGITUDE, + CONF_TOKEN, ) -from homeassistant.components.sensor import PLATFORM_SCHEMA +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity CONF_COUNTRY_CODE = "country_code" @@ -97,7 +98,6 @@ class CO2Sensor(Entity): def update(self): """Get the latest data and updates the states.""" - import CO2Signal _LOGGER.debug("Update data for %s", self._friendly_name) diff --git a/homeassistant/components/coinbase/__init__.py b/homeassistant/components/coinbase/__init__.py index 6eca0616ca8..67869e6b88c 100644 --- a/homeassistant/components/coinbase/__init__.py +++ b/homeassistant/components/coinbase/__init__.py @@ -2,6 +2,8 @@ from datetime import timedelta import logging +from coinbase.wallet.client import Client +from coinbase.wallet.error import AuthenticationError import voluptuous as vol from homeassistant.const import CONF_API_KEY @@ -79,7 +81,6 @@ class CoinbaseData: def __init__(self, api_key, api_secret): """Init the coinbase data object.""" - from coinbase.wallet.client import Client self.client = Client(api_key, api_secret) self.update() @@ -87,7 +88,6 @@ class CoinbaseData: @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data from coinbase.""" - from coinbase.wallet.error import AuthenticationError try: self.accounts = self.client.get_accounts() diff --git a/homeassistant/components/coinmarketcap/sensor.py b/homeassistant/components/coinmarketcap/sensor.py index fbe05187684..ca166aa793a 100644 --- a/homeassistant/components/coinmarketcap/sensor.py +++ b/homeassistant/components/coinmarketcap/sensor.py @@ -1,13 +1,14 @@ """Details about crypto currencies from CoinMarketCap.""" -import logging from datetime import timedelta +import logging from urllib.error import HTTPError +from coinmarketcap import Market 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_DISPLAY_CURRENCY +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -159,6 +160,5 @@ class CoinMarketCapData: def update(self): """Get the latest data from coinmarketcap.com.""" - from coinmarketcap import Market self.ticker = Market().ticker(self.currency_id, convert=self.display_currency) diff --git a/homeassistant/components/comfoconnect/__init__.py b/homeassistant/components/comfoconnect/__init__.py index 22e9d95bbd8..aef4bf1deeb 100644 --- a/homeassistant/components/comfoconnect/__init__.py +++ b/homeassistant/components/comfoconnect/__init__.py @@ -1,6 +1,12 @@ """Support to control a Zehnder ComfoAir Q350/450/600 ventilation unit.""" import logging +from pycomfoconnect import ( + SENSOR_TEMPERATURE_EXTRACT, + SENSOR_TEMPERATURE_OUTDOOR, + Bridge, + ComfoConnect, +) import voluptuous as vol from homeassistant.const import ( @@ -56,7 +62,6 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): """Set up the ComfoConnect bridge.""" - from pycomfoconnect import Bridge conf = config[DOMAIN] host = conf.get(CONF_HOST) @@ -97,7 +102,6 @@ class ComfoConnectBridge: def __init__(self, hass, bridge, name, token, friendly_name, pin): """Initialize the ComfoConnect bridge.""" - from pycomfoconnect import ComfoConnect self.data = {} self.name = name @@ -125,11 +129,6 @@ class ComfoConnectBridge: """Call function for sensor updates.""" _LOGGER.debug("Got value from bridge: %d = %d", var, value) - from pycomfoconnect import ( - SENSOR_TEMPERATURE_EXTRACT, - SENSOR_TEMPERATURE_OUTDOOR, - ) - if var in [SENSOR_TEMPERATURE_EXTRACT, SENSOR_TEMPERATURE_OUTDOOR]: self.data[var] = value / 10 else: diff --git a/homeassistant/components/comfoconnect/fan.py b/homeassistant/components/comfoconnect/fan.py index 6c90ab8cba1..bbb4b0176bf 100644 --- a/homeassistant/components/comfoconnect/fan.py +++ b/homeassistant/components/comfoconnect/fan.py @@ -1,6 +1,14 @@ """Platform to control a Zehnder ComfoAir Q350/450/600 ventilation unit.""" import logging +from pycomfoconnect import ( + CMD_FAN_MODE_AWAY, + CMD_FAN_MODE_HIGH, + CMD_FAN_MODE_LOW, + CMD_FAN_MODE_MEDIUM, + SENSOR_FAN_SPEED_MODE, +) + from homeassistant.components.fan import ( SPEED_HIGH, SPEED_LOW, @@ -30,7 +38,6 @@ class ComfoConnectFan(FanEntity): def __init__(self, hass, name, ccb: ComfoConnectBridge) -> None: """Initialize the ComfoConnect fan.""" - from pycomfoconnect import SENSOR_FAN_SPEED_MODE self._ccb = ccb self._name = name @@ -64,7 +71,6 @@ class ComfoConnectFan(FanEntity): @property def speed(self): """Return the current fan mode.""" - from pycomfoconnect import SENSOR_FAN_SPEED_MODE try: speed = self._ccb.data[SENSOR_FAN_SPEED_MODE] @@ -91,13 +97,6 @@ class ComfoConnectFan(FanEntity): """Set fan speed.""" _LOGGER.debug("Changing fan speed to %s.", speed) - from pycomfoconnect import ( - CMD_FAN_MODE_AWAY, - CMD_FAN_MODE_LOW, - CMD_FAN_MODE_MEDIUM, - CMD_FAN_MODE_HIGH, - ) - if speed == SPEED_OFF: self._ccb.comfoconnect.cmd_rmi_request(CMD_FAN_MODE_AWAY) elif speed == SPEED_LOW: diff --git a/homeassistant/components/comfoconnect/sensor.py b/homeassistant/components/comfoconnect/sensor.py index 4099804d413..06d0506e2cf 100644 --- a/homeassistant/components/comfoconnect/sensor.py +++ b/homeassistant/components/comfoconnect/sensor.py @@ -1,6 +1,15 @@ """Platform to control a Zehnder ComfoAir Q350/450/600 ventilation unit.""" import logging +from pycomfoconnect import ( + SENSOR_FAN_EXHAUST_FLOW, + SENSOR_FAN_SUPPLY_FLOW, + SENSOR_HUMIDITY_EXTRACT, + SENSOR_HUMIDITY_OUTDOOR, + SENSOR_TEMPERATURE_EXTRACT, + SENSOR_TEMPERATURE_OUTDOOR, +) + from homeassistant.const import CONF_RESOURCES, TEMP_CELSIUS from homeassistant.helpers.dispatcher import dispatcher_connect from homeassistant.helpers.entity import Entity @@ -24,14 +33,6 @@ SENSOR_TYPES = {} def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the ComfoConnect fan platform.""" - from pycomfoconnect import ( - SENSOR_TEMPERATURE_EXTRACT, - SENSOR_HUMIDITY_EXTRACT, - SENSOR_TEMPERATURE_OUTDOOR, - SENSOR_HUMIDITY_OUTDOOR, - SENSOR_FAN_SUPPLY_FLOW, - SENSOR_FAN_EXHAUST_FLOW, - ) global SENSOR_TYPES SENSOR_TYPES = { diff --git a/homeassistant/components/concord232/alarm_control_panel.py b/homeassistant/components/concord232/alarm_control_panel.py index e86ec02040e..37bbf052838 100644 --- a/homeassistant/components/concord232/alarm_control_panel.py +++ b/homeassistant/components/concord232/alarm_control_panel.py @@ -2,22 +2,23 @@ import datetime import logging +from concord232 import client as concord232_client import requests import voluptuous as vol import homeassistant.components.alarm_control_panel as alarm -import homeassistant.helpers.config_validation as cv from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA from homeassistant.const import ( + CONF_CODE, CONF_HOST, + CONF_MODE, CONF_NAME, CONF_PORT, - CONF_CODE, - CONF_MODE, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, ) +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -60,7 +61,6 @@ class Concord232Alarm(alarm.AlarmControlPanel): def __init__(self, url, name, code, mode): """Initialize the Concord232 alarm panel.""" - from concord232 import client as concord232_client self._state = None self._name = name diff --git a/homeassistant/components/concord232/binary_sensor.py b/homeassistant/components/concord232/binary_sensor.py index 1a406d743b7..2d119e2cf86 100644 --- a/homeassistant/components/concord232/binary_sensor.py +++ b/homeassistant/components/concord232/binary_sensor.py @@ -2,13 +2,14 @@ import datetime import logging +from concord232 import client as concord232_client import requests import voluptuous as vol from homeassistant.components.binary_sensor import ( - BinarySensorDevice, - PLATFORM_SCHEMA, DEVICE_CLASSES, + PLATFORM_SCHEMA, + BinarySensorDevice, ) from homeassistant.const import CONF_HOST, CONF_PORT import homeassistant.helpers.config_validation as cv @@ -42,7 +43,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Concord232 binary sensor platform.""" - from concord232 import client as concord232_client host = config.get(CONF_HOST) port = config.get(CONF_PORT) diff --git a/homeassistant/components/config/automation.py b/homeassistant/components/config/automation.py index 97ddf1e0714..0e9b4053b7b 100644 --- a/homeassistant/components/config/automation.py +++ b/homeassistant/components/config/automation.py @@ -5,12 +5,11 @@ 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 import homeassistant.helpers.config_validation as cv from . import EditIdBasedConfigView -CONFIG_PATH = "automations.yaml" - async def async_setup(hass): """Set up the Automation config API.""" @@ -23,7 +22,7 @@ async def async_setup(hass): EditAutomationConfigView( DOMAIN, "config", - CONFIG_PATH, + AUTOMATION_CONFIG_PATH, cv.string, PLATFORM_SCHEMA, post_write_hook=hook, diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index b21991a8479..81065665e34 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -1,6 +1,7 @@ """Http views to control the config manager.""" import aiohttp.web_exceptions import voluptuous as vol +import voluptuous_serialize from homeassistant import config_entries, data_entry_flow from homeassistant.auth.permissions.const import CAT_CONFIG_ENTRIES @@ -41,8 +42,6 @@ def _prepare_json(result): if result["type"] != data_entry_flow.RESULT_TYPE_FORM: return result - import voluptuous_serialize - data = result.copy() schema = data["data_schema"] diff --git a/homeassistant/components/config/group.py b/homeassistant/components/config/group.py index 371bd98cf08..d104cd2e1df 100644 --- a/homeassistant/components/config/group.py +++ b/homeassistant/components/config/group.py @@ -1,12 +1,11 @@ """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 import homeassistant.helpers.config_validation as cv from . import EditKeyBasedConfigView -CONFIG_PATH = "groups.yaml" - async def async_setup(hass): """Set up the Group config API.""" @@ -17,7 +16,12 @@ async def async_setup(hass): hass.http.register_view( EditKeyBasedConfigView( - "group", "config", CONFIG_PATH, cv.slug, GROUP_SCHEMA, post_write_hook=hook + "group", + "config", + GROUP_CONFIG_PATH, + cv.slug, + GROUP_SCHEMA, + post_write_hook=hook, ) ) return True diff --git a/homeassistant/components/config/script.py b/homeassistant/components/config/script.py index 8ce163745f1..e63651d8f2a 100644 --- a/homeassistant/components/config/script.py +++ b/homeassistant/components/config/script.py @@ -1,12 +1,11 @@ """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 import homeassistant.helpers.config_validation as cv from . import EditKeyBasedConfigView -CONFIG_PATH = "scripts.yaml" - async def async_setup(hass): """Set up the script config API.""" @@ -19,7 +18,7 @@ async def async_setup(hass): EditKeyBasedConfigView( "script", "config", - CONFIG_PATH, + SCRIPT_CONFIG_PATH, cv.slug, SCRIPT_ENTRY_SCHEMA, post_write_hook=hook, diff --git a/homeassistant/components/conversation/__init__.py b/homeassistant/components/conversation/__init__.py index 9d7d510b10e..798fc926e0f 100644 --- a/homeassistant/components/conversation/__init__.py +++ b/homeassistant/components/conversation/__init__.py @@ -6,15 +6,12 @@ import voluptuous as vol from homeassistant import core from homeassistant.components import http -from homeassistant.components.cover import INTENT_CLOSE_COVER, INTENT_OPEN_COVER from homeassistant.components.http.data_validator import RequestDataValidator -from homeassistant.const import EVENT_COMPONENT_LOADED -from homeassistant.core import callback from homeassistant.helpers import config_validation as cv, intent from homeassistant.loader import bind_hass -from homeassistant.setup import ATTR_COMPONENT -from .util import create_matcher +from .agent import AbstractConversationAgent +from .default_agent import async_register, DefaultAgent _LOGGER = logging.getLogger(__name__) @@ -22,15 +19,8 @@ ATTR_TEXT = "text" DOMAIN = "conversation" -REGEX_TURN_COMMAND = re.compile(r"turn (?P(?: |\w)+) (?P\w+)") REGEX_TYPE = type(re.compile("")) - -UTTERANCES = { - "cover": { - INTENT_OPEN_COVER: ["Open [the] [a] [an] {name}[s]"], - INTENT_CLOSE_COVER: ["Close [the] [a] [an] {name}[s]"], - } -} +DATA_AGENT = "conversation_agent" SERVICE_PROCESS = "process" @@ -50,137 +40,64 @@ CONFIG_SCHEMA = vol.Schema( ) +async_register = bind_hass(async_register) # pylint: disable=invalid-name + + @core.callback @bind_hass -def async_register(hass, intent_type, utterances): - """Register utterances and any custom intents. - - Registrations don't require conversations to be loaded. They will become - active once the conversation component is loaded. - """ - intents = hass.data.get(DOMAIN) - - if intents is None: - intents = hass.data[DOMAIN] = {} - - conf = intents.get(intent_type) - - if conf is None: - conf = intents[intent_type] = [] - - for utterance in utterances: - if isinstance(utterance, REGEX_TYPE): - conf.append(utterance) - else: - conf.append(create_matcher(utterance)) +def async_set_agent(hass: core.HomeAssistant, agent: AbstractConversationAgent): + """Set the agent to handle the conversations.""" + hass.data[DATA_AGENT] = agent async def async_setup(hass, config): """Register the process service.""" - config = config.get(DOMAIN, {}) - intents = hass.data.get(DOMAIN) - if intents is None: - intents = hass.data[DOMAIN] = {} + async def process(hass, text): + """Process a line of text.""" + agent = hass.data.get(DATA_AGENT) - for intent_type, utterances in config.get("intents", {}).items(): - conf = intents.get(intent_type) + if agent is None: + agent = hass.data[DATA_AGENT] = DefaultAgent(hass) + await agent.async_initialize(config) - if conf is None: - conf = intents[intent_type] = [] + return await agent.async_process(text) - conf.extend(create_matcher(utterance) for utterance in utterances) - - async def process(service): + async def handle_service(service): """Parse text into commands.""" text = service.data[ATTR_TEXT] _LOGGER.debug("Processing: <%s>", text) try: - await _process(hass, text) + await process(hass, text) except intent.IntentHandleError as err: _LOGGER.error("Error processing %s: %s", text, err) hass.services.async_register( - DOMAIN, SERVICE_PROCESS, process, schema=SERVICE_PROCESS_SCHEMA + DOMAIN, SERVICE_PROCESS, handle_service, schema=SERVICE_PROCESS_SCHEMA ) - hass.http.register_view(ConversationProcessView) - - # We strip trailing 's' from name because our state matcher will fail - # if a letter is not there. By removing 's' we can match singular and - # plural names. - - async_register( - hass, - intent.INTENT_TURN_ON, - ["Turn [the] [a] {name}[s] on", "Turn on [the] [a] [an] {name}[s]"], - ) - async_register( - hass, - intent.INTENT_TURN_OFF, - ["Turn [the] [a] [an] {name}[s] off", "Turn off [the] [a] [an] {name}[s]"], - ) - async_register( - hass, - intent.INTENT_TOGGLE, - ["Toggle [the] [a] [an] {name}[s]", "[the] [a] [an] {name}[s] toggle"], - ) - - @callback - def register_utterances(component): - """Register utterances for a component.""" - if component not in UTTERANCES: - return - for intent_type, sentences in UTTERANCES[component].items(): - async_register(hass, intent_type, sentences) - - @callback - def component_loaded(event): - """Handle a new component loaded.""" - register_utterances(event.data[ATTR_COMPONENT]) - - hass.bus.async_listen(EVENT_COMPONENT_LOADED, component_loaded) - - # Check already loaded components. - for component in hass.config.components: - register_utterances(component) + hass.http.register_view(ConversationProcessView(process)) return True -async def _process(hass, text): - """Process a line of text.""" - intents = hass.data.get(DOMAIN, {}) - - for intent_type, matchers in intents.items(): - for matcher in matchers: - match = matcher.match(text) - - if not match: - continue - - response = await hass.helpers.intent.async_handle( - DOMAIN, - intent_type, - {key: {"value": value} for key, value in match.groupdict().items()}, - text, - ) - return response - - class ConversationProcessView(http.HomeAssistantView): """View to retrieve shopping list content.""" url = "/api/conversation/process" name = "api:conversation:process" + def __init__(self, process): + """Initialize the conversation process view.""" + self._process = process + @RequestDataValidator(vol.Schema({vol.Required("text"): str})) async def post(self, request, data): """Send a request for processing.""" hass = request.app["hass"] try: - intent_result = await _process(hass, data["text"]) + intent_result = await self._process(hass, data["text"]) except intent.IntentHandleError as err: intent_result = intent.IntentResponse() intent_result.async_set_speech(str(err)) diff --git a/homeassistant/components/conversation/agent.py b/homeassistant/components/conversation/agent.py new file mode 100644 index 00000000000..eae6402530c --- /dev/null +++ b/homeassistant/components/conversation/agent.py @@ -0,0 +1,12 @@ +"""Agent foundation for conversation integration.""" +from abc import ABC, abstractmethod + +from homeassistant.helpers import intent + + +class AbstractConversationAgent(ABC): + """Abstract conversation agent.""" + + @abstractmethod + async def async_process(self, text: str) -> intent.IntentResponse: + """Process a sentence.""" diff --git a/homeassistant/components/conversation/const.py b/homeassistant/components/conversation/const.py new file mode 100644 index 00000000000..04bfa373061 --- /dev/null +++ b/homeassistant/components/conversation/const.py @@ -0,0 +1,3 @@ +"""Const for conversation integration.""" + +DOMAIN = "conversation" diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py new file mode 100644 index 00000000000..e93afcfaf65 --- /dev/null +++ b/homeassistant/components/conversation/default_agent.py @@ -0,0 +1,127 @@ +"""Standard conversastion implementation for Home Assistant.""" +import logging +import re + +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.const import EVENT_COMPONENT_LOADED +from homeassistant.core import callback +from homeassistant.helpers import intent +from homeassistant.setup import ATTR_COMPONENT + +from .agent import AbstractConversationAgent +from .const import DOMAIN +from .util import create_matcher + +_LOGGER = logging.getLogger(__name__) + +REGEX_TURN_COMMAND = re.compile(r"turn (?P(?: |\w)+) (?P\w+)") +REGEX_TYPE = type(re.compile("")) + +UTTERANCES = { + "cover": { + INTENT_OPEN_COVER: ["Open [the] [a] [an] {name}[s]"], + INTENT_CLOSE_COVER: ["Close [the] [a] [an] {name}[s]"], + }, + "shopping_list": { + INTENT_ADD_ITEM: ["Add [the] [a] [an] {item} to my shopping list"], + INTENT_LAST_ITEMS: ["What is on my shopping list"], + }, +} + + +@core.callback +def async_register(hass, intent_type, utterances): + """Register utterances and any custom intents for the default agent. + + Registrations don't require conversations to be loaded. They will become + active once the conversation component is loaded. + """ + intents = hass.data.setdefault(DOMAIN, {}) + conf = intents.setdefault(intent_type, []) + + for utterance in utterances: + if isinstance(utterance, REGEX_TYPE): + conf.append(utterance) + else: + conf.append(create_matcher(utterance)) + + +class DefaultAgent(AbstractConversationAgent): + """Default agent for conversation agent.""" + + def __init__(self, hass: core.HomeAssistant): + """Initialize the default agent.""" + self.hass = hass + + async def async_initialize(self, config): + """Initialize the default agent.""" + config = config.get(DOMAIN, {}) + intents = self.hass.data.setdefault(DOMAIN, {}) + + for intent_type, utterances in config.get("intents", {}).items(): + conf = intents.get(intent_type) + + if conf is None: + conf = intents[intent_type] = [] + + conf.extend(create_matcher(utterance) for utterance in utterances) + + # We strip trailing 's' from name because our state matcher will fail + # if a letter is not there. By removing 's' we can match singular and + # plural names. + + async_register( + self.hass, + intent.INTENT_TURN_ON, + ["Turn [the] [a] {name}[s] on", "Turn on [the] [a] [an] {name}[s]"], + ) + async_register( + self.hass, + intent.INTENT_TURN_OFF, + ["Turn [the] [a] [an] {name}[s] off", "Turn off [the] [a] [an] {name}[s]"], + ) + async_register( + self.hass, + intent.INTENT_TOGGLE, + ["Toggle [the] [a] [an] {name}[s]", "[the] [a] [an] {name}[s] toggle"], + ) + + @callback + def component_loaded(event): + """Handle a new component loaded.""" + self.register_utterances(event.data[ATTR_COMPONENT]) + + self.hass.bus.async_listen(EVENT_COMPONENT_LOADED, component_loaded) + + # Check already loaded components. + for component in self.hass.config.components: + self.register_utterances(component) + + @callback + def register_utterances(self, component): + """Register utterances for a component.""" + if component not in UTTERANCES: + return + for intent_type, sentences in UTTERANCES[component].items(): + async_register(self.hass, intent_type, sentences) + + async def async_process(self, text) -> intent.IntentResponse: + """Process a sentence.""" + intents = self.hass.data[DOMAIN] + + for intent_type, matchers in intents.items(): + for matcher in matchers: + match = matcher.match(text) + + if not match: + continue + + return await intent.async_handle( + self.hass, + DOMAIN, + intent_type, + {key: {"value": value} for key, value in match.groupdict().items()}, + text, + ) diff --git a/homeassistant/components/coolmaster/.translations/en.json b/homeassistant/components/coolmaster/.translations/en.json new file mode 100644 index 00000000000..6c30efc594a --- /dev/null +++ b/homeassistant/components/coolmaster/.translations/en.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "connection_error": "Failed to connect to CoolMasterNet instance. Please check your host.", + "no_units": "Could not find any HVAC units in CoolMasterNet host." + }, + "step": { + "user": { + "data": { + "cool": "Support cool mode", + "dry": "Support dry mode", + "fan_only": "Support fan only mode", + "heat": "Support heat mode", + "heat_cool": "Support automatic heat/cool mode", + "host": "Host", + "off": "Can be turned off" + }, + "title": "Setup your CoolMasterNet connection details." + } + }, + "title": "CoolMasterNet" + } +} \ No newline at end of file diff --git a/homeassistant/components/coolmaster/__init__.py b/homeassistant/components/coolmaster/__init__.py index b27ae5f25b4..530427d33ad 100644 --- a/homeassistant/components/coolmaster/__init__.py +++ b/homeassistant/components/coolmaster/__init__.py @@ -1 +1,22 @@ -"""The coolmaster component.""" +"""The Coolmaster integration.""" + + +async def async_setup(hass, config): + """Set up Coolmaster components.""" + return True + + +async def async_setup_entry(hass, entry): + """Set up Coolmaster from a config entry.""" + hass.async_add_job(hass.config_entries.async_forward_entry_setup(entry, "climate")) + + return True + + +async def async_unload_entry(hass, entry): + """Unload a Coolmaster config entry.""" + await hass.async_add_job( + hass.config_entries.async_forward_entry_unload(entry, "climate") + ) + + return True diff --git a/homeassistant/components/coolmaster/climate.py b/homeassistant/components/coolmaster/climate.py index 8a319c655f6..a52431dd89b 100644 --- a/homeassistant/components/coolmaster/climate.py +++ b/homeassistant/components/coolmaster/climate.py @@ -2,16 +2,16 @@ import logging -import voluptuous as vol +from pycoolmasternet import CoolMasterNet -from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( - HVAC_MODE_OFF, - 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_TARGET_TEMPERATURE, ) @@ -22,21 +22,11 @@ from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, ) -import homeassistant.helpers.config_validation as cv + +from .const import CONF_SUPPORTED_MODES, DOMAIN SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE -DEFAULT_PORT = 10102 - -AVAILABLE_MODES = [ - HVAC_MODE_OFF, - HVAC_MODE_HEAT, - HVAC_MODE_COOL, - HVAC_MODE_DRY, - HVAC_MODE_HEAT_COOL, - HVAC_MODE_FAN_ONLY, -] - CM_TO_HA_STATE = { "heat": HVAC_MODE_HEAT, "cool": HVAC_MODE_COOL, @@ -49,17 +39,6 @@ HA_STATE_TO_CM = {value: key for key, value in CM_TO_HA_STATE.items()} FAN_MODES = ["low", "med", "high", "auto"] -CONF_SUPPORTED_MODES = "supported_modes" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_SUPPORTED_MODES, default=AVAILABLE_MODES): vol.All( - cv.ensure_list, [vol.In(AVAILABLE_MODES)] - ), - } -) - _LOGGER = logging.getLogger(__name__) @@ -68,19 +47,17 @@ def _build_entity(device, supported_modes): return CoolmasterClimate(device, supported_modes) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_entry(hass, config_entry, async_add_devices): """Set up the CoolMasterNet climate platform.""" - from pycoolmasternet import CoolMasterNet - - supported_modes = config.get(CONF_SUPPORTED_MODES) - host = config[CONF_HOST] - port = config[CONF_PORT] + supported_modes = config_entry.data.get(CONF_SUPPORTED_MODES) + host = config_entry.data[CONF_HOST] + port = config_entry.data[CONF_PORT] cool = CoolMasterNet(host, port=port) - devices = cool.devices() + devices = await hass.async_add_executor_job(cool.devices) all_devices = [_build_entity(device, supported_modes) for device in devices] - add_entities(all_devices, True) + async_add_devices(all_devices, True) class CoolmasterClimate(ClimateDevice): @@ -118,6 +95,16 @@ class CoolmasterClimate(ClimateDevice): else: self._unit = TEMP_FAHRENHEIT + @property + def device_info(self): + """Return device info for this device.""" + return { + "identifiers": {(DOMAIN, self.unique_id)}, + "name": self.name, + "manufacturer": "CoolAutomation", + "model": "CoolMasterNet", + } + @property def unique_id(self): """Return unique ID for this device.""" diff --git a/homeassistant/components/coolmaster/config_flow.py b/homeassistant/components/coolmaster/config_flow.py new file mode 100644 index 00000000000..543b4c239c8 --- /dev/null +++ b/homeassistant/components/coolmaster/config_flow.py @@ -0,0 +1,64 @@ +"""Config flow to configure Coolmaster.""" + +from pycoolmasternet import CoolMasterNet +import voluptuous as vol + +from homeassistant import core, config_entries +from homeassistant.const import CONF_HOST, CONF_PORT + +# pylint: disable=unused-import +from .const import AVAILABLE_MODES, CONF_SUPPORTED_MODES, DEFAULT_PORT, DOMAIN + +MODES_SCHEMA = {vol.Required(mode, default=True): bool for mode in AVAILABLE_MODES} + +DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str, **MODES_SCHEMA}) + + +async def validate_connection(hass: core.HomeAssistant, host): + """Validate that we can connect to the Coolmaster instance.""" + cool = CoolMasterNet(host, port=DEFAULT_PORT) + devices = await hass.async_add_executor_job(cool.devices) + return len(devices) > 0 + + +class CoolmasterConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a Coolmaster config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + def _async_get_entry(self, data): + supported_modes = [ + key for (key, value) in data.items() if key in AVAILABLE_MODES and value + ] + return self.async_create_entry( + title=data[CONF_HOST], + data={ + CONF_HOST: data[CONF_HOST], + CONF_PORT: DEFAULT_PORT, + CONF_SUPPORTED_MODES: supported_modes, + }, + ) + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + if user_input is None: + return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA) + + errors = {} + + host = user_input[CONF_HOST] + + try: + result = await validate_connection(self.hass, host) + if not result: + errors["base"] = "no_units" + except (ConnectionRefusedError, TimeoutError): + errors["base"] = "connection_error" + + if errors: + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) + + return self._async_get_entry(user_input) diff --git a/homeassistant/components/coolmaster/const.py b/homeassistant/components/coolmaster/const.py new file mode 100644 index 00000000000..d4cfea73820 --- /dev/null +++ b/homeassistant/components/coolmaster/const.py @@ -0,0 +1,25 @@ +"""Constants for the Coolmaster integration.""" + +from homeassistant.components.climate.const import ( + HVAC_MODE_COOL, + HVAC_MODE_DRY, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, +) + +DOMAIN = "coolmaster" + +DEFAULT_PORT = 10102 + +CONF_SUPPORTED_MODES = "supported_modes" + +AVAILABLE_MODES = [ + HVAC_MODE_OFF, + HVAC_MODE_HEAT, + HVAC_MODE_COOL, + HVAC_MODE_DRY, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_FAN_ONLY, +] diff --git a/homeassistant/components/coolmaster/manifest.json b/homeassistant/components/coolmaster/manifest.json index 69ab8ee3c4b..124a1e4a5b9 100644 --- a/homeassistant/components/coolmaster/manifest.json +++ b/homeassistant/components/coolmaster/manifest.json @@ -1,6 +1,7 @@ { "domain": "coolmaster", "name": "Coolmaster", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/coolmaster", "requirements": [ "pycoolmasternet==0.0.4" diff --git a/homeassistant/components/coolmaster/strings.json b/homeassistant/components/coolmaster/strings.json new file mode 100644 index 00000000000..d309f8c9c93 --- /dev/null +++ b/homeassistant/components/coolmaster/strings.json @@ -0,0 +1,23 @@ +{ + "config": { + "title": "CoolMasterNet", + "step": { + "user": { + "title": "Setup your CoolMasterNet connection details.", + "data": { + "host": "Host", + "off": "Can be turned off", + "heat": "Support heat mode", + "cool": "Support cool mode", + "heat_cool": "Support automatic heat/cool mode", + "dry": "Support dry mode", + "fan_only": "Support fan only mode" + } + } + }, + "error": { + "connection_error": "Failed to connect to CoolMasterNet instance. Please check your host.", + "no_units": "Could not find any HVAC units in CoolMasterNet host." + } + } +} diff --git a/homeassistant/components/counter/__init__.py b/homeassistant/components/counter/__init__.py index 79877d63f14..aca3461b4f7 100644 --- a/homeassistant/components/counter/__init__.py +++ b/homeassistant/components/counter/__init__.py @@ -16,6 +16,7 @@ ATTR_INITIAL = "initial" ATTR_STEP = "step" ATTR_MINIMUM = "minimum" ATTR_MAXIMUM = "maximum" +VALUE = "value" CONF_INITIAL = "initial" CONF_RESTORE = "restore" @@ -37,6 +38,8 @@ 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, } ) @@ -171,6 +174,10 @@ class Counter(RestoreEntity): state = await self.async_get_last_state() if state is not None: self._state = self.compute_next_state(int(state.state)) + self._initial = state.attributes.get(ATTR_INITIAL) + self._max = state.attributes.get(ATTR_MAXIMUM) + self._min = state.attributes.get(ATTR_MINIMUM) + self._step = state.attributes.get(ATTR_STEP) async def async_decrement(self): """Decrement the counter.""" @@ -195,6 +202,10 @@ class Counter(RestoreEntity): self._max = kwargs[CONF_MAXIMUM] if CONF_STEP in kwargs: self._step = kwargs[CONF_STEP] + if CONF_INITIAL in kwargs: + self._initial = kwargs[CONF_INITIAL] + if VALUE in kwargs: + self._state = kwargs[VALUE] self._state = self.compute_next_state(self._state) await self.async_update_ha_state() diff --git a/homeassistant/components/counter/reproduce_state.py b/homeassistant/components/counter/reproduce_state.py new file mode 100644 index 00000000000..ac5045d68e7 --- /dev/null +++ b/homeassistant/components/counter/reproduce_state.py @@ -0,0 +1,71 @@ +"""Reproduce an Counter state.""" +import asyncio +import logging +from typing import Iterable, Optional + +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import Context, State +from homeassistant.helpers.typing import HomeAssistantType + +from . import ( + ATTR_INITIAL, + ATTR_MAXIMUM, + ATTR_MINIMUM, + ATTR_STEP, + VALUE, + DOMAIN, + SERVICE_CONFIGURE, +) + +_LOGGER = logging.getLogger(__name__) + + +async def _async_reproduce_state( + hass: HomeAssistantType, state: State, context: Optional[Context] = None +) -> None: + """Reproduce a single state.""" + cur_state = hass.states.get(state.entity_id) + + if cur_state is None: + _LOGGER.warning("Unable to find entity %s", state.entity_id) + return + + if not state.state.isdigit(): + _LOGGER.warning( + "Invalid state specified for %s: %s", state.entity_id, state.state + ) + return + + # Return if we are already at the right state. + if ( + cur_state.state == state.state + and cur_state.attributes.get(ATTR_INITIAL) == state.attributes.get(ATTR_INITIAL) + and cur_state.attributes.get(ATTR_MAXIMUM) == state.attributes.get(ATTR_MAXIMUM) + and cur_state.attributes.get(ATTR_MINIMUM) == state.attributes.get(ATTR_MINIMUM) + and cur_state.attributes.get(ATTR_STEP) == state.attributes.get(ATTR_STEP) + ): + return + + service_data = {ATTR_ENTITY_ID: state.entity_id, VALUE: state.state} + service = SERVICE_CONFIGURE + if ATTR_INITIAL in state.attributes: + service_data[ATTR_INITIAL] = state.attributes[ATTR_INITIAL] + if ATTR_MAXIMUM in state.attributes: + service_data[ATTR_MAXIMUM] = state.attributes[ATTR_MAXIMUM] + if ATTR_MINIMUM in state.attributes: + service_data[ATTR_MINIMUM] = state.attributes[ATTR_MINIMUM] + if ATTR_STEP in state.attributes: + service_data[ATTR_STEP] = state.attributes[ATTR_STEP] + + await hass.services.async_call( + DOMAIN, service, service_data, context=context, blocking=True + ) + + +async def async_reproduce_states( + hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None +) -> None: + """Reproduce Counter states.""" + await asyncio.gather( + *(_async_reproduce_state(hass, state, context) for state in states) + ) diff --git a/homeassistant/components/counter/services.yaml b/homeassistant/components/counter/services.yaml index fc3f0ad36cb..449ae6841ff 100644 --- a/homeassistant/components/counter/services.yaml +++ b/homeassistant/components/counter/services.yaml @@ -33,3 +33,9 @@ configure: step: description: New value for step example: 2 + initial: + description: New value for initial + example: 6 + value: + description: New state value + example: 3 diff --git a/homeassistant/components/cover/.translations/ca.json b/homeassistant/components/cover/.translations/ca.json new file mode 100644 index 00000000000..ffa9ca1a927 --- /dev/null +++ b/homeassistant/components/cover/.translations/ca.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "condition_type": { + "is_closed": "{entity_name} est\u00e0 tancat/da", + "is_closing": "{entity_name} est\u00e0 tancan't-se", + "is_open": "{entity_name} est\u00e0 obert/a", + "is_opening": "{entity_name} s'est\u00e0 obrint" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/da.json b/homeassistant/components/cover/.translations/da.json new file mode 100644 index 00000000000..e603723b564 --- /dev/null +++ b/homeassistant/components/cover/.translations/da.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "condition_type": { + "is_closed": "{entity_name} er lukket", + "is_closing": "{entity_name} lukker", + "is_open": "{entity_name} er \u00e5ben", + "is_opening": "{entity_name} \u00e5bnes" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/de.json b/homeassistant/components/cover/.translations/de.json new file mode 100644 index 00000000000..e9ed497ccc2 --- /dev/null +++ b/homeassistant/components/cover/.translations/de.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "condition_type": { + "is_closed": "{entity_name} ist geschlossen", + "is_closing": "{entity_name} wird geschlossen", + "is_open": "{entity_name} ist offen", + "is_opening": "{entity_name} wird ge\u00f6ffnet" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/en.json b/homeassistant/components/cover/.translations/en.json new file mode 100644 index 00000000000..f9f47be3104 --- /dev/null +++ b/homeassistant/components/cover/.translations/en.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "condition_type": { + "is_closed": "{entity_name} is closed", + "is_closing": "{entity_name} is closing", + "is_open": "{entity_name} is open", + "is_opening": "{entity_name} is opening" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/es.json b/homeassistant/components/cover/.translations/es.json new file mode 100644 index 00000000000..d0193b939a5 --- /dev/null +++ b/homeassistant/components/cover/.translations/es.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "condition_type": { + "is_closed": "{entity_name} est\u00e1 cerrado", + "is_closing": "{entity_name} se est\u00e1 cerrando", + "is_open": "{entity_name} est\u00e1 abierto", + "is_opening": "{entity_name} se est\u00e1 abriendo" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/fr.json b/homeassistant/components/cover/.translations/fr.json new file mode 100644 index 00000000000..95978ed0fa5 --- /dev/null +++ b/homeassistant/components/cover/.translations/fr.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "condition_type": { + "is_closed": "{entity_name} est ferm\u00e9", + "is_closing": "{entity_name} se ferme", + "is_open": "{entity_name} est ouvert", + "is_opening": "{entity_name} est en train de s'ouvrir" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/it.json b/homeassistant/components/cover/.translations/it.json new file mode 100644 index 00000000000..6a25c0f3f2f --- /dev/null +++ b/homeassistant/components/cover/.translations/it.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "condition_type": { + "is_closed": "{entity_name} \u00e8 chiuso", + "is_closing": "{entity_name} si sta chiudendo", + "is_open": "{entity_name} \u00e8 aperto", + "is_opening": "{entity_name} si sta aprendo" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/ko.json b/homeassistant/components/cover/.translations/ko.json new file mode 100644 index 00000000000..02f900a8fe5 --- /dev/null +++ b/homeassistant/components/cover/.translations/ko.json @@ -0,0 +1,10 @@ +{ + "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" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/lb.json b/homeassistant/components/cover/.translations/lb.json new file mode 100644 index 00000000000..b0c9e1d0d4c --- /dev/null +++ b/homeassistant/components/cover/.translations/lb.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "condition_type": { + "is_closed": "{entity_name} ass zou", + "is_closing": "{entity_name} g\u00ebtt zougemaach", + "is_open": "{entity_name} ass op", + "is_opening": "{entity_name} g\u00ebtt opgemaach" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/nl.json b/homeassistant/components/cover/.translations/nl.json new file mode 100644 index 00000000000..93015afbfdd --- /dev/null +++ b/homeassistant/components/cover/.translations/nl.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "condition_type": { + "is_closed": "{entity_name} is gesloten", + "is_closing": "{entity_name} wordt gesloten", + "is_open": "{entity_name} is open", + "is_opening": "{entity_name} wordt geopend" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/no.json b/homeassistant/components/cover/.translations/no.json new file mode 100644 index 00000000000..af567bcfcfc --- /dev/null +++ b/homeassistant/components/cover/.translations/no.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "condition_type": { + "is_closed": "{entity_name} er stengt", + "is_closing": "{entity_name} stenges", + "is_open": "{entity_name} er \u00e5pen", + "is_opening": "{entity_name} \u00e5pnes" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/pl.json b/homeassistant/components/cover/.translations/pl.json new file mode 100644 index 00000000000..4adc0c17b54 --- /dev/null +++ b/homeassistant/components/cover/.translations/pl.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "condition_type": { + "is_closed": "pokrywa {entity_name} jest zamkni\u0119ta", + "is_closing": "{entity_name} si\u0119 zamyka", + "is_open": "pokrywa {entity_name} jest otwarta", + "is_opening": "{entity_name} si\u0119 otwiera" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/pt.json b/homeassistant/components/cover/.translations/pt.json new file mode 100644 index 00000000000..cb9f85c4a93 --- /dev/null +++ b/homeassistant/components/cover/.translations/pt.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "condition_type": { + "is_closed": "{entity_name} est\u00e1 fechada", + "is_closing": "{entity_name} est\u00e1 a fechar", + "is_open": "{entity_name} est\u00e1 aberta", + "is_opening": "{entity_name} est\u00e1 a abrir" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/ru.json b/homeassistant/components/cover/.translations/ru.json new file mode 100644 index 00000000000..46456bb9464 --- /dev/null +++ b/homeassistant/components/cover/.translations/ru.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "condition_type": { + "is_closed": "{entity_name} \u0432 \u0437\u0430\u043a\u0440\u044b\u0442\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_closing": "{entity_name} \u0437\u0430\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", + "is_open": "{entity_name} \u0432 \u043e\u0442\u043a\u0440\u044b\u0442\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_opening": "{entity_name} \u043e\u0442\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/sl.json b/homeassistant/components/cover/.translations/sl.json new file mode 100644 index 00000000000..cb5109b5cb0 --- /dev/null +++ b/homeassistant/components/cover/.translations/sl.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "condition_type": { + "is_closed": "{entity_name} je/so zaprt/a", + "is_closing": "{entity_name} se zapira/jo", + "is_open": "{entity_name} je odprt/a/o", + "is_opening": "{entity_name} se odpira/jo" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/zh-Hant.json b/homeassistant/components/cover/.translations/zh-Hant.json new file mode 100644 index 00000000000..9723d1a0dd6 --- /dev/null +++ b/homeassistant/components/cover/.translations/zh-Hant.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "condition_type": { + "is_closed": "{entity_name} \u5df2\u95dc\u9589", + "is_closing": "{entity_name} \u6b63\u5728\u95dc\u9589", + "is_open": "{entity_name} \u5df2\u958b\u555f", + "is_opening": "{entity_name} \u6b63\u5728\u958b\u555f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index 8d2b4430fe1..cfac143a5d8 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -34,7 +34,7 @@ from homeassistant.const import ( ) -# mypy: allow-untyped-calls, allow-untyped-defs +# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/cover/device_condition.py b/homeassistant/components/cover/device_condition.py new file mode 100644 index 00000000000..129462047e4 --- /dev/null +++ b/homeassistant/components/cover/device_condition.py @@ -0,0 +1,103 @@ +"""Provides device automations for Cover.""" +from typing import Any, Dict, List +import voluptuous as vol + +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_CONDITION, + CONF_DOMAIN, + CONF_TYPE, + CONF_DEVICE_ID, + CONF_ENTITY_ID, + STATE_OPEN, + STATE_CLOSED, + STATE_OPENING, + STATE_CLOSING, +) +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 + +CONDITION_TYPES = {"is_open", "is_closed", "is_opening", "is_closing"} + +CONDITION_SCHEMA = DEVICE_CONDITION_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In(CONDITION_TYPES), + } +) + + +async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device conditions for Cover devices.""" + registry = await entity_registry.async_get_registry(hass) + conditions: List[Dict[str, Any]] = [] + + # 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 + + # Add conditions for each entity that belongs to this integration + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_open", + } + ) + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_closed", + } + ) + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_opening", + } + ) + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_closing", + } + ) + + return conditions + + +def async_condition_from_config( + config: ConfigType, config_validation: bool +) -> condition.ConditionCheckerType: + """Create a function to test a device condition.""" + if config_validation: + config = CONDITION_SCHEMA(config) + if config[CONF_TYPE] == "is_open": + state = STATE_OPEN + elif config[CONF_TYPE] == "is_closed": + state = STATE_CLOSED + elif config[CONF_TYPE] == "is_opening": + state = STATE_OPENING + elif config[CONF_TYPE] == "is_closing": + state = STATE_CLOSING + + def test_is_state(hass: HomeAssistant, variables: TemplateVarsType) -> bool: + """Test if an entity is a certain state.""" + return condition.state(hass, config[ATTR_ENTITY_ID], state) + + return test_is_state diff --git a/homeassistant/components/cover/reproduce_state.py b/homeassistant/components/cover/reproduce_state.py new file mode 100644 index 00000000000..64ea410ce93 --- /dev/null +++ b/homeassistant/components/cover/reproduce_state.py @@ -0,0 +1,117 @@ +"""Reproduce an Cover state.""" +import asyncio +import logging +from typing import Iterable, Optional + +from homeassistant.components.cover import ( + ATTR_CURRENT_POSITION, + ATTR_CURRENT_TILT_POSITION, + ATTR_POSITION, + ATTR_TILT_POSITION, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_CLOSE_COVER, + SERVICE_CLOSE_COVER_TILT, + SERVICE_OPEN_COVER, + SERVICE_OPEN_COVER_TILT, + SERVICE_SET_COVER_POSITION, + SERVICE_SET_COVER_TILT_POSITION, + STATE_CLOSED, + STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, +) +from homeassistant.core import Context, State +from homeassistant.helpers.typing import HomeAssistantType + +from . import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +VALID_STATES = {STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING} + + +async def _async_reproduce_state( + hass: HomeAssistantType, state: State, context: Optional[Context] = None +) -> None: + """Reproduce a single state.""" + cur_state = hass.states.get(state.entity_id) + + if cur_state is None: + _LOGGER.warning("Unable to find entity %s", state.entity_id) + return + + if state.state not in VALID_STATES: + _LOGGER.warning( + "Invalid state specified for %s: %s", state.entity_id, state.state + ) + return + + # Return if we are already at the right state. + if ( + cur_state.state == state.state + and cur_state.attributes.get(ATTR_CURRENT_POSITION) + == state.attributes.get(ATTR_CURRENT_POSITION) + and cur_state.attributes.get(ATTR_CURRENT_TILT_POSITION) + == state.attributes.get(ATTR_CURRENT_TILT_POSITION) + ): + return + + service_data = {ATTR_ENTITY_ID: state.entity_id} + service_data_tilting = {ATTR_ENTITY_ID: state.entity_id} + + if cur_state.state != state.state or cur_state.attributes.get( + ATTR_CURRENT_POSITION + ) != state.attributes.get(ATTR_CURRENT_POSITION): + # Open/Close + if state.state == STATE_CLOSED or state.state == STATE_CLOSING: + service = SERVICE_CLOSE_COVER + elif state.state == STATE_OPEN or state.state == STATE_OPENING: + if ( + ATTR_CURRENT_POSITION in cur_state.attributes + and ATTR_CURRENT_POSITION in state.attributes + ): + service = SERVICE_SET_COVER_POSITION + service_data[ATTR_POSITION] = state.attributes[ATTR_CURRENT_POSITION] + else: + service = SERVICE_OPEN_COVER + + await hass.services.async_call( + DOMAIN, service, service_data, context=context, blocking=True + ) + + if ( + ATTR_CURRENT_TILT_POSITION in state.attributes + and ATTR_CURRENT_TILT_POSITION in cur_state.attributes + and cur_state.attributes.get(ATTR_CURRENT_TILT_POSITION) + != state.attributes.get(ATTR_CURRENT_TILT_POSITION) + ): + # Tilt position + if state.attributes.get(ATTR_CURRENT_TILT_POSITION) == 100: + service_tilting = SERVICE_OPEN_COVER_TILT + elif state.attributes.get(ATTR_CURRENT_TILT_POSITION) == 0: + service_tilting = SERVICE_CLOSE_COVER_TILT + else: + service_tilting = SERVICE_SET_COVER_TILT_POSITION + service_data_tilting[ATTR_TILT_POSITION] = state.attributes[ + ATTR_CURRENT_TILT_POSITION + ] + + await hass.services.async_call( + DOMAIN, + service_tilting, + service_data_tilting, + context=context, + blocking=True, + ) + + +async def async_reproduce_states( + hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None +) -> None: + """Reproduce Cover states.""" + # Reproduce states in parallel. + await asyncio.gather( + *(_async_reproduce_state(hass, state, context) for state in states) + ) diff --git a/homeassistant/components/cover/strings.json b/homeassistant/components/cover/strings.json new file mode 100644 index 00000000000..db3ccf9119f --- /dev/null +++ b/homeassistant/components/cover/strings.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "condition_type": { + "is_open": "{entity_name} is open", + "is_closed": "{entity_name} is closed", + "is_opening": "{entity_name} is opening", + "is_closing": "{entity_name} is closing" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cppm_tracker/device_tracker.py b/homeassistant/components/cppm_tracker/device_tracker.py index c1c62a26dd9..1bb723091d4 100644 --- a/homeassistant/components/cppm_tracker/device_tracker.py +++ b/homeassistant/components/cppm_tracker/device_tracker.py @@ -1,15 +1,17 @@ """Support for ClearPass Policy Manager.""" -import logging from datetime import timedelta +import logging +from clearpasspy import ClearPass import voluptuous as vol -import homeassistant.helpers.config_validation as cv + from homeassistant.components.device_tracker import ( + DOMAIN, PLATFORM_SCHEMA, DeviceScanner, - DOMAIN, ) -from homeassistant.const import CONF_HOST, CONF_API_KEY +from homeassistant.const import CONF_API_KEY, CONF_HOST +import homeassistant.helpers.config_validation as cv SCAN_INTERVAL = timedelta(seconds=120) @@ -30,7 +32,6 @@ _LOGGER = logging.getLogger(__name__) def get_scanner(hass, config): """Initialize Scanner.""" - from clearpasspy import ClearPass data = { "server": config[DOMAIN][CONF_HOST], diff --git a/homeassistant/components/cpuspeed/sensor.py b/homeassistant/components/cpuspeed/sensor.py index 9484e770998..53598e24c70 100644 --- a/homeassistant/components/cpuspeed/sensor.py +++ b/homeassistant/components/cpuspeed/sensor.py @@ -1,11 +1,12 @@ """Support for displaying the current CPU speed.""" import logging +from cpuinfo import cpuinfo 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__) @@ -75,7 +76,6 @@ class CpuSpeedSensor(Entity): def update(self): """Get the latest data and updates the state.""" - from cpuinfo import cpuinfo self.info = cpuinfo.get_cpu_info() if HZ_ACTUAL_RAW in self.info: diff --git a/homeassistant/components/crimereports/sensor.py b/homeassistant/components/crimereports/sensor.py index 6295125b7ca..cf5b2e374e2 100644 --- a/homeassistant/components/crimereports/sensor.py +++ b/homeassistant/components/crimereports/sensor.py @@ -3,27 +3,28 @@ from collections import defaultdict from datetime import timedelta import logging +import crimereports import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_INCLUDE, - CONF_EXCLUDE, - CONF_NAME, - CONF_LATITUDE, - CONF_LONGITUDE, ATTR_ATTRIBUTION, ATTR_LATITUDE, ATTR_LONGITUDE, + CONF_EXCLUDE, + CONF_INCLUDE, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_NAME, CONF_RADIUS, LENGTH_KILOMETERS, LENGTH_METERS, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import slugify from homeassistant.util.distance import convert from homeassistant.util.dt import now -import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -65,8 +66,6 @@ class CrimeReportsSensor(Entity): def __init__(self, hass, name, latitude, longitude, radius, include, exclude): """Initialize the Crime Reports sensor.""" - import crimereports - self._hass = hass self._name = name self._include = include @@ -113,8 +112,6 @@ class CrimeReportsSensor(Entity): def update(self): """Update device state.""" - import crimereports - incident_counts = defaultdict(int) incidents = self._crimereports.get_incidents( now().date(), include=self._include, exclude=self._exclude diff --git a/homeassistant/components/cups/sensor.py b/homeassistant/components/cups/sensor.py index f6a5133d8a9..4af51e911a1 100644 --- a/homeassistant/components/cups/sensor.py +++ b/homeassistant/components/cups/sensor.py @@ -276,11 +276,11 @@ class MarkerSensor(Entity): if self._attributes is None: return None - high_level = self._attributes[self._printer]["marker-high-levels"] + high_level = self._attributes[self._printer].get("marker-high-levels") if isinstance(high_level, list): high_level = high_level[self._index] - low_level = self._attributes[self._printer]["marker-low-levels"] + low_level = self._attributes[self._printer].get("marker-low-levels") if isinstance(low_level, list): low_level = low_level[self._index] diff --git a/homeassistant/components/daikin/.translations/ru.json b/homeassistant/components/daikin/.translations/ru.json index 98ab98e6b17..00a517f701f 100644 --- a/homeassistant/components/daikin/.translations/ru.json +++ b/homeassistant/components/daikin/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "device_fail": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430.", "device_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443." }, diff --git a/homeassistant/components/darksky/sensor.py b/homeassistant/components/darksky/sensor.py index d4e7e7ec63a..cd8417e3e84 100644 --- a/homeassistant/components/darksky/sensor.py +++ b/homeassistant/components/darksky/sensor.py @@ -2,6 +2,7 @@ import logging from datetime import timedelta +import forecastio import voluptuous as vol from requests.exceptions import ConnectionError as ConnectError, HTTPError, Timeout @@ -797,8 +798,6 @@ class DarkSkyData: def _update(self): """Get the latest data from Dark Sky.""" - import forecastio - try: self.data = forecastio.load_forecast( self._api_key, diff --git a/homeassistant/components/darksky/weather.py b/homeassistant/components/darksky/weather.py index 5296f346626..41f063399c1 100644 --- a/homeassistant/components/darksky/weather.py +++ b/homeassistant/components/darksky/weather.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +import forecastio from requests.exceptions import ConnectionError as ConnectError, HTTPError, Timeout import voluptuous as vol @@ -102,6 +103,11 @@ class DarkSkyWeather(WeatherEntity): self._ds_hourly = None self._ds_daily = None + @property + def available(self): + """Return if weather data is available from Dark Sky.""" + return self._ds_data is not None + @property def attribution(self): """Return the attribution.""" @@ -215,7 +221,8 @@ class DarkSkyWeather(WeatherEntity): self._dark_sky.update() self._ds_data = self._dark_sky.data - self._ds_currently = self._dark_sky.currently.d + currently = self._dark_sky.currently + self._ds_currently = currently.d if currently else {} self._ds_hourly = self._dark_sky.hourly self._ds_daily = self._dark_sky.daily @@ -238,8 +245,6 @@ class DarkSkyData: @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data from Dark Sky.""" - import forecastio - try: self.data = forecastio.load_forecast( self._api_key, self.latitude, self.longitude, units=self.requested_units diff --git a/homeassistant/components/ddwrt/device_tracker.py b/homeassistant/components/ddwrt/device_tracker.py index 4e661376719..bd2728d03dc 100644 --- a/homeassistant/components/ddwrt/device_tracker.py +++ b/homeassistant/components/ddwrt/device_tracker.py @@ -165,4 +165,4 @@ class DdWrtDeviceScanner(DeviceScanner): def _parse_ddwrt_response(data_str): """Parse the DD-WRT data format.""" - return {key: val for key, val in _DDWRT_DATA_REGEX.findall(data_str)} + return dict(_DDWRT_DATA_REGEX.findall(data_str)) diff --git a/homeassistant/components/deconz/.translations/ca.json b/homeassistant/components/deconz/.translations/ca.json index d36de4acc1e..a2facf0d7c2 100644 --- a/homeassistant/components/deconz/.translations/ca.json +++ b/homeassistant/components/deconz/.translations/ca.json @@ -11,6 +11,7 @@ "error": { "no_key": "No s'ha pogut obtenir una clau API" }, + "flow_title": "Passarel\u00b7la d'enlla\u00e7 deCONZ Zigbee ({host})", "step": { "hassio_confirm": { "data": { diff --git a/homeassistant/components/deconz/.translations/da.json b/homeassistant/components/deconz/.translations/da.json index 6b74c09107a..ec9c4dc35b1 100644 --- a/homeassistant/components/deconz/.translations/da.json +++ b/homeassistant/components/deconz/.translations/da.json @@ -11,6 +11,7 @@ "error": { "no_key": "Kunne ikke f\u00e5 en API-n\u00f8gle" }, + "flow_title": "deCONZ Zigbee gateway ({host})", "step": { "hassio_confirm": { "data": { diff --git a/homeassistant/components/deconz/.translations/de.json b/homeassistant/components/deconz/.translations/de.json index 830ae0fd13f..2bf0667cadb 100644 --- a/homeassistant/components/deconz/.translations/de.json +++ b/homeassistant/components/deconz/.translations/de.json @@ -11,6 +11,7 @@ "error": { "no_key": "Es konnte kein API-Schl\u00fcssel abgerufen werden" }, + "flow_title": "deCONZ Zigbee Gateway", "step": { "hassio_confirm": { "data": { @@ -43,12 +44,32 @@ }, "device_automation": { "trigger_subtype": { + "both_buttons": "Beide Tasten", + "button_1": "Erste Taste", + "button_2": "Zweite Taste", + "button_3": "Dritte Taste", + "button_4": "Vierte Taste", "close": "Schlie\u00dfen", + "dim_down": "Dimmer runter", + "dim_up": "Dimmer hoch", "left": "Links", "open": "Offen", "right": "Rechts", "turn_off": "Ausschalten", "turn_on": "Einschalten" + }, + "trigger_type": { + "remote_button_double_press": "\"{subtype}\" Taste doppelt angeklickt", + "remote_button_long_press": "\"{subtype}\" Taste kontinuierlich gedr\u00fcckt", + "remote_button_long_release": "\"{subtype}\" Taste nach langem Dr\u00fccken losgelassen", + "remote_button_quadruple_press": "\"{subtype}\" Taste vierfach geklickt", + "remote_button_quintuple_press": "\"{subtype}\" Taste f\u00fcnffach geklickt", + "remote_button_rotated": "Button gedreht \"{subtype}\".", + "remote_button_rotation_stopped": "Die Tastendrehung \"{subtype}\" wurde gestoppt", + "remote_button_short_press": "\"{subtype}\" Taste gedr\u00fcckt", + "remote_button_short_release": "\"{subtype}\" Taste losgelassen", + "remote_button_triple_press": "\"{subtype}\" Taste dreimal geklickt", + "remote_gyro_activated": "Ger\u00e4t ersch\u00fcttert" } }, "options": { diff --git a/homeassistant/components/deconz/.translations/en.json b/homeassistant/components/deconz/.translations/en.json index c00bfca3564..e9c64ffe5fa 100644 --- a/homeassistant/components/deconz/.translations/en.json +++ b/homeassistant/components/deconz/.translations/en.json @@ -11,6 +11,7 @@ "error": { "no_key": "Couldn't get an API key" }, + "flow_title": "deCONZ Zigbee gateway ({host})", "step": { "hassio_confirm": { "data": { diff --git a/homeassistant/components/deconz/.translations/es.json b/homeassistant/components/deconz/.translations/es.json index 04a08d185b3..d4f8de9f282 100644 --- a/homeassistant/components/deconz/.translations/es.json +++ b/homeassistant/components/deconz/.translations/es.json @@ -11,6 +11,7 @@ "error": { "no_key": "No se pudo obtener una clave API" }, + "flow_title": "pasarela deCONZ Zigbee ({host})", "step": { "hassio_confirm": { "data": { diff --git a/homeassistant/components/deconz/.translations/fr.json b/homeassistant/components/deconz/.translations/fr.json index 3729f7f556a..d1fc7fa7286 100644 --- a/homeassistant/components/deconz/.translations/fr.json +++ b/homeassistant/components/deconz/.translations/fr.json @@ -11,6 +11,7 @@ "error": { "no_key": "Impossible d'obtenir une cl\u00e9 d'API" }, + "flow_title": "Passerelle deCONZ Zigbee ({host})", "step": { "hassio_confirm": { "data": { diff --git a/homeassistant/components/deconz/.translations/it.json b/homeassistant/components/deconz/.translations/it.json index 1f0b344a32d..975d69a450f 100644 --- a/homeassistant/components/deconz/.translations/it.json +++ b/homeassistant/components/deconz/.translations/it.json @@ -11,6 +11,7 @@ "error": { "no_key": "Impossibile ottenere una API key" }, + "flow_title": "Gateway Zigbee deCONZ ({host})", "step": { "hassio_confirm": { "data": { diff --git a/homeassistant/components/deconz/.translations/ko.json b/homeassistant/components/deconz/.translations/ko.json index 923a2beb2ff..61725316b13 100644 --- a/homeassistant/components/deconz/.translations/ko.json +++ b/homeassistant/components/deconz/.translations/ko.json @@ -11,6 +11,7 @@ "error": { "no_key": "API \ud0a4\ub97c \uac00\uc838\uc62c \uc218 \uc5c6\uc2b5\ub2c8\ub2e4" }, + "flow_title": "deCONZ Zigbee \uac8c\uc774\ud2b8\uc6e8\uc774 ({host})", "step": { "hassio_confirm": { "data": { @@ -64,6 +65,7 @@ "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", diff --git a/homeassistant/components/deconz/.translations/lb.json b/homeassistant/components/deconz/.translations/lb.json index f5f41a28a32..49394eb9773 100644 --- a/homeassistant/components/deconz/.translations/lb.json +++ b/homeassistant/components/deconz/.translations/lb.json @@ -11,6 +11,7 @@ "error": { "no_key": "Konnt keen API Schl\u00ebssel kr\u00e9ien" }, + "flow_title": "deCONZ Zigbee gateway ({host})", "step": { "hassio_confirm": { "data": { diff --git a/homeassistant/components/deconz/.translations/nl.json b/homeassistant/components/deconz/.translations/nl.json index 116f6254b37..7f690f11f1d 100644 --- a/homeassistant/components/deconz/.translations/nl.json +++ b/homeassistant/components/deconz/.translations/nl.json @@ -11,6 +11,7 @@ "error": { "no_key": "Kon geen API-sleutel ophalen" }, + "flow_title": "deCONZ Zigbee gateway ( {host} )", "step": { "hassio_confirm": { "data": { @@ -64,6 +65,7 @@ "remote_button_quadruple_press": "\" {subtype} \" knop viervoudig aangeklikt", "remote_button_quintuple_press": "\" {subtype} \" knop vijf keer aangeklikt", "remote_button_rotated": "Knop gedraaid \" {subtype} \"", + "remote_button_rotation_stopped": "Knoprotatie \" {subtype} \" gestopt", "remote_button_short_press": "\" {subtype} \" knop ingedrukt", "remote_button_short_release": "\"{subtype}\" knop losgelaten", "remote_button_triple_press": "\" {subtype} \" knop driemaal geklikt", diff --git a/homeassistant/components/deconz/.translations/no.json b/homeassistant/components/deconz/.translations/no.json index 7d05a366cf2..7db8f3f118d 100644 --- a/homeassistant/components/deconz/.translations/no.json +++ b/homeassistant/components/deconz/.translations/no.json @@ -11,6 +11,7 @@ "error": { "no_key": "Kunne ikke f\u00e5 en API-n\u00f8kkel" }, + "flow_title": "deCONZ Zigbee gateway ({host})", "step": { "hassio_confirm": { "data": { @@ -64,7 +65,7 @@ "remote_button_quadruple_press": "\"{subtype}\"-knappen ble firedoblet klikket", "remote_button_quintuple_press": "\"{subtype}\"-knappen femdobbelt klikket", "remote_button_rotated": "Knappen roterte \"{subtype}\"", - "remote_button_rotation_stopped": "Knappe rotasjon \"{subtype}\" stoppet", + "remote_button_rotation_stopped": "Knapperotasjon \"{subtype}\" stoppet", "remote_button_short_press": "\"{subtype}\" -knappen ble trykket", "remote_button_short_release": "\"{subtype}\"-knappen sluppet", "remote_button_triple_press": "\"{subtype}\"-knappen trippel klikket", diff --git a/homeassistant/components/deconz/.translations/pl.json b/homeassistant/components/deconz/.translations/pl.json index 11a1beb10d6..ac9f06f1f17 100644 --- a/homeassistant/components/deconz/.translations/pl.json +++ b/homeassistant/components/deconz/.translations/pl.json @@ -11,6 +11,7 @@ "error": { "no_key": "Nie mo\u017cna uzyska\u0107 klucza API" }, + "flow_title": "Bramka deCONZ Zigbee ({host})", "step": { "hassio_confirm": { "data": { @@ -48,26 +49,27 @@ "button_2": "drugi przycisk", "button_3": "trzeci przycisk", "button_4": "czwarty przycisk", - "close": "zamkni\u0119cie", - "dim_down": "zmniejszenie jasno\u015bci", - "dim_up": "zwi\u0119kszenie jasno\u015bci", + "close": "nast\u0105pi zamkni\u0119cie", + "dim_down": "nast\u0105pi zmniejszenie jasno\u015bci", + "dim_up": "nast\u0105pi zwi\u0119kszenie jasno\u015bci", "left": "w lewo", "open": "otwarcie", "right": "w prawo", - "turn_off": "wy\u0142\u0105czenie", - "turn_on": "wy\u0142\u0105czenie" + "turn_off": "nast\u0105pi wy\u0142\u0105czenie", + "turn_on": "nast\u0105pi w\u0142\u0105czenie" }, "trigger_type": { - "remote_button_double_press": "przycisk \"{subtype}\" podw\u00f3jnie naci\u015bni\u0119ty", - "remote_button_long_press": "przycisk \"{subtype}\" naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y", - "remote_button_long_release": "przycisk \"{subtype}\" zwolniony po d\u0142ugim naci\u015bni\u0119ciu", - "remote_button_quadruple_press": "przycisk \"{subtype}\" czterokrotnie naci\u015bni\u0119ty", - "remote_button_quintuple_press": "przycisk \"{subtype}\" pi\u0119ciokrotnie naci\u015bni\u0119ty", - "remote_button_rotated": "przycisk obr\u00f3cony \"{subtype}\"", - "remote_button_short_press": "przycisk \"{subtype}\" naci\u015bni\u0119ty", - "remote_button_short_release": "przycisk \"{subtype}\" zwolniony", - "remote_button_triple_press": "przycisk \"{subtype}\" trzykrotnie naci\u015bni\u0119ty", - "remote_gyro_activated": "potrz\u0105\u015bni\u0119cie urz\u0105dzeniem" + "remote_button_double_press": "przycisk \"{subtype}\" zostanie podw\u00f3jnie naci\u015bni\u0119ty", + "remote_button_long_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y", + "remote_button_long_release": "przycisk \"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu", + "remote_button_quadruple_press": "przycisk \"{subtype}\" zostanie czterokrotnie naci\u015bni\u0119ty", + "remote_button_quintuple_press": "przycisk \"{subtype}\" zostanie pi\u0119ciokrotnie naci\u015bni\u0119ty", + "remote_button_rotated": "przycisk zostanie obr\u00f3cony \"{subtype}\"", + "remote_button_rotation_stopped": "nast\u0105pi zatrzymanie obrotu przycisku \"{subtype}\"", + "remote_button_short_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty", + "remote_button_short_release": "przycisk \"{subtype}\" zostanie zwolniony", + "remote_button_triple_press": "przycisk \"{subtype}\" zostanie trzykrotnie naci\u015bni\u0119ty", + "remote_gyro_activated": "nast\u0105pi potrz\u0105\u015bni\u0119cie urz\u0105dzeniem" } }, "options": { diff --git a/homeassistant/components/deconz/.translations/pt.json b/homeassistant/components/deconz/.translations/pt.json index 47f5bb7db59..63a66595ace 100644 --- a/homeassistant/components/deconz/.translations/pt.json +++ b/homeassistant/components/deconz/.translations/pt.json @@ -29,5 +29,10 @@ } }, "title": "Gateway Zigbee deCONZ" + }, + "device_automation": { + "trigger_subtype": { + "left": "Esquerda" + } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/ru.json b/homeassistant/components/deconz/.translations/ru.json index 558fd9e5897..2dc3df17aa9 100644 --- a/homeassistant/components/deconz/.translations/ru.json +++ b/homeassistant/components/deconz/.translations/ru.json @@ -1,16 +1,17 @@ { "config": { "abort": { - "already_configured": "\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.", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\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 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u043d\u0430\u0447\u0430\u0442\u0430.", - "no_bridges": "\u0428\u043b\u044e\u0437\u044b deCONZ \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b", - "not_deconz_bridge": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0448\u043b\u044e\u0437\u043e\u043c deCONZ", - "one_instance_only": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440 deCONZ", - "updated_instance": "\u0410\u0434\u0440\u0435\u0441 \u0445\u043e\u0441\u0442\u0430 deCONZ \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d" + "no_bridges": "\u0428\u043b\u044e\u0437\u044b deCONZ \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b.", + "not_deconz_bridge": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0448\u043b\u044e\u0437\u043e\u043c deCONZ.", + "one_instance_only": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440 deCONZ.", + "updated_instance": "\u0410\u0434\u0440\u0435\u0441 \u0445\u043e\u0441\u0442\u0430 deCONZ \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d." }, "error": { - "no_key": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043a\u043b\u044e\u0447 API" + "no_key": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043a\u043b\u044e\u0447 API." }, + "flow_title": "\u0428\u043b\u044e\u0437 Zigbee deCONZ ({host})", "step": { "hassio_confirm": { "data": { @@ -28,7 +29,7 @@ "title": "deCONZ" }, "link": { - "description": "\u0420\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u0443\u0439\u0442\u0435 \u0448\u043b\u044e\u0437 deCONZ \u0434\u043b\u044f \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u0432 Home Assistant:\n\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043a \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u043c \u0441\u0438\u0441\u0442\u0435\u043c\u044b deCONZ -> Gateway -> Advanced\n2. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u00abAuthenticate app\u00bb", + "description": "\u0420\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u0443\u0439\u0442\u0435 \u0448\u043b\u044e\u0437 deCONZ \u0434\u043b\u044f \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u0432 Home Assistant:\n\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043a \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u043c \u0441\u0438\u0441\u0442\u0435\u043c\u044b deCONZ -> Gateway -> Advanced.\n2. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u00abAuthenticate app\u00bb.", "title": "\u0421\u0432\u044f\u0437\u044c \u0441 deCONZ" }, "options": { @@ -48,26 +49,27 @@ "button_2": "\u0412\u0442\u043e\u0440\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", "button_3": "\u0422\u0440\u0435\u0442\u044c\u044f \u043a\u043d\u043e\u043f\u043a\u0430", "button_4": "\u0427\u0435\u0442\u0432\u0435\u0440\u0442\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", - "close": "\u0417\u0430\u043a\u0440\u044b\u0442\u043e", - "dim_down": "\u0423\u0431\u0430\u0432\u0438\u0442\u044c \u044f\u0440\u043a\u043e\u0441\u0442\u044c", - "dim_up": "\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u044f\u0440\u043a\u043e\u0441\u0442\u044c", + "close": "\u0417\u0430\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", + "dim_down": "\u042f\u0440\u043a\u043e\u0441\u0442\u044c \u0443\u043c\u0435\u043d\u044c\u0448\u0430\u0435\u0442\u0441\u044f", + "dim_up": "\u042f\u0440\u043a\u043e\u0441\u0442\u044c \u0443\u0432\u0435\u043b\u0438\u0447\u0438\u0432\u0430\u0435\u0442\u0441\u044f", "left": "\u041d\u0430\u043b\u0435\u0432\u043e", - "open": "\u041e\u0442\u043a\u0440\u044b\u0442\u043e", + "open": "\u041e\u0442\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", "right": "\u041d\u0430\u043f\u0440\u0430\u0432\u043e", - "turn_off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", - "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + "turn_off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f", + "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f" }, "trigger_type": { - "remote_button_double_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0434\u0432\u0430\u0436\u0434\u044b", + "remote_button_double_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0434\u0432\u0430 \u0440\u0430\u0437\u0430", "remote_button_long_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0435\u043f\u0440\u0435\u0440\u044b\u0432\u043d\u043e \u043d\u0430\u0436\u0430\u0442\u0430", - "remote_button_long_release": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u0434\u043b\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", + "remote_button_long_release": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u043d\u0435\u043f\u0440\u0435\u0440\u044b\u0432\u043d\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", "remote_button_quadruple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0447\u0435\u0442\u044b\u0440\u0435 \u0440\u0430\u0437\u0430", "remote_button_quintuple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u043f\u044f\u0442\u044c \u0440\u0430\u0437", "remote_button_rotated": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043f\u043e\u0432\u0451\u0440\u043d\u0443\u0442\u0430", + "remote_button_rotation_stopped": "\u041f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442\u0441\u044f \u0432\u0440\u0430\u0449\u0435\u043d\u0438\u0435 \u043a\u043d\u043e\u043f\u043a\u0438 \"{subtype}\"", "remote_button_short_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430", "remote_button_short_release": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430", - "remote_button_triple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0442\u0440\u0438\u0436\u0434\u044b", - "remote_gyro_activated": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u043e\u0442\u0440\u044f\u0441\u043b\u0438" + "remote_button_triple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0442\u0440\u0438 \u0440\u0430\u0437\u0430", + "remote_gyro_activated": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0432\u0441\u0442\u0440\u044f\u0445\u043d\u0443\u043b\u0438" } }, "options": { diff --git a/homeassistant/components/deconz/.translations/sl.json b/homeassistant/components/deconz/.translations/sl.json index 0717bcfc39f..217007c07d4 100644 --- a/homeassistant/components/deconz/.translations/sl.json +++ b/homeassistant/components/deconz/.translations/sl.json @@ -11,6 +11,7 @@ "error": { "no_key": "Klju\u010da API ni mogo\u010de dobiti" }, + "flow_title": "deCONZ Zigbee prehod ({host})", "step": { "hassio_confirm": { "data": { diff --git a/homeassistant/components/deconz/.translations/zh-Hant.json b/homeassistant/components/deconz/.translations/zh-Hant.json index 2ad613cde68..975600a5745 100644 --- a/homeassistant/components/deconz/.translations/zh-Hant.json +++ b/homeassistant/components/deconz/.translations/zh-Hant.json @@ -11,6 +11,7 @@ "error": { "no_key": "\u7121\u6cd5\u53d6\u5f97 API key" }, + "flow_title": "deCONZ Zigbee \u9598\u9053\u5668\uff08{host}\uff09", "step": { "hassio_confirm": { "data": { diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index 66df687047f..5ede8e715b9 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -187,8 +187,9 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ): return self.async_abort(reason="already_in_progress") - # pylint: disable=unsupported-assignment-operation + # 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.deconz_config = { CONF_HOST: discovery_info[CONF_HOST], diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index badbe8b8651..2d097d30c0b 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -204,8 +204,10 @@ def _get_deconz_event_from_device_id(hass, device_id): return None -async def async_attach_trigger(hass, config, action, automation_info): - """Listen for state changes based on configuration.""" +async def async_validate_trigger_config(hass, config): + """Validate config.""" + config = TRIGGER_SCHEMA(config) + device_registry = await hass.helpers.device_registry.async_get_registry() device = device_registry.async_get(config[CONF_DEVICE_ID]) @@ -214,6 +216,16 @@ async def async_attach_trigger(hass, config, action, automation_info): if device.model not in REMOTES or trigger not in REMOTES[device.model]: raise InvalidDeviceAutomationConfig + return config + + +async def async_attach_trigger(hass, config, action, automation_info): + """Listen for state changes based on configuration.""" + device_registry = await hass.helpers.device_registry.async_get_registry() + device = device_registry.async_get(config[CONF_DEVICE_ID]) + + trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) + trigger = REMOTES[device.model][trigger] deconz_event = _get_deconz_event_from_device_id(hass, device.id) @@ -222,13 +234,15 @@ async def async_attach_trigger(hass, config, action, automation_info): event_id = deconz_event.serial - state_config = { + event_config = { + event.CONF_PLATFORM: "event", event.CONF_EVENT_TYPE: CONF_DECONZ_EVENT, event.CONF_EVENT_DATA: {CONF_UNIQUE_ID: event_id, CONF_EVENT: trigger}, } + event_config = event.TRIGGER_SCHEMA(event_config) return await event.async_attach_trigger( - hass, state_config, action, automation_info, platform_type="device" + hass, event_config, action, automation_info, platform_type="device" ) diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index 1e5cd414425..63ab17d001a 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==63" + "pydeconz==64" ], "ssdp": { "manufacturer": [ diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json index db43c022822..3571a9e1207 100644 --- a/homeassistant/components/deconz/strings.json +++ b/homeassistant/components/deconz/strings.json @@ -1,6 +1,7 @@ { "config": { "title": "deCONZ Zigbee gateway", + "flow_title": "deCONZ Zigbee gateway ({host})", "step": { "init": { "title": "Define deCONZ gateway", diff --git a/homeassistant/components/decora/light.py b/homeassistant/components/decora/light.py index cad98f9d8a4..6ca427f2476 100644 --- a/homeassistant/components/decora/light.py +++ b/homeassistant/components/decora/light.py @@ -1,30 +1,49 @@ """Support for Decora dimmers.""" -import importlib -import logging +import copy from functools import wraps +import logging import time +from bluepy.btle import BTLEException # pylint: disable=import-error, no-member +import decora # pylint: disable=import-error, no-member import voluptuous as vol -from homeassistant.const import CONF_API_KEY, CONF_DEVICES, CONF_NAME from homeassistant.components.light import ( ATTR_BRIGHTNESS, + PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, Light, - PLATFORM_SCHEMA, ) +from homeassistant.const import CONF_API_KEY, CONF_DEVICES, CONF_NAME import homeassistant.helpers.config_validation as cv +import homeassistant.util as util _LOGGER = logging.getLogger(__name__) SUPPORT_DECORA_LED = SUPPORT_BRIGHTNESS + +def _name_validator(config): + """Validate the name.""" + config = copy.deepcopy(config) + for address, device_config in config[CONF_DEVICES].items(): + if CONF_NAME not in device_config: + device_config[CONF_NAME] = util.slugify(address) + + return config + + DEVICE_SCHEMA = vol.Schema( {vol.Optional(CONF_NAME): cv.string, vol.Required(CONF_API_KEY): cv.string} ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Optional(CONF_DEVICES, default={}): {cv.string: DEVICE_SCHEMA}} +PLATFORM_SCHEMA = vol.Schema( + vol.All( + PLATFORM_SCHEMA.extend( + {vol.Optional(CONF_DEVICES, default={}): {cv.string: DEVICE_SCHEMA}} + ), + _name_validator, + ) ) @@ -34,9 +53,6 @@ def retry(method): @wraps(method) def wrapper_retry(device, *args, **kwargs): """Try send command and retry on error.""" - # pylint: disable=import-error, no-member - import decora - import bluepy initial = time.monotonic() while True: @@ -44,7 +60,7 @@ def retry(method): return None try: return method(device, *args, **kwargs) - except (decora.decoraException, AttributeError, bluepy.btle.BTLEException): + except (decora.decoraException, AttributeError, BTLEException): _LOGGER.warning( "Decora connect error for device %s. " "Reconnecting...", device.name, @@ -74,8 +90,6 @@ class DecoraLight(Light): def __init__(self, device): """Initialize the light.""" - # pylint: disable=no-member - decora = importlib.import_module("decora") self._name = device["name"] self._address = device["address"] diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index 51fc890c873..1725b2d105c 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -3,9 +3,10 @@ from collections import namedtuple import logging +import denonavr 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_MUSIC, @@ -88,7 +89,6 @@ NewHost = namedtuple("NewHost", ["host", "name"]) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Denon platform.""" - import denonavr # Initialize list with receivers to be started receivers = [] diff --git a/homeassistant/components/deutsche_bahn/sensor.py b/homeassistant/components/deutsche_bahn/sensor.py index fbe0efa15ac..ad7b40f78db 100644 --- a/homeassistant/components/deutsche_bahn/sensor.py +++ b/homeassistant/components/deutsche_bahn/sensor.py @@ -4,6 +4,8 @@ import logging import voluptuous as vol +import schiene + from homeassistant.components.sensor import PLATFORM_SCHEMA import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -89,7 +91,6 @@ class SchieneData: def __init__(self, start, goal, offset, only_direct): """Initialize the sensor.""" - import schiene self.start = start self.goal = goal diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index fa6deac40ba..80e64033295 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -59,6 +59,12 @@ async def async_setup(hass, config): hass.components.websocket_api.async_register_command( websocket_device_automation_list_triggers ) + hass.components.websocket_api.async_register_command( + websocket_device_automation_get_action_capabilities + ) + hass.components.websocket_api.async_register_command( + websocket_device_automation_get_condition_capabilities + ) hass.components.websocket_api.async_register_command( websocket_device_automation_get_trigger_capabilities ) @@ -150,7 +156,11 @@ async def _async_get_device_automation_capabilities(hass, automation_type, autom # The device automation has no capabilities return {} - capabilities = await getattr(platform, function_name)(hass, automation) + try: + capabilities = await getattr(platform, function_name)(hass, automation) + except InvalidDeviceAutomationConfig: + return {} + capabilities = capabilities.copy() extra_fields = capabilities.get("extra_fields") @@ -206,6 +216,38 @@ async def websocket_device_automation_list_triggers(hass, connection, msg): connection.send_result(msg["id"], triggers) +@websocket_api.async_response +@websocket_api.websocket_command( + { + vol.Required("type"): "device_automation/action/capabilities", + vol.Required("action"): dict, + } +) +async def websocket_device_automation_get_action_capabilities(hass, connection, msg): + """Handle request for device action capabilities.""" + action = msg["action"] + capabilities = await _async_get_device_automation_capabilities( + hass, "action", action + ) + connection.send_result(msg["id"], capabilities) + + +@websocket_api.async_response +@websocket_api.websocket_command( + { + vol.Required("type"): "device_automation/condition/capabilities", + vol.Required("condition"): dict, + } +) +async def websocket_device_automation_get_condition_capabilities(hass, connection, msg): + """Handle request for device condition capabilities.""" + condition = msg["condition"] + capabilities = await _async_get_device_automation_capabilities( + hass, "condition", condition + ) + connection.send_result(msg["id"], capabilities) + + @websocket_api.async_response @websocket_api.websocket_command( { diff --git a/homeassistant/components/device_automation/toggle_entity.py b/homeassistant/components/device_automation/toggle_entity.py index c9588c1efa7..5f01f4d9d71 100644 --- a/homeassistant/components/device_automation/toggle_entity.py +++ b/homeassistant/components/device_automation/toggle_entity.py @@ -3,7 +3,10 @@ from typing import Any, Dict, List import voluptuous as vol from homeassistant.core import Context, HomeAssistant, CALLBACK_TYPE -from homeassistant.components.automation import state, AutomationActionType +from homeassistant.components.automation import ( + state as state_automation, + AutomationActionType, +) from homeassistant.components.device_automation.const import ( CONF_IS_OFF, CONF_IS_ON, @@ -14,6 +17,7 @@ from homeassistant.components.device_automation.const import ( CONF_TURNED_ON, ) from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_CONDITION, CONF_ENTITY_ID, CONF_FOR, @@ -21,7 +25,7 @@ from homeassistant.const import ( CONF_TYPE, ) from homeassistant.helpers.entity_registry import async_entries_for_device -from homeassistant.helpers import condition, config_validation as cv, service +from homeassistant.helpers import condition, config_validation as cv from homeassistant.helpers.typing import ConfigType, TemplateVarsType from . import TRIGGER_BASE_SCHEMA @@ -80,6 +84,7 @@ CONDITION_SCHEMA = cv.DEVICE_CONDITION_BASE_SCHEMA.extend( { vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_TYPE): vol.In([CONF_IS_OFF, CONF_IS_ON]), + vol.Optional(CONF_FOR): cv.positive_time_period_dict, } ) @@ -108,19 +113,14 @@ async def async_call_action_from_config( else: action = "toggle" - service_action = { - service.CONF_SERVICE: "{}.{}".format(domain, action), - CONF_ENTITY_ID: config[CONF_ENTITY_ID], - } + service_data = {ATTR_ENTITY_ID: config[CONF_ENTITY_ID]} - await service.async_call_from_config( - hass, service_action, blocking=True, variables=variables, context=context + await hass.services.async_call( + domain, action, service_data, blocking=True, context=context ) -def async_condition_from_config( - config: ConfigType, config_validation: bool -) -> condition.ConditionCheckerType: +def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType: """Evaluate state based on configuration.""" condition_type = config[CONF_TYPE] if condition_type == CONF_IS_ON: @@ -132,8 +132,10 @@ def async_condition_from_config( condition.CONF_ENTITY_ID: config[CONF_ENTITY_ID], condition.CONF_STATE: stat, } + if CONF_FOR in config: + state_config[CONF_FOR] = config[CONF_FOR] - return condition.state_from_config(state_config, config_validation) + return condition.state_from_config(state_config) async def async_attach_trigger( @@ -151,14 +153,16 @@ async def async_attach_trigger( from_state = "on" to_state = "off" state_config = { - state.CONF_ENTITY_ID: config[CONF_ENTITY_ID], - state.CONF_FROM: from_state, - state.CONF_TO: to_state, + state_automation.CONF_PLATFORM: "state", + state_automation.CONF_ENTITY_ID: config[CONF_ENTITY_ID], + state_automation.CONF_FROM: from_state, + state_automation.CONF_TO: to_state, } if CONF_FOR in config: state_config[CONF_FOR] = config[CONF_FOR] - return await state.async_attach_trigger( + state_config = state_automation.TRIGGER_SCHEMA(state_config) + return await state_automation.async_attach_trigger( hass, state_config, action, automation_info, platform_type="device" ) @@ -201,7 +205,7 @@ async def async_get_actions( async def async_get_conditions( hass: HomeAssistant, device_id: str, domain: str -) -> List[dict]: +) -> List[Dict[str, str]]: """List device conditions.""" return await _async_get_automations(hass, device_id, ENTITY_CONDITIONS, domain) @@ -213,7 +217,16 @@ async def async_get_triggers( return await _async_get_automations(hass, device_id, ENTITY_TRIGGERS, domain) -async def async_get_trigger_capabilities(hass: HomeAssistant, trigger: dict) -> dict: +async def async_get_condition_capabilities(hass: HomeAssistant, config: dict) -> dict: + """List condition capabilities.""" + return { + "extra_fields": vol.Schema( + {vol.Optional(CONF_FOR): cv.positive_time_period_dict} + ) + } + + +async def async_get_trigger_capabilities(hass: HomeAssistant, config: dict) -> dict: """List trigger capabilities.""" return { "extra_fields": vol.Schema( diff --git a/homeassistant/components/device_tracker/legacy.py b/homeassistant/components/device_tracker/legacy.py index 5c186cc12a1..ad7ff3fe3f5 100644 --- a/homeassistant/components/device_tracker/legacy.py +++ b/homeassistant/components/device_tracker/legacy.py @@ -1,11 +1,12 @@ """Legacy device tracker classes.""" import asyncio from datetime import timedelta +import hashlib from typing import Any, List, Sequence import voluptuous as vol -from homeassistant.core import callback +from homeassistant import util from homeassistant.components import zone from homeassistant.components.group import ( ATTR_ADD_ENTITIES, @@ -16,16 +17,7 @@ from homeassistant.components.group import ( SERVICE_SET, ) from homeassistant.components.zone import async_active_zone -from homeassistant.config import load_yaml_config_file, async_log_exception -from homeassistant.exceptions import HomeAssistantError -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_registry import async_get_registry -from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.typing import GPSType, HomeAssistantType -from homeassistant import util -import homeassistant.util.dt as dt_util -from homeassistant.util.yaml import dump - +from homeassistant.config import async_log_exception, load_yaml_config_file from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_GPS_ACCURACY, @@ -37,9 +29,17 @@ from homeassistant.const import ( CONF_MAC, CONF_NAME, DEVICE_DEFAULT_NAME, - STATE_NOT_HOME, STATE_HOME, + STATE_NOT_HOME, ) +from homeassistant.core import callback +from homeassistant.exceptions import HomeAssistantError +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_registry import async_get_registry +from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.typing import GPSType, HomeAssistantType +import homeassistant.util.dt as dt_util +from homeassistant.util.yaml import dump from .const import ( ATTR_BATTERY, @@ -635,7 +635,6 @@ def get_gravatar_for_email(email: str): Async friendly. """ - import hashlib url = "https://www.gravatar.com/avatar/{}.jpg?s=80&d=wavatar" return url.format(hashlib.md5(email.encode("utf-8").lower()).hexdigest()) diff --git a/homeassistant/components/dht/sensor.py b/homeassistant/components/dht/sensor.py index aadb6b2d4cb..648e0e1ed72 100644 --- a/homeassistant/components/dht/sensor.py +++ b/homeassistant/components/dht/sensor.py @@ -2,6 +2,7 @@ import logging from datetime import timedelta +import Adafruit_DHT # pylint: disable=import-error import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -50,7 +51,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the DHT sensor.""" - import Adafruit_DHT # pylint: disable=import-error SENSOR_TYPES[SENSOR_TEMPERATURE][1] = hass.config.units.temperature_unit available_sensors = { diff --git a/homeassistant/components/digital_ocean/__init__.py b/homeassistant/components/digital_ocean/__init__.py index 18dfb49365a..bdb0c348803 100644 --- a/homeassistant/components/digital_ocean/__init__.py +++ b/homeassistant/components/digital_ocean/__init__.py @@ -2,6 +2,7 @@ import logging from datetime import timedelta +import digitalocean import voluptuous as vol from homeassistant.const import CONF_ACCESS_TOKEN @@ -38,7 +39,6 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): """Set up the Digital Ocean component.""" - import digitalocean conf = config[DOMAIN] access_token = conf.get(CONF_ACCESS_TOKEN) @@ -63,7 +63,6 @@ class DigitalOcean: def __init__(self, access_token): """Initialize the Digital Ocean connection.""" - import digitalocean self._access_token = access_token self.data = None diff --git a/homeassistant/components/digitalloggers/switch.py b/homeassistant/components/digitalloggers/switch.py index 9983ccc93fa..10c8ce73a47 100644 --- a/homeassistant/components/digitalloggers/switch.py +++ b/homeassistant/components/digitalloggers/switch.py @@ -2,6 +2,7 @@ import logging from datetime import timedelta +import dlipower import voluptuous as vol from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA @@ -45,7 +46,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Find and return DIN III Relay switch.""" - import dlipower host = config.get(CONF_HOST) controller_name = config.get(CONF_NAME) diff --git a/homeassistant/components/discogs/sensor.py b/homeassistant/components/discogs/sensor.py index 64528f4ca5e..b3f29fbe75b 100644 --- a/homeassistant/components/discogs/sensor.py +++ b/homeassistant/components/discogs/sensor.py @@ -3,6 +3,7 @@ from datetime import timedelta import logging import random +import discogs_client import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -65,19 +66,18 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Discogs sensor.""" - import discogs_client token = config[CONF_TOKEN] name = config[CONF_NAME] try: - discogs_client = discogs_client.Client(SERVER_SOFTWARE, user_token=token) + _discogs_client = discogs_client.Client(SERVER_SOFTWARE, user_token=token) discogs_data = { - "user": discogs_client.identity().name, - "folders": discogs_client.identity().collection_folders, - "collection_count": discogs_client.identity().num_collection, - "wantlist_count": discogs_client.identity().num_wantlist, + "user": _discogs_client.identity().name, + "folders": _discogs_client.identity().collection_folders, + "collection_count": _discogs_client.identity().num_collection, + "wantlist_count": _discogs_client.identity().num_wantlist, } except discogs_client.exceptions.HTTPError: _LOGGER.error("API token is not valid") diff --git a/homeassistant/components/discord/__init__.py b/homeassistant/components/discord/__init__.py index a3cd87bc895..67b9f1b39ba 100644 --- a/homeassistant/components/discord/__init__.py +++ b/homeassistant/components/discord/__init__.py @@ -1 +1 @@ -"""The discord component.""" +"""The discord integration.""" diff --git a/homeassistant/components/discord/manifest.json b/homeassistant/components/discord/manifest.json index 50c03bad25d..d00d26d2b5e 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.3" + "discord.py==1.2.4" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/discord/notify.py b/homeassistant/components/discord/notify.py index 17ff0a192d0..f35cf5b0ce9 100644 --- a/homeassistant/components/discord/notify.py +++ b/homeassistant/components/discord/notify.py @@ -2,6 +2,7 @@ import logging import os.path +import discord import voluptuous as vol from homeassistant.const import CONF_TOKEN @@ -44,7 +45,6 @@ class DiscordNotificationService(BaseNotificationService): async def async_send_message(self, message, **kwargs): """Login to Discord, send message to channel(s) and log out.""" - import discord discord.VoiceClient.warn_nacl = False discord_bot = discord.Client() diff --git a/homeassistant/components/dlib_face_detect/image_processing.py b/homeassistant/components/dlib_face_detect/image_processing.py index 749e536e2e8..cdd8bc101b5 100644 --- a/homeassistant/components/dlib_face_detect/image_processing.py +++ b/homeassistant/components/dlib_face_detect/image_processing.py @@ -1,17 +1,19 @@ """Component that will help set the Dlib face detect processing.""" -import logging import io +import logging -from homeassistant.core import split_entity_id +import face_recognition # pylint: disable=import-error + +from homeassistant.components.image_processing import ( + CONF_ENTITY_ID, + CONF_NAME, + CONF_SOURCE, + ImageProcessingFaceEntity, +) # pylint: disable=unused-import from homeassistant.components.image_processing import PLATFORM_SCHEMA # noqa -from homeassistant.components.image_processing import ( - ImageProcessingFaceEntity, - CONF_SOURCE, - CONF_ENTITY_ID, - CONF_NAME, -) +from homeassistant.core import split_entity_id _LOGGER = logging.getLogger(__name__) @@ -55,7 +57,6 @@ class DlibFaceDetectEntity(ImageProcessingFaceEntity): def process_image(self, image): """Process image.""" - import face_recognition # pylint: disable=import-error fak_file = io.BytesIO(image) fak_file.name = "snapshot.jpg" diff --git a/homeassistant/components/dlib_face_identify/image_processing.py b/homeassistant/components/dlib_face_identify/image_processing.py index d4851be28c8..d5b55b6a68c 100644 --- a/homeassistant/components/dlib_face_identify/image_processing.py +++ b/homeassistant/components/dlib_face_identify/image_processing.py @@ -2,6 +2,8 @@ import logging import io +# pylint: disable=import-error +import face_recognition import voluptuous as vol from homeassistant.core import split_entity_id @@ -49,8 +51,6 @@ class DlibFaceIdentifyEntity(ImageProcessingFaceEntity): def __init__(self, camera_entity, faces, name, tolerance): """Initialize Dlib face identify entry.""" - # pylint: disable=import-error - import face_recognition super().__init__() @@ -83,8 +83,6 @@ class DlibFaceIdentifyEntity(ImageProcessingFaceEntity): def process_image(self, image): """Process image.""" - # pylint: disable=import-error - import face_recognition fak_file = io.BytesIO(image) fak_file.name = "snapshot.jpg" diff --git a/homeassistant/components/dnsip/sensor.py b/homeassistant/components/dnsip/sensor.py index 0053d5a95ea..fb57040f2c2 100644 --- a/homeassistant/components/dnsip/sensor.py +++ b/homeassistant/components/dnsip/sensor.py @@ -2,6 +2,8 @@ from datetime import timedelta import logging +import aiodns +from aiodns.error import DNSError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -58,7 +60,6 @@ class WanIpSensor(Entity): def __init__(self, hass, name, hostname, resolver, ipv6): """Initialize the DNS IP sensor.""" - import aiodns self.hass = hass self._name = name @@ -80,7 +81,6 @@ class WanIpSensor(Entity): async def async_update(self): """Get the current DNS IP address for hostname.""" - from aiodns.error import DNSError try: response = await self.resolver.query(self.hostname, self.querytype) diff --git a/homeassistant/components/doods/image_processing.py b/homeassistant/components/doods/image_processing.py index 3eec85b3e53..02d7ce26f1c 100644 --- a/homeassistant/components/doods/image_processing.py +++ b/homeassistant/components/doods/image_processing.py @@ -7,6 +7,7 @@ import voluptuous as vol from PIL import Image, ImageDraw from pydoods import PyDOODS +from homeassistant.const import CONF_TIMEOUT from homeassistant.components.image_processing import ( CONF_CONFIDENCE, CONF_ENTITY_ID, @@ -31,6 +32,7 @@ CONF_AUTH_KEY = "auth_key" CONF_DETECTOR = "detector" CONF_LABELS = "labels" CONF_AREA = "area" +CONF_COVERS = "covers" CONF_TOP = "top" CONF_BOTTOM = "bottom" CONF_RIGHT = "right" @@ -43,6 +45,7 @@ AREA_SCHEMA = vol.Schema( vol.Optional(CONF_LEFT, default=0): cv.small_float, vol.Optional(CONF_RIGHT, default=1): cv.small_float, vol.Optional(CONF_TOP, default=0): cv.small_float, + vol.Optional(CONF_COVERS, default=True): cv.boolean, } ) @@ -50,7 +53,7 @@ LABEL_SCHEMA = vol.Schema( { vol.Required(CONF_NAME): cv.string, vol.Optional(CONF_AREA): AREA_SCHEMA, - vol.Optional(CONF_CONFIDENCE, default=0.0): vol.Range(min=0, max=100), + vol.Optional(CONF_CONFIDENCE): vol.Range(min=0, max=100), } ) @@ -58,6 +61,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_URL): cv.string, vol.Required(CONF_DETECTOR): cv.string, + vol.Required(CONF_TIMEOUT, default=90): cv.positive_int, vol.Optional(CONF_AUTH_KEY, default=""): cv.string, vol.Optional(CONF_FILE_OUT, default=[]): vol.All(cv.ensure_list, [cv.template]), vol.Optional(CONF_CONFIDENCE, default=0.0): vol.Range(min=0, max=100), @@ -74,8 +78,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): url = config[CONF_URL] auth_key = config[CONF_AUTH_KEY] detector_name = config[CONF_DETECTOR] + timeout = config[CONF_TIMEOUT] - doods = PyDOODS(url, auth_key) + doods = PyDOODS(url, auth_key, timeout) response = doods.get_detectors() if not isinstance(response, dict): _LOGGER.warning("Could not connect to doods server: %s", url) @@ -140,6 +145,7 @@ class Doods(ImageProcessingEntity): # handle labels and specific detection areas labels = config[CONF_LABELS] self._label_areas = {} + self._label_covers = {} for label in labels: if isinstance(label, dict): label_name = label[CONF_NAME] @@ -147,14 +153,17 @@ class Doods(ImageProcessingEntity): _LOGGER.warning("Detector does not support label %s", label_name) continue - # Label Confidence - label_confidence = label[CONF_CONFIDENCE] + # If label confidence is not specified, use global confidence + label_confidence = label.get(CONF_CONFIDENCE) + if not label_confidence: + label_confidence = confidence if label_name not in dconfig or dconfig[label_name] > label_confidence: dconfig[label_name] = label_confidence # Label area label_area = label.get(CONF_AREA) self._label_areas[label_name] = [0, 0, 1, 1] + self._label_covers[label_name] = True if label_area: self._label_areas[label_name] = [ label_area[CONF_TOP], @@ -162,6 +171,7 @@ class Doods(ImageProcessingEntity): label_area[CONF_BOTTOM], label_area[CONF_RIGHT], ] + self._label_covers[label_name] = label_area[CONF_COVERS] else: if label not in detector["labels"] and label != "*": _LOGGER.warning("Detector does not support label %s", label) @@ -175,6 +185,7 @@ class Doods(ImageProcessingEntity): # Handle global detection area self._area = [0, 0, 1, 1] + self._covers = True area_config = config.get(CONF_AREA) if area_config: self._area = [ @@ -183,6 +194,7 @@ class Doods(ImageProcessingEntity): area_config[CONF_BOTTOM], area_config[CONF_RIGHT], ] + self._covers = area_config[CONF_COVERS] template.attach(hass, self._file_out) @@ -308,22 +320,41 @@ class Doods(ImageProcessingEntity): continue # Exclude matches outside global area definition - if ( - boxes[0] < self._area[0] - or boxes[1] < self._area[1] - or boxes[2] > self._area[2] - or boxes[3] > self._area[3] - ): - continue + if self._covers: + if ( + boxes[0] < self._area[0] + or boxes[1] < self._area[1] + or boxes[2] > self._area[2] + or boxes[3] > self._area[3] + ): + continue + else: + if ( + boxes[0] > self._area[2] + or boxes[1] > self._area[3] + or boxes[2] < self._area[0] + or boxes[3] < self._area[1] + ): + continue # Exclude matches outside label specific area definition - if self._label_areas and ( - boxes[0] < self._label_areas[label][0] - or boxes[1] < self._label_areas[label][1] - or boxes[2] > self._label_areas[label][2] - or boxes[3] > self._label_areas[label][3] - ): - continue + if self._label_areas.get(label): + if self._label_covers[label]: + if ( + boxes[0] < self._label_areas[label][0] + or boxes[1] < self._label_areas[label][1] + or boxes[2] > self._label_areas[label][2] + or boxes[3] > self._label_areas[label][3] + ): + continue + else: + if ( + boxes[0] > self._label_areas[label][2] + or boxes[1] > self._label_areas[label][3] + or boxes[2] < self._label_areas[label][0] + or boxes[3] < self._label_areas[label][1] + ): + continue if label not in matches: matches[label] = [] diff --git a/homeassistant/components/dovado/__init__.py b/homeassistant/components/dovado/__init__.py index 29f0cc59392..a13c49cc61a 100644 --- a/homeassistant/components/dovado/__init__.py +++ b/homeassistant/components/dovado/__init__.py @@ -2,6 +2,7 @@ import logging from datetime import timedelta +import dovado import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -32,7 +33,6 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) def setup(hass, config): """Set up the Dovado component.""" - import dovado hass.data[DOMAIN] = DovadoData( dovado.Dovado( diff --git a/homeassistant/components/downloader/services.yaml b/homeassistant/components/downloader/services.yaml index e69de29bb2d..d16b2788c70 100644 --- a/homeassistant/components/downloader/services.yaml +++ b/homeassistant/components/downloader/services.yaml @@ -0,0 +1,15 @@ +download_file: + description: Downloads a file to the download location. + fields: + url: + description: The URL of the file to download. + example: 'http://example.org/myfile' + subdir: + description: Download into subdirectory. + example: 'download_dir' + filename: + description: Determine the filename. + example: 'my_file_name' + overwrite: + description: Whether to overwrite the file or not. + example: 'false' \ No newline at end of file diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 82a81118dbd..253e8409f1b 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -4,6 +4,9 @@ from datetime import timedelta from functools import partial import logging +from dsmr_parser import obis_references as obis_ref +from dsmr_parser.clients.protocol import create_dsmr_reader, create_tcp_dsmr_reader +import serial import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -52,10 +55,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= # Suppress logging logging.getLogger("dsmr_parser").setLevel(logging.ERROR) - from dsmr_parser import obis_references as obis_ref - from dsmr_parser.clients.protocol import create_dsmr_reader, create_tcp_dsmr_reader - import serial - dsmr_version = config[CONF_DSMR_VERSION] # Define list of name,obis mappings to generate entities @@ -212,11 +211,9 @@ class DSMREntity(Entity): @property def state(self): """Return the state of sensor, if available, translate if needed.""" - from dsmr_parser import obis_references as obis - value = self.get_dsmr_object_attr("value") - if self._obis == obis.ELECTRICITY_ACTIVE_TARIFF: + if self._obis == obis_ref.ELECTRICITY_ACTIVE_TARIFF: return self.translate_tariff(value) try: diff --git a/homeassistant/components/dte_energy_bridge/sensor.py b/homeassistant/components/dte_energy_bridge/sensor.py index b904d004c61..aa822da0d6a 100644 --- a/homeassistant/components/dte_energy_bridge/sensor.py +++ b/homeassistant/components/dte_energy_bridge/sensor.py @@ -1,12 +1,13 @@ """Support for monitoring energy usage using the DTE energy bridge.""" import logging +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_NAME +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -78,8 +79,6 @@ class DteEnergyBridgeSensor(Entity): def update(self): """Get the energy usage data from the DTE energy bridge.""" - import requests - try: response = requests.get(self._url, timeout=5) except (requests.exceptions.RequestException, ValueError): diff --git a/homeassistant/components/dweet/__init__.py b/homeassistant/components/dweet/__init__.py index bf1298479c3..db985e57a41 100644 --- a/homeassistant/components/dweet/__init__.py +++ b/homeassistant/components/dweet/__init__.py @@ -1,7 +1,8 @@ """Support for sending data to Dweet.io.""" -import logging from datetime import timedelta +import logging +import dweepy import voluptuous as vol from homeassistant.const import ( @@ -10,8 +11,8 @@ from homeassistant.const import ( EVENT_STATE_CHANGED, STATE_UNKNOWN, ) -import homeassistant.helpers.config_validation as cv from homeassistant.helpers import state as state_helper +import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -69,8 +70,6 @@ def setup(hass, config): @Throttle(MIN_TIME_BETWEEN_UPDATES) def send_data(name, msg): """Send the collected data to Dweet.io.""" - import dweepy - try: dweepy.dweet_for(name, msg) except dweepy.DweepyError: diff --git a/homeassistant/components/dweet/sensor.py b/homeassistant/components/dweet/sensor.py index 937de9b030a..f3f604ff369 100644 --- a/homeassistant/components/dweet/sensor.py +++ b/homeassistant/components/dweet/sensor.py @@ -1,18 +1,19 @@ """Support for showing values from Dweet.io.""" +from datetime import timedelta import json import logging -from datetime import timedelta +import dweepy 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_VALUE_TEMPLATE, - CONF_UNIT_OF_MEASUREMENT, CONF_DEVICE, + 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__) @@ -33,8 +34,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Dweet sensor.""" - import dweepy - name = config.get(CONF_NAME) device = config.get(CONF_DEVICE) value_template = config.get(CONF_VALUE_TEMPLATE) @@ -107,8 +106,6 @@ class DweetData: def update(self): """Get the latest data from Dweet.io.""" - import dweepy - try: self.data = dweepy.get_latest_dweet_for(self._device) except dweepy.DweepyError: diff --git a/homeassistant/components/ebusd/__init__.py b/homeassistant/components/ebusd/__init__.py index e11de446e40..e4d0bdbcdb1 100644 --- a/homeassistant/components/ebusd/__init__.py +++ b/homeassistant/components/ebusd/__init__.py @@ -3,13 +3,14 @@ from datetime import timedelta import logging import socket +import ebusdpy import voluptuous as vol from homeassistant.const import ( - CONF_NAME, CONF_HOST, - CONF_PORT, CONF_MONITORED_CONDITIONS, + CONF_NAME, + CONF_PORT, ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform @@ -66,7 +67,6 @@ def setup(hass, config): try: _LOGGER.debug("Ebusd integration setup started") - import ebusdpy ebusdpy.init(server_address) hass.data[DOMAIN] = EbusdData(server_address, circuit) @@ -98,8 +98,6 @@ class EbusdData: @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self, name, stype): """Call the Ebusd API to update the data.""" - import ebusdpy - try: _LOGGER.debug("Opening socket to ebusd %s", name) command_result = ebusdpy.read( @@ -116,8 +114,6 @@ class EbusdData: def write(self, call): """Call write methon on ebusd.""" - import ebusdpy - name = call.data.get("name") value = call.data.get("value") diff --git a/homeassistant/components/ebusd/const.py b/homeassistant/components/ebusd/const.py index db79d81736e..ec097a153c9 100644 --- a/homeassistant/components/ebusd/const.py +++ b/homeassistant/components/ebusd/const.py @@ -1,5 +1,5 @@ """Constants for ebus component.""" -from homeassistant.const import ENERGY_KILO_WATT_HOUR, TEMP_CELSIUS, PRESSURE_BAR +from homeassistant.const import ENERGY_KILO_WATT_HOUR, PRESSURE_BAR, TEMP_CELSIUS DOMAIN = "ebusd" TIME_SECONDS = "seconds" diff --git a/homeassistant/components/ebusd/sensor.py b/homeassistant/components/ebusd/sensor.py index 4bc79e7bd39..63f72a89ccd 100644 --- a/homeassistant/components/ebusd/sensor.py +++ b/homeassistant/components/ebusd/sensor.py @@ -1,6 +1,6 @@ """Support for Ebusd sensors.""" -import logging import datetime +import logging from homeassistant.helpers.entity import Entity import homeassistant.util.dt as dt_util diff --git a/homeassistant/components/ecobee/.translations/de.json b/homeassistant/components/ecobee/.translations/de.json index 1959f769d3a..33d493f6db0 100644 --- a/homeassistant/components/ecobee/.translations/de.json +++ b/homeassistant/components/ecobee/.translations/de.json @@ -1,11 +1,25 @@ { "config": { + "abort": { + "one_instance_only": "Diese Integration unterst\u00fctzt derzeit nur eine Ecobee-Instanz." + }, + "error": { + "pin_request_failed": "Fehler beim Anfordern der PIN von ecobee; Bitte \u00fcberpr\u00fcfe, ob der API-Schl\u00fcssel korrekt ist.", + "token_request_failed": "Fehler beim Anfordern eines Token von ecobee; Bitte versuche es erneut." + }, "step": { + "authorize": { + "description": "Bitte autorisiere diese App unter https://www.ecobee.com/consumerportal/index.html mit Pincode:\n\n{pin}\n\nDr\u00fccke dann auf Senden.", + "title": "App auf ecobee.com autorisieren" + }, "user": { "data": { "api_key": "API Key" - } + }, + "description": "Bitte geben Sie den von ecobee.com erhaltenen API-Schl\u00fcssel ein.", + "title": "ecobee API-Schl\u00fcssel" } - } + }, + "title": "ecobee" } } \ No newline at end of file diff --git a/homeassistant/components/ecobee/.translations/fr.json b/homeassistant/components/ecobee/.translations/fr.json index 85da5b3a4ec..7f308fdf3a3 100644 --- a/homeassistant/components/ecobee/.translations/fr.json +++ b/homeassistant/components/ecobee/.translations/fr.json @@ -9,6 +9,7 @@ }, "step": { "authorize": { + "description": "Veuillez autoriser cette application \u00e0 https://www.ecobee.com/consumerportal/index.html avec un code PIN :\n\n{pin}\n\nEnsuite, appuyez sur Soumettre.", "title": "Autoriser l'application sur ecobee.com" }, "user": { diff --git a/homeassistant/components/ecobee/.translations/ko.json b/homeassistant/components/ecobee/.translations/ko.json new file mode 100644 index 00000000000..2fea66a9d38 --- /dev/null +++ b/homeassistant/components/ecobee/.translations/ko.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "one_instance_only": "\uc774 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub294 \ud604\uc7ac \ud558\ub098\uc758 ecobee \uc778\uc2a4\ud134\uc2a4\ub9cc \uc9c0\uc6d0\ud569\ub2c8\ub2e4." + }, + "error": { + "pin_request_failed": "ecobee \ub85c\ubd80\ud130 PIN \uc694\uccad\uc5d0 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4; API \ud0a4\uac00 \uc62c\ubc14\ub978\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694.", + "token_request_failed": "ecobee \ub85c\ubd80\ud130 \ud1a0\ud070 \uc694\uccad\uc5d0 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4; \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694." + }, + "step": { + "authorize": { + "description": "https://www.ecobee.com/consumerportal/index.html \uc5d0\uc11c PIN \ucf54\ub4dc\ub97c \uc0ac\uc6a9\ud558\uc5ec \uc774 \uc571\uc744 \uc2b9\uc778\ud574\uc8fc\uc138\uc694:\n\n {pin} \n \n \uadf8\ub7f0 \ub2e4\uc74c Submit \uc744 \ub20c\ub7ec\uc8fc\uc138\uc694.", + "title": "ecobee.com \uc5d0\uc11c \uc571 \uc2b9\uc778\ud558\uae30" + }, + "user": { + "data": { + "api_key": "API \ud0a4" + }, + "description": "ecobee.com \uc5d0\uc11c \uc5bb\uc740 API \ud0a4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "title": "ecobee API \ud0a4" + } + }, + "title": "ecobee" + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/.translations/nl.json b/homeassistant/components/ecobee/.translations/nl.json new file mode 100644 index 00000000000..56bb3ace26f --- /dev/null +++ b/homeassistant/components/ecobee/.translations/nl.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "one_instance_only": "Deze integratie ondersteunt momenteel slechts \u00e9\u00e9n ecobee-instantie." + }, + "error": { + "pin_request_failed": "Fout bij het aanvragen van pincode bij ecobee; Controleer of de API-sleutel correct is.", + "token_request_failed": "Fout bij het aanvragen van tokens bij ecobee; probeer het opnieuw." + }, + "step": { + "authorize": { + "description": "Autoriseer deze app op https://www.ecobee.com/consumerportal/index.html met pincode: \n\n {pin} \n \nDruk vervolgens op Submit.", + "title": "Autoriseer app op ecobee.com" + }, + "user": { + "data": { + "api_key": "API-sleutel" + }, + "description": "Voer de API-sleutel in die u van ecobee.com hebt gekregen.", + "title": "ecobee API-sleutel" + } + }, + "title": "ecobee" + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/.translations/no.json b/homeassistant/components/ecobee/.translations/no.json index 2bf141f6489..efaa566c424 100644 --- a/homeassistant/components/ecobee/.translations/no.json +++ b/homeassistant/components/ecobee/.translations/no.json @@ -1,15 +1,15 @@ { "config": { "abort": { - "one_instance_only": "Denne integrasjonen st\u00f8tter forel\u00f8pig bare en ecobee-forekomst." + "one_instance_only": "Denne integrasjonen st\u00f8tter forel\u00f8pig bare \u00e9n ecobee-forekomst." }, "error": { "pin_request_failed": "Feil under foresp\u00f8rsel om PIN-kode fra ecobee. Kontroller at API-n\u00f8kkelen er riktig.", - "token_request_failed": "Feil ved foresp\u00f8rsel om tokener fra ecobee; Pr\u00f8v p\u00e5 nytt." + "token_request_failed": "Feil ved foresp\u00f8rsel om tokener fra ecobee: Pr\u00f8v p\u00e5 nytt." }, "step": { "authorize": { - "description": "Vennligst autoriser denne appen p\u00e5 https://www.ecobee.com/consumerportal/index.html med pin-kode:\n\n{pin}\n\nDeretter, trykk p\u00e5 Send.", + "description": "Vennligst autoriser denne appen p\u00e5 https://www.ecobee.com/consumerportal/index.html med pin-kode:\n\n{pin}\n\nTrykk deretter p\u00e5 Send.", "title": "Autoriser app p\u00e5 ecobee.com" }, "user": { diff --git a/homeassistant/components/ecobee/.translations/pl.json b/homeassistant/components/ecobee/.translations/pl.json index 5c51d86fee4..bd4e7aa1ddc 100644 --- a/homeassistant/components/ecobee/.translations/pl.json +++ b/homeassistant/components/ecobee/.translations/pl.json @@ -5,7 +5,7 @@ }, "error": { "pin_request_failed": "B\u0142\u0105d podczas \u017c\u0105dania kodu PIN od ecobee; sprawd\u017a, czy klucz API jest poprawny.", - "token_request_failed": "B\u0142\u0105d podczas \u017c\u0105dania token\u00f3w od ecobee; prosz\u0119 spr\u00f3buj ponownie." + "token_request_failed": "B\u0142\u0105d podczas \u017c\u0105dania token\u00f3w od ecobee. Spr\u00f3buj ponownie." }, "step": { "authorize": { diff --git a/homeassistant/components/ecobee/.translations/pt.json b/homeassistant/components/ecobee/.translations/pt.json new file mode 100644 index 00000000000..20bba0ede4b --- /dev/null +++ b/homeassistant/components/ecobee/.translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "Chave da API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/binary_sensor.py b/homeassistant/components/ecobee/binary_sensor.py index 375146b96e8..06289572aea 100644 --- a/homeassistant/components/ecobee/binary_sensor.py +++ b/homeassistant/components/ecobee/binary_sensor.py @@ -4,7 +4,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_OCCUPANCY, ) -from .const import DOMAIN +from .const import DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER, _LOGGER async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -53,6 +53,44 @@ class EcobeeBinarySensor(BinarySensorDevice): thermostat = self.data.ecobee.get_thermostat(self.index) return f"{thermostat['identifier']}-{sensor['id']}-{self.device_class}" + @property + def device_info(self): + """Return device information for this sensor.""" + identifier = None + model = None + for sensor in self.data.ecobee.get_remote_sensors(self.index): + if sensor["name"] != self.sensor_name: + continue + if "code" in sensor: + identifier = sensor["code"] + model = "ecobee Room Sensor" + else: + thermostat = self.data.ecobee.get_thermostat(self.index) + identifier = thermostat["identifier"] + try: + model = ( + f"{ECOBEE_MODEL_TO_NAME[thermostat['modelNumber']]} Thermostat" + ) + except KeyError: + _LOGGER.error( + "Model number for ecobee thermostat %s not recognized. " + "Please visit this link and provide the following information: " + "https://github.com/home-assistant/home-assistant/issues/27172 " + "Unrecognized model number: %s", + thermostat["name"], + thermostat["modelNumber"], + ) + break + + if identifier is not None and model is not None: + return { + "identifiers": {(DOMAIN, identifier)}, + "name": self.sensor_name, + "manufacturer": MANUFACTURER, + "model": model, + } + return None + @property def is_on(self): """Return the status of the sensor.""" diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index f930282ba7b..e29e2381008 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -36,7 +36,7 @@ from homeassistant.const import ( from homeassistant.util.temperature import convert import homeassistant.helpers.config_validation as cv -from .const import DOMAIN, _LOGGER +from .const import DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER, _LOGGER from .util import ecobee_date, ecobee_time ATTR_COOL_TEMP = "cool_temp" @@ -264,6 +264,7 @@ class Thermostat(ClimateDevice): self.thermostat = self.data.ecobee.get_thermostat(self.thermostat_index) self._name = self.thermostat["name"] self.vacation = None + self._last_active_hvac_mode = HVAC_MODE_AUTO self._operation_list = [] if self.thermostat["settings"]["heatStages"]: @@ -289,6 +290,8 @@ class Thermostat(ClimateDevice): else: await self.data.update() self.thermostat = self.data.ecobee.get_thermostat(self.thermostat_index) + if self.hvac_mode is not HVAC_MODE_OFF: + self._last_active_hvac_mode = self.hvac_mode @property def available(self): @@ -310,6 +313,29 @@ class Thermostat(ClimateDevice): """Return a unique identifier for this ecobee thermostat.""" return self.thermostat["identifier"] + @property + def device_info(self): + """Return device information for this ecobee thermostat.""" + try: + model = f"{ECOBEE_MODEL_TO_NAME[self.thermostat['modelNumber']]} Thermostat" + except KeyError: + _LOGGER.error( + "Model number for ecobee thermostat %s not recognized. " + "Please visit this link and provide the following information: " + "https://github.com/home-assistant/home-assistant/issues/27172 " + "Unrecognized model number: %s", + self.name, + self.thermostat["modelNumber"], + ) + return None + + return { + "identifiers": {(DOMAIN, self.thermostat["identifier"])}, + "name": self.name, + "manufacturer": MANUFACTURER, + "model": model, + } + @property def temperature_unit(self): """Return the unit of measurement.""" @@ -677,3 +703,12 @@ class Thermostat(ClimateDevice): vacation_name, ) self.data.ecobee.delete_vacation(self.thermostat_index, vacation_name) + + def turn_on(self): + """Set the thermostat to the last active HVAC mode.""" + _LOGGER.debug( + "Turning on ecobee thermostat %s in %s mode", + self.name, + self._last_active_hvac_mode, + ) + self.set_hvac_mode(self._last_active_hvac_mode) diff --git a/homeassistant/components/ecobee/const.py b/homeassistant/components/ecobee/const.py index c3a23099b8a..5022cb71903 100644 --- a/homeassistant/components/ecobee/const.py +++ b/homeassistant/components/ecobee/const.py @@ -9,4 +9,47 @@ DATA_ECOBEE_CONFIG = "ecobee_config" CONF_INDEX = "index" CONF_REFRESH_TOKEN = "refresh_token" +ECOBEE_MODEL_TO_NAME = { + "idtSmart": "ecobee Smart", + "idtEms": "ecobee Smart EMS", + "siSmart": "ecobee Si Smart", + "siEms": "ecobee Si EMS", + "athenaSmart": "ecobee3 Smart", + "athenaEms": "ecobee3 EMS", + "corSmart": "Carrier/Bryant Cor", + "nikeSmart": "ecobee3 lite Smart", + "nikeEms": "ecobee3 lite EMS", + "apolloSmart": "ecobee4 Smart", +} + ECOBEE_PLATFORMS = ["binary_sensor", "climate", "sensor", "weather"] + +MANUFACTURER = "ecobee" + +# Translates ecobee API weatherSymbol to HASS usable names +# https://www.ecobee.com/home/developer/api/documentation/v1/objects/WeatherForecast.shtml +ECOBEE_WEATHER_SYMBOL_TO_HASS = { + 0: "sunny", + 1: "partlycloudy", + 2: "partlycloudy", + 3: "cloudy", + 4: "cloudy", + 5: "cloudy", + 6: "rainy", + 7: "snowy-rainy", + 8: "pouring", + 9: "hail", + 10: "snowy", + 11: "snowy", + 12: "snowy-rainy", + 13: "snowy-heavy", + 14: "hail", + 15: "lightning-rainy", + 16: "windy", + 17: "tornado", + 18: "fog", + 19: "hazy", + 20: "hazy", + 21: "hazy", + -2: None, +} diff --git a/homeassistant/components/ecobee/sensor.py b/homeassistant/components/ecobee/sensor.py index 48a616a1d1f..76945080bfa 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 +from .const import DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER, _LOGGER SENSOR_TYPES = { "temperature": ["Temperature", TEMP_FAHRENHEIT], @@ -64,6 +64,44 @@ class EcobeeSensor(Entity): thermostat = self.data.ecobee.get_thermostat(self.index) return f"{thermostat['identifier']}-{sensor['id']}-{self.device_class}" + @property + def device_info(self): + """Return device information for this sensor.""" + identifier = None + model = None + for sensor in self.data.ecobee.get_remote_sensors(self.index): + if sensor["name"] != self.sensor_name: + continue + if "code" in sensor: + identifier = sensor["code"] + model = "ecobee Room Sensor" + else: + thermostat = self.data.ecobee.get_thermostat(self.index) + identifier = thermostat["identifier"] + try: + model = ( + f"{ECOBEE_MODEL_TO_NAME[thermostat['modelNumber']]} Thermostat" + ) + except KeyError: + _LOGGER.error( + "Model number for ecobee thermostat %s not recognized. " + "Please visit this link and provide the following information: " + "https://github.com/home-assistant/home-assistant/issues/27172 " + "Unrecognized model number: %s", + thermostat["name"], + thermostat["modelNumber"], + ) + break + + if identifier is not None and model is not None: + return { + "identifiers": {(DOMAIN, identifier)}, + "name": self.sensor_name, + "manufacturer": MANUFACTURER, + "model": model, + } + return None + @property def device_class(self): """Return the device class of the sensor.""" diff --git a/homeassistant/components/ecobee/weather.py b/homeassistant/components/ecobee/weather.py index 6175405638e..7b057f09a0c 100644 --- a/homeassistant/components/ecobee/weather.py +++ b/homeassistant/components/ecobee/weather.py @@ -8,17 +8,19 @@ from homeassistant.components.weather import ( ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, + ATTR_FORECAST_WIND_BEARING, ATTR_FORECAST_WIND_SPEED, WeatherEntity, ) from homeassistant.const import TEMP_FAHRENHEIT -from .const import DOMAIN - -ATTR_FORECAST_TEMP_HIGH = "temphigh" -ATTR_FORECAST_PRESSURE = "pressure" -ATTR_FORECAST_VISIBILITY = "visibility" -ATTR_FORECAST_HUMIDITY = "humidity" +from .const import ( + DOMAIN, + ECOBEE_MODEL_TO_NAME, + ECOBEE_WEATHER_SYMBOL_TO_HASS, + MANUFACTURER, + _LOGGER, +) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -66,11 +68,35 @@ class EcobeeWeather(WeatherEntity): """Return a unique identifier for the weather platform.""" return self.data.ecobee.get_thermostat(self._index)["identifier"] + @property + def device_info(self): + """Return device information for the ecobee weather platform.""" + thermostat = self.data.ecobee.get_thermostat(self._index) + try: + model = f"{ECOBEE_MODEL_TO_NAME[thermostat['modelNumber']]} Thermostat" + except KeyError: + _LOGGER.error( + "Model number for ecobee thermostat %s not recognized. " + "Please visit this link and provide the following information: " + "https://github.com/home-assistant/home-assistant/issues/27172 " + "Unrecognized model number: %s", + thermostat["name"], + thermostat["modelNumber"], + ) + return None + + return { + "identifiers": {(DOMAIN, thermostat["identifier"])}, + "name": self.name, + "manufacturer": MANUFACTURER, + "model": model, + } + @property def condition(self): """Return the current condition.""" try: - return self.get_forecast(0, "condition") + return ECOBEE_WEATHER_SYMBOL_TO_HASS[self.get_forecast(0, "weatherSymbol")] except ValueError: return None @@ -107,7 +133,7 @@ class EcobeeWeather(WeatherEntity): def visibility(self): """Return the visibility.""" try: - return int(self.get_forecast(0, "visibility")) + return int(self.get_forecast(0, "visibility")) / 1000 except ValueError: return None @@ -130,45 +156,59 @@ class EcobeeWeather(WeatherEntity): @property def attribution(self): """Return the attribution.""" - if self.weather: - station = self.weather.get("weatherStation", "UNKNOWN") - time = self.weather.get("timestamp", "UNKNOWN") - return f"Ecobee weather provided by {station} at {time}" - return None + if not self.weather: + return None + + station = self.weather.get("weatherStation", "UNKNOWN") + time = self.weather.get("timestamp", "UNKNOWN") + return f"Ecobee weather provided by {station} at {time} UTC" @property def forecast(self): """Return the forecast array.""" - try: - forecasts = [] - for day in self.weather["forecasts"]: - date_time = datetime.strptime( - day["dateTime"], "%Y-%m-%d %H:%M:%S" - ).isoformat() - forecast = { - ATTR_FORECAST_TIME: date_time, - ATTR_FORECAST_CONDITION: day["condition"], - ATTR_FORECAST_TEMP: float(day["tempHigh"]) / 10, - } - if day["tempHigh"] == ECOBEE_STATE_UNKNOWN: - break - if day["tempLow"] != ECOBEE_STATE_UNKNOWN: - forecast[ATTR_FORECAST_TEMP_LOW] = float(day["tempLow"]) / 10 - if day["pressure"] != ECOBEE_STATE_UNKNOWN: - forecast[ATTR_FORECAST_PRESSURE] = int(day["pressure"]) - if day["windSpeed"] != ECOBEE_STATE_UNKNOWN: - forecast[ATTR_FORECAST_WIND_SPEED] = int(day["windSpeed"]) - if day["visibility"] != ECOBEE_STATE_UNKNOWN: - forecast[ATTR_FORECAST_WIND_SPEED] = int(day["visibility"]) - if day["relativeHumidity"] != ECOBEE_STATE_UNKNOWN: - forecast[ATTR_FORECAST_HUMIDITY] = int(day["relativeHumidity"]) - forecasts.append(forecast) - return forecasts - except (ValueError, IndexError, KeyError): + if "forecasts" not in self.weather: return None + forecasts = list() + for day in range(1, 5): + forecast = _process_forecast(self.weather["forecasts"][day]) + if forecast is None: + continue + forecasts.append(forecast) + + if forecasts: + return forecasts + return None + async def async_update(self): """Get the latest weather data.""" await self.data.update() thermostat = self.data.ecobee.get_thermostat(self._index) self.weather = thermostat.get("weather", None) + + +def _process_forecast(json): + """Process a single ecobee API forecast to return expected values.""" + forecast = dict() + try: + forecast[ATTR_FORECAST_TIME] = datetime.strptime( + json["dateTime"], "%Y-%m-%d %H:%M:%S" + ).isoformat() + forecast[ATTR_FORECAST_CONDITION] = ECOBEE_WEATHER_SYMBOL_TO_HASS[ + json["weatherSymbol"] + ] + if json["tempHigh"] != ECOBEE_STATE_UNKNOWN: + forecast[ATTR_FORECAST_TEMP] = float(json["tempHigh"]) / 10 + if json["tempLow"] != ECOBEE_STATE_UNKNOWN: + forecast[ATTR_FORECAST_TEMP_LOW] = float(json["tempLow"]) / 10 + if json["windBearing"] != ECOBEE_STATE_UNKNOWN: + forecast[ATTR_FORECAST_WIND_BEARING] = int(json["windBearing"]) + if json["windSpeed"] != ECOBEE_STATE_UNKNOWN: + forecast[ATTR_FORECAST_WIND_SPEED] = int(json["windSpeed"]) + + except (ValueError, IndexError, KeyError): + return None + + if forecast: + return forecast + return None diff --git a/homeassistant/components/eliqonline/sensor.py b/homeassistant/components/eliqonline/sensor.py index 1f21263a4d6..b3d56e42325 100644 --- a/homeassistant/components/eliqonline/sensor.py +++ b/homeassistant/components/eliqonline/sensor.py @@ -1,15 +1,16 @@ """Monitors home energy use for the ELIQ Online service.""" +import asyncio from datetime import timedelta import logging -import asyncio +import eliqonline import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME, POWER_WATT -from homeassistant.helpers.entity import Entity -import homeassistant.helpers.config_validation as cv 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__) @@ -34,8 +35,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the ELIQ Online sensor.""" - import eliqonline - access_token = config.get(CONF_ACCESS_TOKEN) name = config.get(CONF_NAME, DEFAULT_NAME) channel_id = config.get(CONF_CHANNEL_ID) diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index d15399df67b..d257c46839c 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -2,7 +2,10 @@ import logging import re +import elkm1_lib as elkm1 +from elkm1_lib.const import Max import voluptuous as vol + from homeassistant.const import ( CONF_EXCLUDE, CONF_HOST, @@ -12,8 +15,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant, callback # noqa -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers import discovery +from homeassistant.helpers import config_validation as cv, discovery from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType # noqa @@ -125,9 +127,6 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool: """Set up the Elk M1 platform.""" - from elkm1_lib.const import Max - import elkm1_lib as elkm1 - devices = {} elk_datas = {} diff --git a/homeassistant/components/elkm1/alarm_control_panel.py b/homeassistant/components/elkm1/alarm_control_panel.py index 927ed53115e..38519ab5b3f 100644 --- a/homeassistant/components/elkm1/alarm_control_panel.py +++ b/homeassistant/components/elkm1/alarm_control_panel.py @@ -1,4 +1,5 @@ """Each ElkM1 area will be created as a separate alarm_control_panel.""" +from elkm1_lib.const import AlarmState, ArmedStatus, ArmLevel, ArmUpState import voluptuous as vol import homeassistant.components.alarm_control_panel as alarm @@ -93,8 +94,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= def _arm_services(): - from elkm1_lib.const import ArmLevel - return { "elkm1_alarm_arm_vacation": ArmLevel.ARMED_VACATION.value, "elkm1_alarm_arm_home_instant": ArmLevel.ARMED_STAY_INSTANT.value, @@ -147,8 +146,6 @@ class ElkArea(ElkEntity, alarm.AlarmControlPanel): @property def device_state_attributes(self): """Attributes of the area.""" - from elkm1_lib.const import AlarmState, ArmedStatus, ArmUpState - attrs = self.initial_attrs() elmt = self._element attrs["is_exit"] = elmt.is_exit @@ -164,8 +161,6 @@ class ElkArea(ElkEntity, alarm.AlarmControlPanel): return attrs def _element_changed(self, element, changeset): - from elkm1_lib.const import ArmedStatus - elk_state_to_hass_state = { ArmedStatus.DISARMED.value: STATE_ALARM_DISARMED, ArmedStatus.ARMED_AWAY.value: STATE_ALARM_ARMED_AWAY, @@ -191,8 +186,6 @@ class ElkArea(ElkEntity, alarm.AlarmControlPanel): return self._element.timer1 > 0 or self._element.timer2 > 0 def _area_is_in_alarm_state(self): - from elkm1_lib.const import AlarmState - return self._element.alarm_state >= AlarmState.FIRE_ALARM.value async def async_alarm_disarm(self, code=None): @@ -201,20 +194,14 @@ class ElkArea(ElkEntity, alarm.AlarmControlPanel): async def async_alarm_arm_home(self, code=None): """Send arm home command.""" - from elkm1_lib.const import ArmLevel - self._element.arm(ArmLevel.ARMED_STAY.value, int(code)) async def async_alarm_arm_away(self, code=None): """Send arm away command.""" - from elkm1_lib.const import ArmLevel - self._element.arm(ArmLevel.ARMED_AWAY.value, int(code)) async def async_alarm_arm_night(self, code=None): """Send arm night command.""" - from elkm1_lib.const import ArmLevel - self._element.arm(ArmLevel.ARMED_NIGHT.value, int(code)) async def _arm_service(self, arm_level, code): diff --git a/homeassistant/components/elkm1/climate.py b/homeassistant/components/elkm1/climate.py index 58273e71222..abc9dc0933c 100644 --- a/homeassistant/components/elkm1/climate.py +++ b/homeassistant/components/elkm1/climate.py @@ -1,4 +1,6 @@ """Support for control of Elk-M1 connected thermostats.""" +from elkm1_lib.const import ThermostatFan, ThermostatMode, ThermostatSetting + from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( ATTR_TARGET_TEMP_HIGH, @@ -16,7 +18,6 @@ from homeassistant.const import PRECISION_WHOLE, STATE_ON from . import DOMAIN as ELK_DOMAIN, ElkEntity, create_elk_entities - SUPPORT_HVAC = [ HVAC_MODE_OFF, HVAC_MODE_HEAT, @@ -67,8 +68,6 @@ class ElkThermostat(ElkEntity, ClimateDevice): @property def target_temperature(self): """Return the temperature we are trying to reach.""" - from elkm1_lib.const import ThermostatMode - if (self._element.mode == ThermostatMode.HEAT.value) or ( self._element.mode == ThermostatMode.EMERGENCY_HEAT.value ): @@ -115,8 +114,6 @@ class ElkThermostat(ElkEntity, ClimateDevice): @property def is_aux_heat(self): """Return if aux heater is on.""" - from elkm1_lib.const import ThermostatMode - return self._element.mode == ThermostatMode.EMERGENCY_HEAT.value @property @@ -132,8 +129,6 @@ class ElkThermostat(ElkEntity, ClimateDevice): @property def fan_mode(self): """Return the fan setting.""" - from elkm1_lib.const import ThermostatFan - if self._element.fan == ThermostatFan.AUTO.value: return HVAC_MODE_AUTO if self._element.fan == ThermostatFan.ON.value: @@ -141,8 +136,6 @@ class ElkThermostat(ElkEntity, ClimateDevice): return None def _elk_set(self, mode, fan): - from elkm1_lib.const import ThermostatSetting - if mode is not None: self._element.set(ThermostatSetting.MODE.value, mode) if fan is not None: @@ -150,8 +143,6 @@ class ElkThermostat(ElkEntity, ClimateDevice): async def async_set_hvac_mode(self, hvac_mode): """Set thermostat operation mode.""" - from elkm1_lib.const import ThermostatFan, ThermostatMode - settings = { HVAC_MODE_OFF: (ThermostatMode.OFF.value, ThermostatFan.AUTO.value), HVAC_MODE_HEAT: (ThermostatMode.HEAT.value, None), @@ -163,14 +154,10 @@ class ElkThermostat(ElkEntity, ClimateDevice): async def async_turn_aux_heat_on(self): """Turn auxiliary heater on.""" - from elkm1_lib.const import ThermostatMode - self._elk_set(ThermostatMode.EMERGENCY_HEAT.value, None) async def async_turn_aux_heat_off(self): """Turn auxiliary heater off.""" - from elkm1_lib.const import ThermostatMode - self._elk_set(ThermostatMode.HEAT.value, None) @property @@ -180,8 +167,6 @@ class ElkThermostat(ElkEntity, ClimateDevice): async def async_set_fan_mode(self, fan_mode): """Set new target fan mode.""" - from elkm1_lib.const import ThermostatFan - if fan_mode == HVAC_MODE_AUTO: self._elk_set(None, ThermostatFan.AUTO.value) elif fan_mode == STATE_ON: @@ -189,8 +174,6 @@ class ElkThermostat(ElkEntity, ClimateDevice): async def async_set_temperature(self, **kwargs): """Set new target temperature.""" - from elkm1_lib.const import ThermostatSetting - low_temp = kwargs.get(ATTR_TARGET_TEMP_LOW) high_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH) if low_temp is not None: @@ -199,8 +182,6 @@ class ElkThermostat(ElkEntity, ClimateDevice): self._element.set(ThermostatSetting.COOL_SETPOINT.value, round(high_temp)) def _element_changed(self, element, changeset): - from elkm1_lib.const import ThermostatFan, ThermostatMode - mode_to_state = { ThermostatMode.OFF.value: HVAC_MODE_OFF, ThermostatMode.COOL.value: HVAC_MODE_COOL, diff --git a/homeassistant/components/elkm1/sensor.py b/homeassistant/components/elkm1/sensor.py index 3f524b778db..3ed5356f4de 100644 --- a/homeassistant/components/elkm1/sensor.py +++ b/homeassistant/components/elkm1/sensor.py @@ -1,4 +1,12 @@ """Support for control of ElkM1 sensors.""" +from elkm1_lib.const import ( + SettingFormat, + ZoneLogicalStatus, + ZonePhysicalStatus, + ZoneType, +) +from elkm1_lib.util import pretty_const, username + from . import DOMAIN as ELK_DOMAIN, ElkEntity, create_elk_entities @@ -79,8 +87,6 @@ class ElkKeypad(ElkSensor): @property def device_state_attributes(self): """Attributes of the sensor.""" - from elkm1_lib.util import username - attrs = self.initial_attrs() attrs["area"] = self._element.area + 1 attrs["temperature"] = self._element.temperature @@ -140,8 +146,6 @@ class ElkSetting(ElkSensor): @property def device_state_attributes(self): """Attributes of the sensor.""" - from elkm1_lib.const import SettingFormat - attrs = self.initial_attrs() attrs["value_format"] = SettingFormat(self._element.value_format).name.lower() return attrs @@ -153,8 +157,6 @@ class ElkZone(ElkSensor): @property def icon(self): """Icon to use in the frontend.""" - from elkm1_lib.const import ZoneType - zone_icons = { ZoneType.FIRE_ALARM.value: "fire", ZoneType.FIRE_VERIFIED.value: "fire", @@ -181,8 +183,6 @@ class ElkZone(ElkSensor): @property def device_state_attributes(self): """Attributes of the sensor.""" - from elkm1_lib.const import ZoneLogicalStatus, ZonePhysicalStatus, ZoneType - attrs = self.initial_attrs() attrs["physical_status"] = ZonePhysicalStatus( self._element.physical_status @@ -199,8 +199,6 @@ class ElkZone(ElkSensor): @property def temperature_unit(self): """Return the temperature unit.""" - from elkm1_lib.const import ZoneType - if self._element.definition == ZoneType.TEMPERATURE.value: return self._temperature_unit return None @@ -208,8 +206,6 @@ class ElkZone(ElkSensor): @property def unit_of_measurement(self): """Return the unit of measurement.""" - from elkm1_lib.const import ZoneType - if self._element.definition == ZoneType.TEMPERATURE.value: return self._temperature_unit if self._element.definition == ZoneType.ANALOG_ZONE.value: @@ -217,9 +213,6 @@ class ElkZone(ElkSensor): return None def _element_changed(self, element, changeset): - from elkm1_lib.const import ZoneLogicalStatus, ZoneType - from elkm1_lib.util import pretty_const - if self._element.definition == ZoneType.TEMPERATURE.value: self._state = temperature_to_state(self._element.temperature, -60) elif self._element.definition == ZoneType.ANALOG_ZONE.value: diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index c62e1e356b6..4d1a8094663 100644 --- a/homeassistant/components/environment_canada/manifest.json +++ b/homeassistant/components/environment_canada/manifest.json @@ -3,7 +3,7 @@ "name": "Environment Canada", "documentation": "https://www.home-assistant.io/integrations/environment_canada", "requirements": [ - "env_canada==0.0.25" + "env_canada==0.0.27" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/envisalink/__init__.py b/homeassistant/components/envisalink/__init__.py index 76d6a7e369c..6cdedf89744 100644 --- a/homeassistant/components/envisalink/__init__.py +++ b/homeassistant/components/envisalink/__init__.py @@ -141,9 +141,12 @@ async def async_setup(hass, config): @callback def connection_fail_callback(data): """Network failure callback.""" - _LOGGER.error("Could not establish a connection with the Envisalink") + _LOGGER.error( + "Could not establish a connection with the Envisalink- retrying..." + ) if not sync_connect.done(): - sync_connect.set_result(False) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_envisalink) + sync_connect.set_result(True) @callback def connection_success_callback(data): diff --git a/homeassistant/components/envisalink/alarm_control_panel.py b/homeassistant/components/envisalink/alarm_control_panel.py index 81e656708c5..663f19c8ed5 100644 --- a/homeassistant/components/envisalink/alarm_control_panel.py +++ b/homeassistant/components/envisalink/alarm_control_panel.py @@ -8,6 +8,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, @@ -126,6 +127,8 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel): if self._info["status"]["alarm"]: state = STATE_ALARM_TRIGGERED + elif self._info["status"]["armed_zero_entry_delay"]: + state = STATE_ALARM_ARMED_NIGHT elif self._info["status"]["armed_away"]: state = STATE_ALARM_ARMED_AWAY elif self._info["status"]["armed_stay"]: @@ -173,6 +176,12 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel): """Alarm trigger command. Will be used to trigger a panic alarm.""" self.hass.data[DATA_EVL].panic_alarm(self._panic_type) + async def async_alarm_arm_night(self, code=None): + """Send arm night command.""" + self.hass.data[DATA_EVL].arm_night_partition( + str(code) if code else str(self._code), self._partition_number + ) + @callback def async_alarm_keypress(self, keypress=None): """Send custom keypress.""" diff --git a/homeassistant/components/envisalink/manifest.json b/homeassistant/components/envisalink/manifest.json index 6c5405c75ea..52303c18413 100644 --- a/homeassistant/components/envisalink/manifest.json +++ b/homeassistant/components/envisalink/manifest.json @@ -3,8 +3,8 @@ "name": "Envisalink", "documentation": "https://www.home-assistant.io/integrations/envisalink", "requirements": [ - "pyenvisalink==3.8" + "pyenvisalink==4.0" ], "dependencies": [], "codeowners": [] -} +} \ No newline at end of file diff --git a/homeassistant/components/epson/media_player.py b/homeassistant/components/epson/media_player.py index 435ef582da8..638f012ac7a 100644 --- a/homeassistant/components/epson/media_player.py +++ b/homeassistant/components/epson/media_player.py @@ -3,6 +3,30 @@ import logging import voluptuous as vol +from epson_projector.const import ( + BACK, + BUSY, + CMODE, + CMODE_LIST, + CMODE_LIST_SET, + DEFAULT_SOURCES, + EPSON_CODES, + FAST, + INV_SOURCES, + MUTE, + PAUSE, + PLAY, + POWER, + SOURCE, + SOURCE_LIST, + TURN_ON, + TURN_OFF, + VOLUME, + VOL_DOWN, + VOL_UP, +) +import epson_projector as epson + from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA from homeassistant.components.media_player.const import ( DOMAIN, @@ -61,8 +85,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Epson media player platform.""" - from epson_projector.const import CMODE_LIST_SET - if DATA_EPSON not in hass.data: hass.data[DATA_EPSON] = [] @@ -71,12 +93,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= port = config.get(CONF_PORT) ssl = config.get(CONF_SSL) - epson = EpsonProjector( + epson_proj = EpsonProjector( async_get_clientsession(hass, verify_ssl=False), name, host, port, ssl ) - hass.data[DATA_EPSON].append(epson) - async_add_entities([epson], update_before_add=True) + hass.data[DATA_EPSON].append(epson_proj) + async_add_entities([epson_proj], update_before_add=True) async def async_service_handler(service): """Handle for services.""" @@ -108,9 +130,6 @@ class EpsonProjector(MediaPlayerDevice): def __init__(self, websession, name, host, port, encryption): """Initialize entity to control Epson projector.""" - import epson_projector as epson - from epson_projector.const import DEFAULT_SOURCES - self._name = name self._projector = epson.Projector(host, websession=websession, port=port) self._cmode = None @@ -121,17 +140,6 @@ class EpsonProjector(MediaPlayerDevice): async def async_update(self): """Update state of device.""" - from epson_projector.const import ( - EPSON_CODES, - POWER, - CMODE, - CMODE_LIST, - SOURCE, - VOLUME, - BUSY, - SOURCE_LIST, - ) - is_turned_on = await self._projector.get_property(POWER) _LOGGER.debug("Project turn on/off status: %s", is_turned_on) if is_turned_on and is_turned_on == EPSON_CODES[POWER]: @@ -165,15 +173,11 @@ class EpsonProjector(MediaPlayerDevice): async def async_turn_on(self): """Turn on epson.""" - from epson_projector.const import TURN_ON - if self._state == STATE_OFF: await self._projector.send_command(TURN_ON) async def async_turn_off(self): """Turn off epson.""" - from epson_projector.const import TURN_OFF - if self._state == STATE_ON: await self._projector.send_command(TURN_OFF) @@ -194,57 +198,39 @@ class EpsonProjector(MediaPlayerDevice): async def select_cmode(self, cmode): """Set color mode in Epson.""" - from epson_projector.const import CMODE_LIST_SET - await self._projector.send_command(CMODE_LIST_SET[cmode]) async def async_select_source(self, source): """Select input source.""" - from epson_projector.const import INV_SOURCES - selected_source = INV_SOURCES[source] await self._projector.send_command(selected_source) async def async_mute_volume(self, mute): """Mute (true) or unmute (false) sound.""" - from epson_projector.const import MUTE - await self._projector.send_command(MUTE) async def async_volume_up(self): """Increase volume.""" - from epson_projector.const import VOL_UP - await self._projector.send_command(VOL_UP) async def async_volume_down(self): """Decrease volume.""" - from epson_projector.const import VOL_DOWN - await self._projector.send_command(VOL_DOWN) async def async_media_play(self): """Play media via Epson.""" - from epson_projector.const import PLAY - await self._projector.send_command(PLAY) async def async_media_pause(self): """Pause media via Epson.""" - from epson_projector.const import PAUSE - await self._projector.send_command(PAUSE) async def async_media_next_track(self): """Skip to next.""" - from epson_projector.const import FAST - await self._projector.send_command(FAST) async def async_media_previous_track(self): """Skip to previous.""" - from epson_projector.const import BACK - await self._projector.send_command(BACK) @property diff --git a/homeassistant/components/epsonworkforce/sensor.py b/homeassistant/components/epsonworkforce/sensor.py index 99e2723bf4a..b310376e5cc 100644 --- a/homeassistant/components/epsonworkforce/sensor.py +++ b/homeassistant/components/epsonworkforce/sensor.py @@ -10,8 +10,6 @@ from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -REQUIREMENTS = ["epsonprinter==0.0.9"] - _LOGGER = logging.getLogger(__name__) MONITORED_CONDITIONS = { "black": ["Ink level Black", "%", "mdi:water"], diff --git a/homeassistant/components/esphome/.translations/ru.json b/homeassistant/components/esphome/.translations/ru.json index 62d24662ab6..27d223012c0 100644 --- a/homeassistant/components/esphome/.translations/ru.json +++ b/homeassistant/components/esphome/.translations/ru.json @@ -6,7 +6,7 @@ "error": { "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a ESP. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0412\u0430\u0448 YAML-\u0444\u0430\u0439\u043b \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0441\u0442\u0440\u043e\u043a\u0443 'api:'.", "invalid_password": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c!", - "resolve_error": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0430\u0434\u0440\u0435\u0441 ESP. \u0415\u0441\u043b\u0438 \u044d\u0442\u0430 \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0432\u0442\u043e\u0440\u044f\u0435\u0442\u0441\u044f, \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0441\u0442\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0439 IP-\u0430\u0434\u0440\u0435\u0441: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + "resolve_error": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0430\u0434\u0440\u0435\u0441 ESP. \u0415\u0441\u043b\u0438 \u044d\u0442\u0430 \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0432\u0442\u043e\u0440\u044f\u0435\u0442\u0441\u044f, \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0441\u0442\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0439 IP-\u0430\u0434\u0440\u0435\u0441: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips." }, "flow_title": "ESPHome: {name}", "step": { diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index bc06aba94ea..a669726ca38 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -95,8 +95,11 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool """Cleanup the socket client on HA stop.""" await _cleanup_instance(hass, entry) + # Use async_listen instead of async_listen_once so that we don't deregister + # the callback twice when shutting down Home Assistant. + # "Unable to remove unknown listener .onetime_listener>" entry_data.cleanup_callbacks.append( - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_stop) + hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, on_stop) ) @callback @@ -365,6 +368,7 @@ async def platform_async_setup_entry( """ entry_data: RuntimeEntryData = hass.data[DOMAIN][entry.entry_id] entry_data.info[component_key] = {} + entry_data.old_info[component_key] = {} entry_data.state[component_key] = {} @callback @@ -390,7 +394,13 @@ async def platform_async_setup_entry( # Remove old entities for info in old_infos.values(): entry_data.async_remove_entity(hass, component_key, info.key) + + # First copy the now-old info into the backup object + entry_data.old_info[component_key] = entry_data.info[component_key] + # Then update the actual info entry_data.info[component_key] = new_infos + + # Add entities to Home Assistant async_add_entities(add_entities) signal = DISPATCHER_ON_LIST.format(entry_id=entry.entry_id) @@ -479,7 +489,9 @@ class EsphomeEntity(Entity): } self._remove_callbacks.append( async_dispatcher_connect( - self.hass, DISPATCHER_UPDATE_ENTITY.format(**kwargs), self._on_update + self.hass, + DISPATCHER_UPDATE_ENTITY.format(**kwargs), + self._on_state_update, ) ) @@ -493,14 +505,23 @@ class EsphomeEntity(Entity): async_dispatcher_connect( self.hass, DISPATCHER_ON_DEVICE_UPDATE.format(**kwargs), - self.async_schedule_update_ha_state, + self._on_device_update, ) ) - async def _on_update(self) -> None: + async def _on_state_update(self) -> None: """Update the entity state when state or static info changed.""" self.async_schedule_update_ha_state() + async def _on_device_update(self) -> None: + """Update the entity state when device info has changed.""" + if self._entry_data.available: + # Don't update the HA state yet when the device comes online. + # Only update the HA state when the full state arrives + # through the next entity state packet. + return + self.async_schedule_update_ha_state() + async def async_will_remove_from_hass(self) -> None: """Unregister callbacks.""" for remove_callback in self._remove_callbacks: @@ -513,7 +534,13 @@ class EsphomeEntity(Entity): @property def _static_info(self) -> EntityInfo: - return self._entry_data.info[self._component_key][self._key] + # Check if value is in info database. Use a single lookup. + info = self._entry_data.info[self._component_key].get(self._key) + if info is not None: + return info + # This entity is in the removal project and has been removed from .info + # already, look in old_info + return self._entry_data.old_info[self._component_key].get(self._key) @property def _device_info(self) -> DeviceInfo: diff --git a/homeassistant/components/esphome/camera.py b/homeassistant/components/esphome/camera.py index cc2e0cede23..c3615c4726d 100644 --- a/homeassistant/components/esphome/camera.py +++ b/homeassistant/components/esphome/camera.py @@ -47,9 +47,9 @@ class EsphomeCamera(Camera, EsphomeEntity): def _state(self) -> Optional[CameraState]: return super()._state - async def _on_update(self) -> None: + async def _on_state_update(self) -> None: """Notify listeners of new image when update arrives.""" - await super()._on_update() + await super()._on_state_update() async with self._image_cond: self._image_cond.notify_all() diff --git a/homeassistant/components/esphome/climate.py b/homeassistant/components/esphome/climate.py index 7337aec4541..1dfe2184952 100644 --- a/homeassistant/components/esphome/climate.py +++ b/homeassistant/components/esphome/climate.py @@ -17,6 +17,7 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE_RANGE, PRESET_AWAY, HVAC_MODE_OFF, + PRESET_HOME, ) from homeassistant.const import ( ATTR_TEMPERATURE, @@ -96,7 +97,7 @@ class EsphomeClimateDevice(EsphomeEntity, ClimateDevice): @property def preset_modes(self): """Return preset modes.""" - return [PRESET_AWAY] if self._static_info.supports_away else [] + return [PRESET_AWAY, PRESET_HOME] if self._static_info.supports_away else [] @property def target_temperature_step(self) -> float: @@ -126,6 +127,9 @@ class EsphomeClimateDevice(EsphomeEntity, ClimateDevice): features |= SUPPORT_PRESET_MODE return features + # https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property + # pylint: disable=invalid-overridden-method + @esphome_state_property def hvac_mode(self) -> Optional[str]: """Return current operation ie. heat, cool, idle.""" diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 9680ed46acd..47c00f43463 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -44,11 +44,12 @@ class EsphomeFlowHandler(config_entries.ConfigFlow): @property def _name(self): + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 return self.context.get("name") @_name.setter def _name(self, value): - # pylint: disable=unsupported-assignment-operation + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context["name"] = value self.context["title_placeholders"] = {"name": self._name} diff --git a/homeassistant/components/esphome/cover.py b/homeassistant/components/esphome/cover.py index 31b895b4eb2..980fc936940 100644 --- a/homeassistant/components/esphome/cover.py +++ b/homeassistant/components/esphome/cover.py @@ -70,6 +70,9 @@ class EsphomeCover(EsphomeEntity, CoverDevice): def _state(self) -> Optional[CoverState]: return super()._state + # https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property + # pylint: disable=invalid-overridden-method + @esphome_state_property def is_closed(self) -> Optional[bool]: """Return if the cover is closed or not.""" diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index b7f9ad9b347..d916e1a90c8 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -56,6 +56,13 @@ class RuntimeEntryData: reconnect_task = attr.ib(type=Optional[asyncio.Task], default=None) state = attr.ib(type=Dict[str, Dict[str, Any]], factory=dict) info = attr.ib(type=Dict[str, Dict[str, Any]], factory=dict) + + # A second list of EntityInfo objects + # This is necessary for when an entity is being removed. HA requires + # some static info to be accessible during removal (unique_id, maybe others) + # If an entity can't find anything in the info array, it will look for info here. + old_info = attr.ib(type=Dict[str, Dict[str, Any]], factory=dict) + services = attr.ib(type=Dict[int, "UserService"], factory=dict) available = attr.ib(type=bool, default=False) device_info = attr.ib(type=DeviceInfo, default=None) diff --git a/homeassistant/components/esphome/fan.py b/homeassistant/components/esphome/fan.py index 44059673f15..cddb75b41bf 100644 --- a/homeassistant/components/esphome/fan.py +++ b/homeassistant/components/esphome/fan.py @@ -92,6 +92,9 @@ class EsphomeFan(EsphomeEntity, FanEntity): key=self._static_info.key, oscillating=oscillating ) + # https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property + # pylint: disable=invalid-overridden-method + @esphome_state_property def is_on(self) -> Optional[bool]: """Return true if the entity is on.""" diff --git a/homeassistant/components/esphome/light.py b/homeassistant/components/esphome/light.py index 1205521706e..9a2a0ccd0bc 100644 --- a/homeassistant/components/esphome/light.py +++ b/homeassistant/components/esphome/light.py @@ -61,6 +61,9 @@ class EsphomeLight(EsphomeEntity, Light): def _state(self) -> Optional[LightState]: return super()._state + # https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property + # pylint: disable=invalid-overridden-method + @esphome_state_property def is_on(self) -> Optional[bool]: """Return true if the switch is on.""" @@ -91,7 +94,7 @@ class EsphomeLight(EsphomeEntity, Light): """Turn the entity off.""" data = {"key": self._static_info.key, "state": False} if ATTR_FLASH in kwargs: - data["flash"] = FLASH_LENGTHS[kwargs[ATTR_FLASH]] + data["flash_length"] = FLASH_LENGTHS[kwargs[ATTR_FLASH]] if ATTR_TRANSITION in kwargs: data["transition_length"] = kwargs[ATTR_TRANSITION] await self._client.light_command(**data) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index bde64762121..40691c653f5 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.2.0" + "aioesphomeapi==2.4.2" ], "dependencies": [], "zeroconf": ["_esphomelib._tcp.local."], diff --git a/homeassistant/components/esphome/sensor.py b/homeassistant/components/esphome/sensor.py index 3168bae7ec8..b6adbf93c41 100644 --- a/homeassistant/components/esphome/sensor.py +++ b/homeassistant/components/esphome/sensor.py @@ -37,6 +37,10 @@ async def async_setup_entry( ) +# https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property +# pylint: disable=invalid-overridden-method + + class EsphomeSensor(EsphomeEntity): """A sensor implementation for esphome.""" @@ -53,6 +57,11 @@ class EsphomeSensor(EsphomeEntity): """Return the icon.""" return self._static_info.icon + @property + def force_update(self) -> bool: + """Return if this sensor should force a state update.""" + return self._static_info.force_update + @esphome_state_property def state(self) -> Optional[str]: """Return the state of the entity.""" diff --git a/homeassistant/components/esphome/switch.py b/homeassistant/components/esphome/switch.py index f66bfaa39f3..b52d630e1b4 100644 --- a/homeassistant/components/esphome/switch.py +++ b/homeassistant/components/esphome/switch.py @@ -49,6 +49,8 @@ class EsphomeSwitch(EsphomeEntity, SwitchDevice): """Return true if we do optimistic updates.""" return self._static_info.assumed_state + # https://github.com/PyCQA/pylint/issues/3150 for @esphome_state_property + # pylint: disable=invalid-overridden-method @esphome_state_property def is_on(self) -> Optional[bool]: """Return true if the switch is on.""" diff --git a/homeassistant/components/eufy/__init__.py b/homeassistant/components/eufy/__init__.py index df6aed3582f..191d6ab5315 100644 --- a/homeassistant/components/eufy/__init__.py +++ b/homeassistant/components/eufy/__init__.py @@ -1,5 +1,6 @@ """Support for Eufy devices.""" import logging +import lakeside import voluptuous as vol @@ -56,7 +57,6 @@ EUFY_DISPATCH = { def setup(hass, config): """Set up Eufy devices.""" - import lakeside if CONF_USERNAME in config[DOMAIN] and CONF_PASSWORD in config[DOMAIN]: data = lakeside.get_devices( diff --git a/homeassistant/components/eufy/light.py b/homeassistant/components/eufy/light.py index f5359e6f2f6..21c26606bdd 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 ( ATTR_BRIGHTNESS, @@ -36,7 +37,6 @@ class EufyLight(Light): def __init__(self, device): """Initialize the light.""" - import lakeside self._temp = None self._brightness = None diff --git a/homeassistant/components/eufy/switch.py b/homeassistant/components/eufy/switch.py index 3d05ef5d351..2e13886dd2a 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 @@ -18,7 +19,6 @@ class EufySwitch(SwitchDevice): def __init__(self, device): """Initialize the light.""" - import lakeside self._state = None self._name = device["name"] diff --git a/homeassistant/components/everlights/light.py b/homeassistant/components/everlights/light.py index 506617e4c60..f7fa9deffa0 100644 --- a/homeassistant/components/everlights/light.py +++ b/homeassistant/components/everlights/light.py @@ -1,25 +1,26 @@ """Support for EverLights lights.""" -import logging from datetime import timedelta +import logging from typing import Tuple +import pyeverlights import voluptuous as vol -from homeassistant.const import CONF_HOSTS from homeassistant.components.light import ( ATTR_BRIGHTNESS, - ATTR_HS_COLOR, ATTR_EFFECT, - SUPPORT_BRIGHTNESS, - SUPPORT_EFFECT, - SUPPORT_COLOR, - Light, + ATTR_HS_COLOR, PLATFORM_SCHEMA, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, + SUPPORT_EFFECT, + Light, ) +from homeassistant.const import CONF_HOSTS +from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util -from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.exceptions import PlatformNotReady _LOGGER = logging.getLogger(__name__) @@ -46,8 +47,6 @@ def color_int_to_rgb(value: int) -> Tuple[int, int, int]: async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the EverLights lights from configuration.yaml.""" - import pyeverlights - lights = [] for ipaddr in config[CONF_HOSTS]: @@ -159,8 +158,6 @@ class EverLightsLight(Light): async def async_update(self): """Synchronize state with control box.""" - import pyeverlights - try: self._status = await self._api.get_status() except pyeverlights.ConnectionError: diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 14bf1223953..29f89dc08d6 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -8,11 +8,11 @@ import re from typing import Any, Dict, Optional, Tuple import aiohttp.client_exceptions -import voluptuous as vol +import evohomeasync import evohomeasync2 +import voluptuous as vol from homeassistant.const import ( - CONF_ACCESS_TOKEN, CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME, @@ -28,14 +28,17 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType, HomeAssistantType import homeassistant.util.dt as dt_util -from .const import DOMAIN, EVO_FOLLOW, STORAGE_VERSION, STORAGE_KEY, GWS, TCS +from .const import DOMAIN, EVO_FOLLOW, GWS, STORAGE_KEY, STORAGE_VERSION, TCS _LOGGER = logging.getLogger(__name__) -CONF_ACCESS_TOKEN_EXPIRES = "access_token_expires" -CONF_REFRESH_TOKEN = "refresh_token" +ACCESS_TOKEN = "access_token" +ACCESS_TOKEN_EXPIRES = "access_token_expires" +REFRESH_TOKEN = "refresh_token" +USER_DATA = "user_data" CONF_LOCATION_IDX = "location_idx" + SCAN_INTERVAL_DEFAULT = timedelta(seconds=300) SCAN_INTERVAL_MINIMUM = timedelta(seconds=60) @@ -96,14 +99,15 @@ def convert_dict(dictionary: Dict[str, Any]) -> Dict[str, Any]: def _handle_exception(err) -> bool: + """Return False if the exception can't be ignored.""" try: raise err except evohomeasync2.AuthenticationError: _LOGGER.error( - "Failed to (re)authenticate with the vendor's server. " + "Failed to authenticate with the vendor's server. " "Check your network and the vendor's service status page. " - "Check that your username and password are correct. " + "Also check that your username and password are correct. " "Message is: %s", err, ) @@ -135,14 +139,77 @@ def _handle_exception(err) -> bool: ) return False - raise # we don't expect/handle any other ClientResponseError + raise # we don't expect/handle any other Exceptions async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: """Create a (EMEA/EU-based) Honeywell evohome system.""" - broker = EvoBroker(hass, config[DOMAIN]) - if not await broker.init_client(): + + async def load_auth_tokens(store) -> Tuple[Dict, Optional[Dict]]: + app_storage = await store.async_load() + tokens = dict(app_storage if app_storage else {}) + + if tokens.pop(CONF_USERNAME, None) != config[DOMAIN][CONF_USERNAME]: + # any tokens wont be valid, and store might be be corrupt + await store.async_save({}) + return ({}, None) + + # evohomeasync2 requires naive/local datetimes as strings + if tokens.get(ACCESS_TOKEN_EXPIRES) is not None: + tokens[ACCESS_TOKEN_EXPIRES] = _dt_to_local_naive( + dt_util.parse_datetime(tokens[ACCESS_TOKEN_EXPIRES]) + ) + + user_data = tokens.pop(USER_DATA, None) + return (tokens, user_data) + + store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + tokens, user_data = await load_auth_tokens(store) + + client_v2 = evohomeasync2.EvohomeClient( + config[DOMAIN][CONF_USERNAME], + config[DOMAIN][CONF_PASSWORD], + **tokens, + session=async_get_clientsession(hass), + ) + + try: + await client_v2.login() + except (aiohttp.ClientError, evohomeasync2.AuthenticationError) as err: + _handle_exception(err) return False + finally: + config[DOMAIN][CONF_PASSWORD] = "REDACTED" + + loc_idx = config[DOMAIN][CONF_LOCATION_IDX] + try: + loc_config = client_v2.installation_info[loc_idx][GWS][0][TCS][0] + except IndexError: + _LOGGER.error( + "Config error: '%s' = %s, but the valid range is 0-%s. " + "Unable to continue. Fix any configuration errors and restart HA.", + CONF_LOCATION_IDX, + loc_idx, + len(client_v2.installation_info) - 1, + ) + return False + + _LOGGER.debug("Config = %s", loc_config) + + client_v1 = evohomeasync.EvohomeClient( + client_v2.username, + client_v2.password, + user_data=user_data, + session=async_get_clientsession(hass), + ) + + hass.data[DOMAIN] = {} + hass.data[DOMAIN]["broker"] = broker = EvoBroker( + hass, client_v2, client_v1, store, config[DOMAIN] + ) + + await broker.save_auth_tokens() + await broker.update() # get initial state hass.async_create_task(async_load_platform(hass, "climate", DOMAIN, {}, config)) if broker.tcs.hotwater: @@ -160,116 +227,100 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: class EvoBroker: """Container for evohome client and data.""" - def __init__(self, hass, params) -> None: + def __init__(self, hass, client, client_v1, store, params) -> None: """Initialize the evohome client and its data structure.""" self.hass = hass + self.client = client + self.client_v1 = client_v1 + self._store = store self.params = params - self.config = {} - - self.client = self.tcs = None - self._app_storage = {} - - hass.data[DOMAIN] = {} - hass.data[DOMAIN]["broker"] = self - - async def init_client(self) -> bool: - """Initialse the evohome data broker. - - Return True if this is successful, otherwise return False. - """ - refresh_token, access_token, access_token_expires = ( - await self._load_auth_tokens() - ) - - # evohomeasync2 uses naive/local datetimes - if access_token_expires is not None: - access_token_expires = _dt_to_local_naive(access_token_expires) - - client = self.client = evohomeasync2.EvohomeClient( - self.params[CONF_USERNAME], - self.params[CONF_PASSWORD], - refresh_token=refresh_token, - access_token=access_token, - access_token_expires=access_token_expires, - session=async_get_clientsession(self.hass), - ) - - try: - await client.login() - except (aiohttp.ClientError, evohomeasync2.AuthenticationError) as err: - if not _handle_exception(err): - return False - - finally: - self.params[CONF_PASSWORD] = "REDACTED" - - self.hass.add_job(self._save_auth_tokens()) - - loc_idx = self.params[CONF_LOCATION_IDX] - try: - self.config = client.installation_info[loc_idx][GWS][0][TCS][0] - - except IndexError: - _LOGGER.error( - "Config error: '%s' = %s, but its valid range is 0-%s. " - "Unable to continue. " - "Fix any configuration errors and restart HA.", - CONF_LOCATION_IDX, - loc_idx, - len(client.installation_info) - 1, - ) - return False + 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.temps = None - _LOGGER.debug("Config = %s", self.config) - if _LOGGER.isEnabledFor(logging.DEBUG): # don't do an I/O unless required - await self.update() # includes: _LOGGER.debug("Status = %s"... - - return True - - async def _load_auth_tokens( - self - ) -> Tuple[Optional[str], Optional[str], Optional[datetime]]: - store = self.hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) - app_storage = self._app_storage = await store.async_load() - - if app_storage is None: - app_storage = self._app_storage = {} - - if app_storage.get(CONF_USERNAME) == self.params[CONF_USERNAME]: - refresh_token = app_storage.get(CONF_REFRESH_TOKEN) - access_token = app_storage.get(CONF_ACCESS_TOKEN) - at_expires_str = app_storage.get(CONF_ACCESS_TOKEN_EXPIRES) - if at_expires_str: - at_expires_dt = dt_util.parse_datetime(at_expires_str) - else: - at_expires_dt = None - - return (refresh_token, access_token, at_expires_dt) - - return (None, None, None) # account switched: so tokens wont be valid - - async def _save_auth_tokens(self, *args) -> None: + async def save_auth_tokens(self) -> None: + """Save access tokens and session IDs to the store for later use.""" # evohomeasync2 uses naive/local datetimes access_token_expires = _local_dt_to_aware(self.client.access_token_expires) - self._app_storage[CONF_USERNAME] = self.params[CONF_USERNAME] - self._app_storage[CONF_REFRESH_TOKEN] = self.client.refresh_token - self._app_storage[CONF_ACCESS_TOKEN] = self.client.access_token - self._app_storage[CONF_ACCESS_TOKEN_EXPIRES] = access_token_expires.isoformat() + app_storage = {CONF_USERNAME: self.client.username} + app_storage[REFRESH_TOKEN] = self.client.refresh_token + app_storage[ACCESS_TOKEN] = self.client.access_token + app_storage[ACCESS_TOKEN_EXPIRES] = access_token_expires.isoformat() - store = self.hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) - await store.async_save(self._app_storage) + if self.client_v1 and self.client_v1.user_data: + app_storage[USER_DATA] = { + "userInfo": {"userID": self.client_v1.user_data["userInfo"]["userID"]}, + "sessionId": self.client_v1.user_data["sessionId"], + } + else: + app_storage[USER_DATA] = None - self.hass.helpers.event.async_track_point_in_utc_time( - self._save_auth_tokens, - access_token_expires + self.params[CONF_SCAN_INTERVAL], - ) + await self._store.async_save(app_storage) + + async def _update_v1(self, *args, **kwargs) -> None: + """Get the latest high-precision temperatures of the default Location.""" + + def get_session_id(client_v1) -> Optional[str]: + user_data = client_v1.user_data if client_v1 else None + return user_data.get("sessionId") if user_data else None + + session_id = get_session_id(self.client_v1) + + try: + temps = list(await self.client_v1.temperatures(force_refresh=True)) + + except aiohttp.ClientError as err: + _LOGGER.warning( + "Unable to obtain the latest high-precision temperatures. " + "Check your network and the vendor's service status page. " + "Proceeding with low-precision temperatures. " + "Message is: %s", + err, + ) + self.temps = None # these are now stale, will fall back to v2 temps + + else: + if ( + str(self.client_v1.location_id) + != self.client.locations[self.params[CONF_LOCATION_IDX]].locationId + ): + _LOGGER.warning( + "The v2 API's configured location doesn't match " + "the v1 API's default location (there is more than one location), " + "so the high-precision feature will be disabled" + ) + self.client_v1 = self.temps = None + else: + self.temps = {str(i["id"]): i["temp"] for i in temps} + + _LOGGER.debug("Temperatures = %s", self.temps) + + if session_id != get_session_id(self.client_v1): + await self.save_auth_tokens() + + async def _update_v2(self, *args, **kwargs) -> None: + """Get the latest modes, temperatures, setpoints of a Location.""" + access_token = self.client.access_token + + loc_idx = self.params[CONF_LOCATION_IDX] + try: + status = await self.client.locations[loc_idx].status() + except (aiohttp.ClientError, evohomeasync2.AuthenticationError) as err: + _handle_exception(err) + else: + self.hass.helpers.dispatcher.async_dispatcher_send(DOMAIN) + + _LOGGER.debug("Status = %s", status[GWS][0][TCS][0]) + + if access_token != self.client.access_token: + await self.save_auth_tokens() async def update(self, *args, **kwargs) -> None: """Get the latest state data of an entire evohome Location. @@ -278,17 +329,13 @@ class EvoBroker: operating mode of the Controller and the current temp of its children (e.g. Zones, DHW controller). """ - loc_idx = self.params[CONF_LOCATION_IDX] + await self._update_v2() - try: - status = await self.client.locations[loc_idx].status() - except (aiohttp.ClientError, evohomeasync2.AuthenticationError) as err: - _handle_exception(err) - else: - # inform the evohome devices that state data has been updated - self.hass.helpers.dispatcher.async_dispatcher_send(DOMAIN) + if self.client_v1: + await self._update_v1() - _LOGGER.debug("Status = %s", status[GWS][0][TCS][0]) + # inform the evohome devices that state data has been updated + self.hass.helpers.dispatcher.async_dispatcher_send(DOMAIN) class EvoDevice(Entity): @@ -305,10 +352,8 @@ class EvoDevice(Entity): self._evo_tcs = evo_broker.tcs self._unique_id = self._name = self._icon = self._precision = None - - self._device_state_attrs = {} - self._state_attributes = [] self._supported_features = None + self._device_state_attrs = {} @callback def _refresh(self) -> None: @@ -394,9 +439,13 @@ class EvoChild(EvoDevice): @property def current_temperature(self) -> Optional[float]: """Return the current temperature of a Zone.""" - if self._evo_device.temperatureStatus["isAvailable"]: - return self._evo_device.temperatureStatus["temperature"] - return None + if not self._evo_device.temperatureStatus["isAvailable"]: + return None + + if self._evo_broker.temps: + return self._evo_broker.temps[self._evo_device.zoneId] + + return self._evo_device.temperatureStatus["temperature"] @property def setpoints(self) -> Dict[str, Any]: @@ -411,37 +460,44 @@ class EvoChild(EvoDevice): day_of_week = int(day_time.strftime("%w")) # 0 is Sunday time_of_day = day_time.strftime("%H:%M:%S") - # Iterate today's switchpoints until past the current time of day... - day = self._schedule["DailySchedules"][day_of_week] - sp_idx = -1 # last switchpoint of the day before - for i, tmp in enumerate(day["Switchpoints"]): - if time_of_day > tmp["TimeOfDay"]: - sp_idx = i # current setpoint - else: - break + try: + # Iterate today's switchpoints until past the current time of day... + day = self._schedule["DailySchedules"][day_of_week] + sp_idx = -1 # last switchpoint of the day before + for i, tmp in enumerate(day["Switchpoints"]): + if time_of_day > tmp["TimeOfDay"]: + sp_idx = i # current setpoint + else: + break - # Did the current SP start yesterday? Does the next start SP tomorrow? - this_sp_day = -1 if sp_idx == -1 else 0 - next_sp_day = 1 if sp_idx + 1 == len(day["Switchpoints"]) else 0 + # Did the current SP start yesterday? Does the next start SP tomorrow? + this_sp_day = -1 if sp_idx == -1 else 0 + next_sp_day = 1 if sp_idx + 1 == len(day["Switchpoints"]) else 0 - for key, offset, idx in [ - ("this", this_sp_day, sp_idx), - ("next", next_sp_day, (sp_idx + 1) * (1 - next_sp_day)), - ]: - sp_date = (day_time + timedelta(days=offset)).strftime("%Y-%m-%d") - day = self._schedule["DailySchedules"][(day_of_week + offset) % 7] - switchpoint = day["Switchpoints"][idx] + for key, offset, idx in [ + ("this", this_sp_day, sp_idx), + ("next", next_sp_day, (sp_idx + 1) * (1 - next_sp_day)), + ]: + sp_date = (day_time + timedelta(days=offset)).strftime("%Y-%m-%d") + day = self._schedule["DailySchedules"][(day_of_week + offset) % 7] + switchpoint = day["Switchpoints"][idx] - dt_local_aware = _local_dt_to_aware( - dt_util.parse_datetime(f"{sp_date}T{switchpoint['TimeOfDay']}") + dt_local_aware = _local_dt_to_aware( + dt_util.parse_datetime(f"{sp_date}T{switchpoint['TimeOfDay']}") + ) + + self._setpoints[f"{key}_sp_from"] = dt_local_aware.isoformat() + try: + self._setpoints[f"{key}_sp_temp"] = switchpoint["heatSetpoint"] + except KeyError: + self._setpoints[f"{key}_sp_state"] = switchpoint["DhwState"] + + except IndexError: + self._setpoints = {} + _LOGGER.warning( + "Failed to get setpoints - please report as an issue", exc_info=True ) - self._setpoints[f"{key}_sp_from"] = dt_local_aware.isoformat() - try: - self._setpoints[f"{key}_sp_temp"] = switchpoint["heatSetpoint"] - except KeyError: - self._setpoints[f"{key}_sp_state"] = switchpoint["DhwState"] - return self._setpoints async def _update_schedule(self) -> None: @@ -454,6 +510,8 @@ class EvoChild(EvoDevice): self._evo_device.schedule(), refresh=False ) + _LOGGER.debug("Schedule['%s'] = %s", self.name, self._schedule) + async def async_update(self) -> None: """Get the latest state data.""" next_sp_from = self._setpoints.get("next_sp_from", "2000-01-01T00:00:00+00:00") diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index 7df2db1b17e..82a7001539d 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -1,6 +1,6 @@ """Support for Climate devices of (EMEA/EU-based) Honeywell TCC systems.""" import logging -from typing import Optional, List +from typing import List, Optional from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( @@ -14,26 +14,26 @@ from homeassistant.components.climate.const import ( PRESET_ECO, PRESET_HOME, PRESET_NONE, - SUPPORT_TARGET_TEMPERATURE, SUPPORT_PRESET_MODE, + SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import PRECISION_TENTHS from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.util.dt import parse_datetime -from . import CONF_LOCATION_IDX, EvoDevice, EvoChild +from . import CONF_LOCATION_IDX, EvoChild, EvoDevice from .const import ( DOMAIN, - EVO_RESET, EVO_AUTO, EVO_AUTOECO, EVO_AWAY, EVO_CUSTOM, EVO_DAYOFF, - EVO_HEATOFF, EVO_FOLLOW, - EVO_TEMPOVER, + EVO_HEATOFF, EVO_PERMOVER, + EVO_RESET, + EVO_TEMPOVER, ) _LOGGER = logging.getLogger(__name__) @@ -72,14 +72,13 @@ async def async_setup_platform( return broker = hass.data[DOMAIN]["broker"] - loc_idx = broker.params[CONF_LOCATION_IDX] _LOGGER.debug( "Found the Location/Controller (%s), id=%s, name=%s (location_idx=%s)", broker.tcs.modelType, broker.tcs.systemId, broker.tcs.location.name, - loc_idx, + broker.params[CONF_LOCATION_IDX], ) # special case of RoundModulation/RoundWireless (is a single zone system) @@ -148,9 +147,12 @@ class EvoZone(EvoChild, EvoClimateDevice): self._name = evo_device.name self._icon = "mdi:radiator" - self._precision = self._evo_device.setpointCapabilities["valueResolution"] self._supported_features = SUPPORT_PRESET_MODE | SUPPORT_TARGET_TEMPERATURE self._preset_modes = list(HA_PRESET_TO_EVO) + if evo_broker.client_v1: + self._precision = PRECISION_TENTHS + else: + self._precision = self._evo_device.setpointCapabilities["valueResolution"] @property def hvac_mode(self) -> str: diff --git a/homeassistant/components/evohome/manifest.json b/homeassistant/components/evohome/manifest.json index 5633880be35..0b112df42bb 100644 --- a/homeassistant/components/evohome/manifest.json +++ b/homeassistant/components/evohome/manifest.json @@ -3,7 +3,7 @@ "name": "Evohome", "documentation": "https://www.home-assistant.io/integrations/evohome", "requirements": [ - "evohome-async==0.3.3b4" + "evohome-async==0.3.4b1" ], "dependencies": [], "codeowners": ["@zxdavb"] diff --git a/homeassistant/components/evohome/water_heater.py b/homeassistant/components/evohome/water_heater.py index 37bdcd82afc..e29dbb49af2 100644 --- a/homeassistant/components/evohome/water_heater.py +++ b/homeassistant/components/evohome/water_heater.py @@ -7,7 +7,7 @@ from homeassistant.components.water_heater import ( SUPPORT_OPERATION_MODE, WaterHeaterDevice, ) -from homeassistant.const import PRECISION_WHOLE, STATE_OFF, STATE_ON +from homeassistant.const import PRECISION_TENTHS, PRECISION_WHOLE, STATE_OFF, STATE_ON from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.util.dt import parse_datetime @@ -55,7 +55,7 @@ class EvoDHW(EvoChild, WaterHeaterDevice): self._name = "DHW controller" self._icon = "mdi:thermometer-lines" - self._precision = PRECISION_WHOLE + self._precision = PRECISION_TENTHS if evo_broker.client_v1 else PRECISION_WHOLE self._supported_features = SUPPORT_AWAY_MODE | SUPPORT_OPERATION_MODE @property diff --git a/homeassistant/components/fan/reproduce_state.py b/homeassistant/components/fan/reproduce_state.py new file mode 100644 index 00000000000..1053861e2bf --- /dev/null +++ b/homeassistant/components/fan/reproduce_state.py @@ -0,0 +1,100 @@ +"""Reproduce an Fan state.""" +import asyncio +import logging +from types import MappingProxyType +from typing import Iterable, Optional + +from homeassistant.const import ( + ATTR_ENTITY_ID, + STATE_ON, + STATE_OFF, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.core import Context, State +from homeassistant.helpers.typing import HomeAssistantType + +from . import ( + DOMAIN, + ATTR_DIRECTION, + ATTR_OSCILLATING, + ATTR_SPEED, + SERVICE_OSCILLATE, + SERVICE_SET_DIRECTION, + SERVICE_SET_SPEED, +) + +_LOGGER = logging.getLogger(__name__) + +VALID_STATES = {STATE_ON, STATE_OFF} +ATTRIBUTES = { # attribute: service + ATTR_DIRECTION: SERVICE_SET_DIRECTION, + ATTR_OSCILLATING: SERVICE_OSCILLATE, + ATTR_SPEED: SERVICE_SET_SPEED, +} + + +async def _async_reproduce_state( + hass: HomeAssistantType, state: State, context: Optional[Context] = None +) -> None: + """Reproduce a single state.""" + cur_state = hass.states.get(state.entity_id) + + if cur_state is None: + _LOGGER.warning("Unable to find entity %s", state.entity_id) + return + + if state.state not in VALID_STATES: + _LOGGER.warning( + "Invalid state specified for %s: %s", state.entity_id, state.state + ) + return + + # Return if we are already at the right state. + if cur_state.state == state.state and all( + check_attr_equal(cur_state.attributes, state.attributes, attr) + for attr in ATTRIBUTES + ): + return + + service_data = {ATTR_ENTITY_ID: state.entity_id} + service_calls = {} # service: service_data + + if state.state == STATE_ON: + # The fan should be on + if cur_state.state != STATE_ON: + # Turn on the fan at first + service_calls[SERVICE_TURN_ON] = service_data + + for attr, service in ATTRIBUTES.items(): + # Call services to adjust the attributes + if attr in state.attributes and not check_attr_equal( + state.attributes, cur_state.attributes, attr + ): + data = service_data.copy() + data[attr] = state.attributes[attr] + service_calls[service] = data + + elif state.state == STATE_OFF: + service_calls[SERVICE_TURN_OFF] = service_data + + for service, data in service_calls.items(): + await hass.services.async_call( + DOMAIN, service, data, context=context, blocking=True + ) + + +async def async_reproduce_states( + hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None +) -> None: + """Reproduce Fan states.""" + await asyncio.gather( + *(_async_reproduce_state(hass, state, context) for state in states) + ) + + +def check_attr_equal( + attr1: MappingProxyType, attr2: MappingProxyType, attr_str: str +) -> bool: + """Return true if the given attributes are equal.""" + return attr1.get(attr_str) == attr2.get(attr_str) diff --git a/homeassistant/components/fan/services.yaml b/homeassistant/components/fan/services.yaml index 16d3742d9ab..0e3978690e6 100644 --- a/homeassistant/components/fan/services.yaml +++ b/homeassistant/components/fan/services.yaml @@ -42,7 +42,7 @@ toggle: fields: entity_id: description: Name(s) of the entities to toggle - exampl: 'fan.living_room' + example: 'fan.living_room' set_direction: description: Set the fan rotation. diff --git a/homeassistant/components/feedreader/__init__.py b/homeassistant/components/feedreader/__init__.py index 44ec95f8213..e4ec154620e 100644 --- a/homeassistant/components/feedreader/__init__.py +++ b/homeassistant/components/feedreader/__init__.py @@ -6,6 +6,7 @@ from threading import Lock import pickle import voluptuous as vol +import feedparser from homeassistant.const import EVENT_HOMEASSISTANT_START, CONF_SCAN_INTERVAL from homeassistant.helpers.event import track_time_interval @@ -87,8 +88,6 @@ class FeedManager: def _update(self): """Update the feed and publish new entries to the event bus.""" - import feedparser - _LOGGER.info("Fetching new data from feed %s", self._url) self._feed = feedparser.parse( self._url, diff --git a/homeassistant/components/ffmpeg/__init__.py b/homeassistant/components/ffmpeg/__init__.py index 51e1cac3859..673a34230fc 100644 --- a/homeassistant/components/ffmpeg/__init__.py +++ b/homeassistant/components/ffmpeg/__init__.py @@ -3,6 +3,7 @@ import logging import re import voluptuous as vol +from haffmpeg.tools import FFVersion from homeassistant.core import callback from homeassistant.const import ( @@ -105,7 +106,6 @@ class FFmpegManager: async def async_get_version(self): """Return ffmpeg version.""" - from haffmpeg.tools import FFVersion ffversion = FFVersion(self._bin, self.hass.loop) self._version = await ffversion.get_version() diff --git a/homeassistant/components/ffmpeg/camera.py b/homeassistant/components/ffmpeg/camera.py index 598ffe36bd4..0f500176933 100644 --- a/homeassistant/components/ffmpeg/camera.py +++ b/homeassistant/components/ffmpeg/camera.py @@ -3,6 +3,8 @@ import asyncio import logging import voluptuous as vol +from haffmpeg.camera import CameraMjpeg +from haffmpeg.tools import ImageFrame, IMAGE_JPEG from homeassistant.components.camera import PLATFORM_SCHEMA, Camera, SUPPORT_STREAM from homeassistant.const import CONF_NAME @@ -53,7 +55,6 @@ class FFmpegCamera(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._manager.binary, loop=self.hass.loop) @@ -66,7 +67,6 @@ class FFmpegCamera(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._input, extra_cmd=self._extra_arguments) diff --git a/homeassistant/components/fitbit/sensor.py b/homeassistant/components/fitbit/sensor.py index 534477d88cf..0d4b8d61e7a 100644 --- a/homeassistant/components/fitbit/sensor.py +++ b/homeassistant/components/fitbit/sensor.py @@ -4,6 +4,9 @@ import logging import datetime import time +from fitbit import Fitbit +from fitbit.api import FitbitOauth2Client +from oauthlib.oauth2.rfc6749.errors import MismatchingStateError, MissingTokenError import voluptuous as vol from homeassistant.core import callback @@ -234,13 +237,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if "fitbit" in _CONFIGURING: hass.components.configurator.request_done(_CONFIGURING.pop("fitbit")) - import fitbit - access_token = config_file.get(ATTR_ACCESS_TOKEN) refresh_token = config_file.get(ATTR_REFRESH_TOKEN) expires_at = config_file.get(ATTR_LAST_SAVED_AT) if None not in (access_token, refresh_token): - authd_client = fitbit.Fitbit( + authd_client = Fitbit( config_file.get(ATTR_CLIENT_ID), config_file.get(ATTR_CLIENT_SECRET), access_token=access_token, @@ -294,7 +295,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(dev, True) else: - oauth = fitbit.api.FitbitOauth2Client( + oauth = FitbitOauth2Client( config_file.get(ATTR_CLIENT_ID), config_file.get(ATTR_CLIENT_SECRET) ) @@ -337,9 +338,6 @@ class FitbitAuthCallbackView(HomeAssistantView): @callback def get(self, request): """Finish OAuth callback request.""" - from oauthlib.oauth2.rfc6749.errors import MismatchingStateError - from oauthlib.oauth2.rfc6749.errors import MissingTokenError - hass = request.app["hass"] data = request.query diff --git a/homeassistant/components/flic/binary_sensor.py b/homeassistant/components/flic/binary_sensor.py index 4fa97334889..416d39e5332 100644 --- a/homeassistant/components/flic/binary_sensor.py +++ b/homeassistant/components/flic/binary_sensor.py @@ -2,6 +2,14 @@ import logging import threading +from pyflic import ( + FlicClient, + ButtonConnectionChannel, + ClickType, + ConnectionStatus, + ScanWizard, + ScanWizardResult, +) import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -49,7 +57,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the flic platform.""" - import pyflic # Initialize flic client responsible for # connecting to buttons and retrieving events @@ -58,7 +65,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): discovery = config.get(CONF_DISCOVERY) try: - client = pyflic.FlicClient(host, port) + client = FlicClient(host, port) except ConnectionRefusedError: _LOGGER.error("Failed to connect to flic server") return @@ -88,15 +95,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None): def start_scanning(config, add_entities, client): """Start a new flic client for scanning and connecting to new buttons.""" - import pyflic - - scan_wizard = pyflic.ScanWizard() + scan_wizard = ScanWizard() def scan_completed_callback(scan_wizard, result, address, name): """Restart scan wizard to constantly check for new buttons.""" - if result == pyflic.ScanWizardResult.WizardSuccess: + if result == ScanWizardResult.WizardSuccess: _LOGGER.info("Found new button %s", address) - elif result != pyflic.ScanWizardResult.WizardFailedTimeout: + elif result != ScanWizardResult.WizardFailedTimeout: _LOGGER.warning( "Failed to connect to button %s. Reason: %s", address, result ) @@ -123,7 +128,6 @@ class FlicButton(BinarySensorDevice): def __init__(self, hass, client, address, timeout, ignored_click_types): """Initialize the flic button.""" - import pyflic self._hass = hass self._address = address @@ -131,10 +135,10 @@ class FlicButton(BinarySensorDevice): self._is_down = False self._ignored_click_types = ignored_click_types or [] self._hass_click_types = { - pyflic.ClickType.ButtonClick: CLICK_TYPE_SINGLE, - pyflic.ClickType.ButtonSingleClick: CLICK_TYPE_SINGLE, - pyflic.ClickType.ButtonDoubleClick: CLICK_TYPE_DOUBLE, - pyflic.ClickType.ButtonHold: CLICK_TYPE_HOLD, + ClickType.ButtonClick: CLICK_TYPE_SINGLE, + ClickType.ButtonSingleClick: CLICK_TYPE_SINGLE, + ClickType.ButtonDoubleClick: CLICK_TYPE_DOUBLE, + ClickType.ButtonHold: CLICK_TYPE_HOLD, } self._channel = self._create_channel() @@ -142,9 +146,7 @@ class FlicButton(BinarySensorDevice): def _create_channel(self): """Create a new connection channel to the button.""" - import pyflic - - channel = pyflic.ButtonConnectionChannel(self._address) + channel = ButtonConnectionChannel(self._address) channel.on_button_up_or_down = self._on_up_down # If all types of clicks should be ignored, skip registering callbacks @@ -212,12 +214,10 @@ class FlicButton(BinarySensorDevice): def _on_up_down(self, channel, click_type, was_queued, time_diff): """Update device state, if event was not queued.""" - import pyflic - if was_queued and self._queued_event_check(click_type, time_diff): return - self._is_down = click_type == pyflic.ClickType.ButtonDown + self._is_down = click_type == ClickType.ButtonDown self.schedule_update_ha_state() def _on_click(self, channel, click_type, was_queued, time_diff): @@ -243,9 +243,7 @@ class FlicButton(BinarySensorDevice): def _connection_status_changed(self, channel, connection_status, disconnect_reason): """Remove device, if button disconnects.""" - import pyflic - - if connection_status == pyflic.ConnectionStatus.Disconnected: + if connection_status == ConnectionStatus.Disconnected: _LOGGER.warning( "Button (%s) disconnected. Reason: %s", self.address, disconnect_reason ) diff --git a/homeassistant/components/flux/switch.py b/homeassistant/components/flux/switch.py index 800ccd1938f..7b58ffbe449 100644 --- a/homeassistant/components/flux/switch.py +++ b/homeassistant/components/flux/switch.py @@ -31,10 +31,12 @@ from homeassistant.const import ( CONF_LIGHTS, CONF_MODE, SERVICE_TURN_ON, + STATE_ON, SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET, ) 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 ( @@ -169,7 +171,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= hass.services.async_register(DOMAIN, service_name, async_update) -class FluxSwitch(SwitchDevice): +class FluxSwitch(SwitchDevice, RestoreEntity): """Representation of a Flux switch.""" def __init__( @@ -214,6 +216,12 @@ class FluxSwitch(SwitchDevice): """Return true if switch is on.""" return self.unsub_tracker is not None + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + last_state = await self.async_get_last_state() + if last_state and last_state.state == STATE_ON: + await self.async_turn_on() + async def async_turn_on(self, **kwargs): """Turn on flux.""" if self.is_on: diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index 0a95de783fa..5bd84cd157f 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -3,6 +3,7 @@ import logging import socket import random +from flux_led import BulbScanner, WifiLedBulb import voluptuous as vol from homeassistant.const import CONF_DEVICES, CONF_NAME, CONF_PROTOCOL, ATTR_MODE @@ -135,8 +136,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Flux lights.""" - import flux_led - lights = [] light_ips = [] @@ -156,7 +155,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return # Find the bulbs on the LAN - scanner = flux_led.BulbScanner() + scanner = BulbScanner() scanner.scan(timeout=10) for device in scanner.getBulbInfo(): ipaddr = device["ipaddr"] @@ -187,9 +186,8 @@ class FluxLight(Light): def _connect(self): """Connect to Flux light.""" - import flux_led - self._bulb = flux_led.WifiLedBulb(self._ipaddr, timeout=5) + self._bulb = WifiLedBulb(self._ipaddr, timeout=5) if self._protocol: self._bulb.setProtocol(self._protocol) diff --git a/homeassistant/components/foscam/camera.py b/homeassistant/components/foscam/camera.py index 63e9956d0df..0e2ca4073bf 100644 --- a/homeassistant/components/foscam/camera.py +++ b/homeassistant/components/foscam/camera.py @@ -1,11 +1,26 @@ """This component provides basic support for Foscam IP cameras.""" import logging +import asyncio + +from libpyfoscam import FoscamCamera import voluptuous as vol from homeassistant.components.camera import Camera, PLATFORM_SCHEMA, SUPPORT_STREAM -from homeassistant.const import CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_PORT +from homeassistant.const import ( + CONF_NAME, + CONF_USERNAME, + CONF_PASSWORD, + CONF_PORT, + ATTR_ENTITY_ID, +) 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 + _LOGGER = logging.getLogger(__name__) @@ -15,7 +30,32 @@ CONF_RTSP_PORT = "rtsp_port" DEFAULT_NAME = "Foscam Camera" DEFAULT_PORT = 88 -FOSCAM_COMM_ERROR = -8 +SERVICE_PTZ = "ptz" +ATTR_MOVEMENT = "movement" +ATTR_TRAVELTIME = "travel_time" + +DEFAULT_TRAVELTIME = 0.125 + +DIR_UP = "up" +DIR_DOWN = "down" +DIR_LEFT = "left" +DIR_RIGHT = "right" + +DIR_TOPLEFT = "top_left" +DIR_TOPRIGHT = "top_right" +DIR_BOTTOMLEFT = "bottom_left" +DIR_BOTTOMRIGHT = "bottom_right" + +MOVEMENT_ATTRS = { + DIR_UP: "ptz_move_up", + DIR_DOWN: "ptz_move_down", + DIR_LEFT: "ptz_move_left", + DIR_RIGHT: "ptz_move_right", + DIR_TOPLEFT: "ptz_move_top_left", + DIR_TOPRIGHT: "ptz_move_top_right", + DIR_BOTTOMLEFT: "ptz_move_bottom_left", + DIR_BOTTOMRIGHT: "ptz_move_bottom_right", +} PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -28,44 +68,114 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( } ) +SERVICE_PTZ_SCHEMA = vol.Schema( + { + vol.Required(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_MOVEMENT): vol.In( + [ + DIR_UP, + DIR_DOWN, + DIR_LEFT, + DIR_RIGHT, + DIR_TOPLEFT, + DIR_TOPRIGHT, + DIR_BOTTOMLEFT, + DIR_BOTTOMRIGHT, + ] + ), + vol.Optional(ATTR_TRAVELTIME, default=DEFAULT_TRAVELTIME): cv.small_float, + } +) -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 a Foscam IP Camera.""" - add_entities([FoscamCam(config)]) + + async def async_handle_ptz(service): + """Handle PTZ service call.""" + movement = service.data[ATTR_MOVEMENT] + travel_time = service.data[ATTR_TRAVELTIME] + entity_ids = await async_extract_entity_ids(hass, service) + + if not entity_ids: + return + + _LOGGER.debug("Moving '%s' camera(s): %s", movement, entity_ids) + + all_cameras = hass.data[FOSCAM_DATA][FOSCAM_ENTITIES] + target_cameras = [ + camera for camera in all_cameras if camera.entity_id in entity_ids + ] + + for camera in target_cameras: + await camera.async_perform_ptz(movement, travel_time) + + hass.services.async_register( + FOSCAM_DOMAIN, SERVICE_PTZ, async_handle_ptz, schema=SERVICE_PTZ_SCHEMA + ) + + camera = FoscamCamera( + config[CONF_IP], + config[CONF_PORT], + config[CONF_USERNAME], + config[CONF_PASSWORD], + verbose=False, + ) + + rtsp_port = config.get(CONF_RTSP_PORT) + if not rtsp_port: + ret, response = await hass.async_add_executor_job(camera.get_port_info) + + if ret == 0: + rtsp_port = response.get("rtspPort") or response.get("mediaPort") + + ret, response = await hass.async_add_executor_job(camera.get_motion_detect_config) + + motion_status = False + if ret != 0 and response == 1: + motion_status = True + + async_add_entities( + [ + HassFoscamCamera( + camera, + config[CONF_NAME], + config[CONF_USERNAME], + config[CONF_PASSWORD], + rtsp_port, + motion_status, + ) + ] + ) -class FoscamCam(Camera): +class HassFoscamCamera(Camera): """An implementation of a Foscam IP camera.""" - def __init__(self, device_info): + def __init__(self, camera, name, username, password, rtsp_port, motion_status): """Initialize a Foscam camera.""" - from libpyfoscam import FoscamCamera - super().__init__() - ip_address = device_info.get(CONF_IP) - port = device_info.get(CONF_PORT) - self._username = device_info.get(CONF_USERNAME) - self._password = device_info.get(CONF_PASSWORD) - self._name = device_info.get(CONF_NAME) - self._motion_status = False + self._foscam_session = camera + self._name = name + self._username = username + self._password = password + self._rtsp_port = rtsp_port + self._motion_status = motion_status - self._foscam_session = FoscamCamera( - ip_address, port, self._username, self._password, verbose=False + async def async_added_to_hass(self): + """Handle entity addition to hass.""" + entities = self.hass.data.setdefault(FOSCAM_DATA, {}).setdefault( + FOSCAM_ENTITIES, [] ) - - self._rtsp_port = device_info.get(CONF_RTSP_PORT) - if not self._rtsp_port: - result, response = self._foscam_session.get_port_info() - if result == 0: - self._rtsp_port = response.get("rtspPort") or response.get("mediaPort") + entities.append(self) def camera_image(self): """Return a still image response from the camera.""" # Send the request to snap a picture and return raw jpg data # Handle exception if host is not reachable or url failed result, response = self._foscam_session.snap_picture_2() - if result == FOSCAM_COMM_ERROR: + if result != 0: return None return response @@ -97,19 +207,47 @@ class FoscamCam(Camera): """Enable motion detection in camera.""" try: ret = self._foscam_session.enable_motion_detection() - self._motion_status = ret == FOSCAM_COMM_ERROR + + if ret != 0: + return + + self._motion_status = True except TypeError: _LOGGER.debug("Communication problem") - self._motion_status = False def disable_motion_detection(self): """Disable motion detection.""" try: ret = self._foscam_session.disable_motion_detection() - self._motion_status = ret == FOSCAM_COMM_ERROR + + if ret != 0: + return + + self._motion_status = False except TypeError: _LOGGER.debug("Communication problem") - self._motion_status = False + + async def async_perform_ptz(self, movement, travel_time): + """Perform a PTZ action on the camera.""" + _LOGGER.debug("PTZ action '%s' on %s", movement, self._name) + + movement_function = getattr(self._foscam_session, MOVEMENT_ATTRS[movement]) + + ret, _ = await self.hass.async_add_executor_job(movement_function) + + if ret != 0: + _LOGGER.error("Error moving %s '%s': %s", movement, self._name, ret) + return + + await asyncio.sleep(travel_time) + + ret, _ = await self.hass.async_add_executor_job( + self._foscam_session.ptz_stop_run + ) + + if ret != 0: + _LOGGER.error("Error stopping movement on '%s': %s", self._name, ret) + return @property def name(self): diff --git a/homeassistant/components/foscam/const.py b/homeassistant/components/foscam/const.py new file mode 100644 index 00000000000..63b4b74a763 --- /dev/null +++ b/homeassistant/components/foscam/const.py @@ -0,0 +1,5 @@ +"""Constants for Foscam component.""" + +DOMAIN = "foscam" +DATA = "foscam" +ENTITIES = "entities" diff --git a/homeassistant/components/foscam/manifest.json b/homeassistant/components/foscam/manifest.json index b2c44c113ee..6a47012ef84 100644 --- a/homeassistant/components/foscam/manifest.json +++ b/homeassistant/components/foscam/manifest.json @@ -6,5 +6,5 @@ "libpyfoscam==1.0" ], "dependencies": [], - "codeowners": [] + "codeowners": ["@skgsergio"] } diff --git a/homeassistant/components/foscam/services.yaml b/homeassistant/components/foscam/services.yaml new file mode 100644 index 00000000000..64e68dd5bc4 --- /dev/null +++ b/homeassistant/components/foscam/services.yaml @@ -0,0 +1,12 @@ +ptz: + description: Pan/Tilt service for Foscam camera. + fields: + entity_id: + description: Name(s) of entities to move. + example: 'camera.living_room_camera' + movement: + description: "Direction of the movement. Allowed values: up, down, left, right, top_left, top_right, bottom_left, bottom_right." + example: 'up' + travel_time: + description: "(Optional) Travel time in seconds. Allowed values: float from 0 to 1. Default: 0.125" + example: 0.125 diff --git a/homeassistant/components/fritz/device_tracker.py b/homeassistant/components/fritz/device_tracker.py index afe0aa3ed02..ab4deec96f7 100644 --- a/homeassistant/components/fritz/device_tracker.py +++ b/homeassistant/components/fritz/device_tracker.py @@ -1,15 +1,16 @@ """Support for FRITZ!Box routers.""" import logging +from fritzconnection import FritzHosts # pylint: disable=import-error 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__) @@ -41,11 +42,9 @@ class FritzBoxScanner(DeviceScanner): self.password = config[CONF_PASSWORD] self.success_init = True - import fritzconnection as fc # pylint: disable=import-error - # Establish a connection to the FRITZ!Box. try: - self.fritz_box = fc.FritzHosts( + self.fritz_box = FritzHosts( address=self.host, user=self.username, password=self.password ) except (ValueError, TypeError): diff --git a/homeassistant/components/fritz/manifest.json b/homeassistant/components/fritz/manifest.json index e6c1fee2c95..15a3406891f 100644 --- a/homeassistant/components/fritz/manifest.json +++ b/homeassistant/components/fritz/manifest.json @@ -3,7 +3,7 @@ "name": "Fritz", "documentation": "https://www.home-assistant.io/integrations/fritz", "requirements": [ - "fritzconnection==0.6.5" + "fritzconnection==0.8.4" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index a053bc6c7ca..40aa3a881d1 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -1,9 +1,9 @@ """Support for AVM Fritz!Box smarthome devices.""" import logging +from pyfritzhome import Fritzhome, LoginError import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( CONF_DEVICES, CONF_HOST, @@ -12,6 +12,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, ) from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -52,7 +53,6 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): """Set up the fritzbox component.""" - from pyfritzhome import Fritzhome, LoginError fritz_list = [] diff --git a/homeassistant/components/fritzbox_callmonitor/manifest.json b/homeassistant/components/fritzbox_callmonitor/manifest.json index 35c27b7ca84..f85f16d6c0d 100644 --- a/homeassistant/components/fritzbox_callmonitor/manifest.json +++ b/homeassistant/components/fritzbox_callmonitor/manifest.json @@ -3,7 +3,7 @@ "name": "Fritzbox callmonitor", "documentation": "https://www.home-assistant.io/integrations/fritzbox_callmonitor", "requirements": [ - "fritzconnection==0.6.5" + "fritzconnection==0.8.4" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/fritzbox_callmonitor/sensor.py b/homeassistant/components/fritzbox_callmonitor/sensor.py index 4dada44f4e5..b1d601ce382 100644 --- a/homeassistant/components/fritzbox_callmonitor/sensor.py +++ b/homeassistant/components/fritzbox_callmonitor/sensor.py @@ -1,24 +1,25 @@ """Sensor to monitor incoming/outgoing phone calls on a Fritz!Box router.""" +import datetime import logging +import re import socket import threading -import datetime import time -import re +import fritzconnection as fc # pylint: disable=import-error import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_HOST, - CONF_PORT, CONF_NAME, CONF_PASSWORD, + CONF_PORT, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -248,8 +249,6 @@ class FritzBoxPhonebook: self.number_dict = None self.prefixes = prefixes or [] - import fritzconnection as fc # pylint: disable=import-error - # Establish a connection to the FRITZ!Box. self.fph = fc.FritzPhonebook( address=self.host, user=self.username, password=self.password diff --git a/homeassistant/components/fritzbox_netmonitor/manifest.json b/homeassistant/components/fritzbox_netmonitor/manifest.json index 88a7ab5a338..9afaa71e699 100644 --- a/homeassistant/components/fritzbox_netmonitor/manifest.json +++ b/homeassistant/components/fritzbox_netmonitor/manifest.json @@ -3,7 +3,7 @@ "name": "Fritzbox netmonitor", "documentation": "https://www.home-assistant.io/integrations/fritzbox_netmonitor", "requirements": [ - "fritzconnection==0.6.5" + "fritzconnection==0.8.4" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/fritzbox_netmonitor/sensor.py b/homeassistant/components/fritzbox_netmonitor/sensor.py index 9d07e7a8055..0a82c5e29c3 100644 --- a/homeassistant/components/fritzbox_netmonitor/sensor.py +++ b/homeassistant/components/fritzbox_netmonitor/sensor.py @@ -1,14 +1,18 @@ """Support for monitoring an AVM Fritz!Box router.""" -import logging from datetime import timedelta -from requests.exceptions import RequestException +import logging +from fritzconnection import FritzStatus # pylint: disable=import-error +from fritzconnection.fritzconnection import ( # pylint: disable=import-error + FritzConnectionException, +) +from requests.exceptions import RequestException import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, CONF_HOST, STATE_UNAVAILABLE -from homeassistant.helpers.entity import Entity +from homeassistant.const import CONF_HOST, CONF_NAME, STATE_UNAVAILABLE import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -45,15 +49,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the FRITZ!Box monitor sensors.""" - # pylint: disable=import-error - import fritzconnection as fc - from fritzconnection.fritzconnection import FritzConnectionException - name = config.get(CONF_NAME) host = config.get(CONF_HOST) try: - fstatus = fc.FritzStatus(address=host) + fstatus = FritzStatus(address=host) except (ValueError, TypeError, FritzConnectionException): fstatus = None diff --git a/homeassistant/components/fritzdect/switch.py b/homeassistant/components/fritzdect/switch.py index dcb700d6636..cc629c54dc3 100644 --- a/homeassistant/components/fritzdect/switch.py +++ b/homeassistant/components/fritzdect/switch.py @@ -1,20 +1,21 @@ """Support for FRITZ!DECT Switches.""" import logging -from requests.exceptions import RequestException, HTTPError - +from fritzhome.fritz import FritzBox +from requests.exceptions import HTTPError, RequestException import voluptuous as vol -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import ( + ATTR_TEMPERATURE, CONF_HOST, CONF_PASSWORD, CONF_USERNAME, - POWER_WATT, ENERGY_KILO_WATT_HOUR, + POWER_WATT, + TEMP_CELSIUS, ) import homeassistant.helpers.config_validation as cv -from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE _LOGGER = logging.getLogger(__name__) @@ -42,7 +43,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Add all switches connected to Fritz Box.""" - from fritzhome.fritz import FritzBox host = config.get(CONF_HOST) username = config.get(CONF_USERNAME) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index e46423c8271..541d1bf473d 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -6,23 +6,23 @@ import os import pathlib from typing import Any, Dict, Optional, Set, Tuple -from aiohttp import web, web_urldispatcher, hdrs -import voluptuous as vol +from aiohttp import hdrs, web, web_urldispatcher +import hass_frontend import jinja2 +import voluptuous as vol from yarl import URL -import homeassistant.helpers.config_validation as cv -from homeassistant.components.http.view import HomeAssistantView from homeassistant.components import websocket_api +from homeassistant.components.http.view import HomeAssistantView from homeassistant.config import find_config_file, load_yaml_config_file from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass from .storage import async_setup_frontend_storage - # mypy: allow-untyped-defs, no-check-untyped-defs # Fix mimetypes for borked Windows machines @@ -242,8 +242,6 @@ def _frontend_root(dev_repo_path): if dev_repo_path is not None: return pathlib.Path(dev_repo_path) / "hass_frontend" - import hass_frontend - return hass_frontend.where() diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 67a66bc9612..aa7ad8b18f9 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==20191002.2" + "home-assistant-frontend==20191025.1" ], "dependencies": [ "api", diff --git a/homeassistant/components/frontier_silicon/media_player.py b/homeassistant/components/frontier_silicon/media_player.py index 8ab379b050b..010420d0f98 100644 --- a/homeassistant/components/frontier_silicon/media_player.py +++ b/homeassistant/components/frontier_silicon/media_player.py @@ -1,9 +1,11 @@ """Support for Frontier Silicon Devices (Medion, Hama, Auna,...).""" import logging +from afsapi import AFSAPI +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, @@ -64,8 +66,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Frontier Silicon platform.""" - import requests - if discovery_info is not None: async_add_entities( [AFSAPIDevice(discovery_info["ssdp_description"], DEFAULT_PASSWORD)], True @@ -118,8 +118,6 @@ class AFSAPIDevice(MediaPlayerDevice): connected to the device in between the updates and invalidated the existing session (i.e UNDOK). """ - from afsapi import AFSAPI - return AFSAPI(self._device_url, self._password) @property diff --git a/homeassistant/components/futurenow/light.py b/homeassistant/components/futurenow/light.py index eba768f82e3..7b9e79dbb3e 100644 --- a/homeassistant/components/futurenow/light.py +++ b/homeassistant/components/futurenow/light.py @@ -2,15 +2,16 @@ import logging +import pyfnip import voluptuous as vol -from homeassistant.const import CONF_NAME, CONF_HOST, CONF_PORT, CONF_DEVICES from homeassistant.components.light import ( ATTR_BRIGHTNESS, + PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, Light, - PLATFORM_SCHEMA, ) +from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_NAME, CONF_PORT import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -68,8 +69,6 @@ class FutureNowLight(Light): def __init__(self, device): """Initialize the light.""" - import pyfnip - self._name = device["name"] self._dimmable = device["dimmable"] self._channel = device["channel"] diff --git a/homeassistant/components/gc100/__init__.py b/homeassistant/components/gc100/__init__.py index 19303fdc6d2..36779b28df2 100644 --- a/homeassistant/components/gc100/__init__.py +++ b/homeassistant/components/gc100/__init__.py @@ -1,9 +1,10 @@ """Support for controlling Global Cache gc100.""" import logging +import gc100 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 import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -31,8 +32,6 @@ CONFIG_SCHEMA = vol.Schema( # pylint: disable=no-member def setup(hass, base_config): """Set up the gc100 component.""" - import gc100 - config = base_config[DOMAIN] host = config[CONF_HOST] port = config[CONF_PORT] diff --git a/homeassistant/components/geniushub/__init__.py b/homeassistant/components/geniushub/__init__.py index d9f6c877cbc..b34c46a9f26 100644 --- a/homeassistant/components/geniushub/__init__.py +++ b/homeassistant/components/geniushub/__init__.py @@ -4,9 +4,8 @@ import logging from typing import Any, Dict, Optional import aiohttp -import voluptuous as vol - from geniushubclient import GeniusHub +import voluptuous as vol from homeassistant.const import ( ATTR_TEMPERATURE, @@ -22,8 +21,8 @@ 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.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 @@ -214,7 +213,7 @@ class GeniusZone(GeniusEntity): super().__init__() self._zone = zone - self._unique_id = f"{broker.hub_uid}_device_{zone.id}" + self._unique_id = f"{broker.hub_uid}_zone_{zone.id}" self._max_temp = self._min_temp = self._supported_features = None diff --git a/homeassistant/components/geniushub/climate.py b/homeassistant/components/geniushub/climate.py index f27b1cc7f1a..9a19edd9f8b 100644 --- a/homeassistant/components/geniushub/climate.py +++ b/homeassistant/components/geniushub/climate.py @@ -1,14 +1,17 @@ """Support for Genius Hub climate devices.""" -from typing import Optional, List +from typing import List, Optional from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( - HVAC_MODE_OFF, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + CURRENT_HVAC_OFF, HVAC_MODE_HEAT, - PRESET_BOOST, + HVAC_MODE_OFF, PRESET_ACTIVITY, - SUPPORT_TARGET_TEMPERATURE, + PRESET_BOOST, SUPPORT_PRESET_MODE, + SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.helpers.typing import ConfigType, HomeAssistantType @@ -68,6 +71,17 @@ class GeniusClimateZone(GeniusZone, ClimateDevice): """Return the list of available hvac operation modes.""" return list(HA_HVAC_TO_GH) + @property + def hvac_action(self) -> Optional[str]: + """Return the current running hvac operation if supported.""" + if "_state" in self._zone.data: # only for v3 API + if not self._zone.data["_state"].get("bIsActive"): + return CURRENT_HVAC_OFF + if self._zone.data["_state"].get("bOutRequestHeat"): + return CURRENT_HVAC_HEAT + return CURRENT_HVAC_IDLE + return None + @property def preset_mode(self) -> Optional[str]: """Return the current preset mode, e.g., home, away, temp.""" diff --git a/homeassistant/components/geniushub/manifest.json b/homeassistant/components/geniushub/manifest.json index 96497388a48..f9e8e6eb4f0 100644 --- a/homeassistant/components/geniushub/manifest.json +++ b/homeassistant/components/geniushub/manifest.json @@ -3,7 +3,7 @@ "name": "Genius Hub", "documentation": "https://www.home-assistant.io/integrations/geniushub", "requirements": [ - "geniushub-client==0.6.26" + "geniushub-client==0.6.28" ], "dependencies": [], "codeowners": ["@zxdavb"] diff --git a/homeassistant/components/geniushub/sensor.py b/homeassistant/components/geniushub/sensor.py index 2f5d9bceb8b..bd73c700e65 100644 --- a/homeassistant/components/geniushub/sensor.py +++ b/homeassistant/components/geniushub/sensor.py @@ -94,6 +94,8 @@ class GeniusIssue(GeniusEntity): super().__init__() self._hub = broker.client + self._unique_id = f"{broker.hub_uid}_{GH_LEVEL_MAPPING[level]}" + self._name = f"GeniusHub {GH_LEVEL_MAPPING[level]}" self._level = level self._issues = [] diff --git a/homeassistant/components/geniushub/water_heater.py b/homeassistant/components/geniushub/water_heater.py index cd4f536e14f..4141e9f8c04 100644 --- a/homeassistant/components/geniushub/water_heater.py +++ b/homeassistant/components/geniushub/water_heater.py @@ -2,9 +2,9 @@ from typing import List from homeassistant.components.water_heater import ( - WaterHeaterDevice, - SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, + SUPPORT_TARGET_TEMPERATURE, + WaterHeaterDevice, ) from homeassistant.const import STATE_OFF from homeassistant.helpers.typing import ConfigType, HomeAssistantType diff --git a/homeassistant/components/geo_rss_events/manifest.json b/homeassistant/components/geo_rss_events/manifest.json index 8fd19f6b034..c681807ad01 100644 --- a/homeassistant/components/geo_rss_events/manifest.json +++ b/homeassistant/components/geo_rss_events/manifest.json @@ -1,10 +1,12 @@ { "domain": "geo_rss_events", - "name": "Geo rss events", + "name": "Geo RSS events", "documentation": "https://www.home-assistant.io/integrations/geo_rss_events", "requirements": [ "georss_generic_client==0.2" ], "dependencies": [], - "codeowners": [] + "codeowners": [ + "@exxamalte" + ] } diff --git a/homeassistant/components/geo_rss_events/sensor.py b/homeassistant/components/geo_rss_events/sensor.py index 9f336668142..39e6c5c7e82 100644 --- a/homeassistant/components/geo_rss_events/sensor.py +++ b/homeassistant/components/geo_rss_events/sensor.py @@ -12,6 +12,8 @@ import logging from datetime import timedelta import voluptuous as vol +from georss_client import UPDATE_OK, UPDATE_OK_NO_DATA +from georss_client.generic_feed import GenericFeed import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -108,7 +110,6 @@ class GeoRssServiceSensor(Entity): self._state = None self._state_attributes = None self._unit_of_measurement = unit_of_measurement - from georss_client.generic_feed import GenericFeed self._feed = GenericFeed( coordinates, @@ -146,10 +147,9 @@ class GeoRssServiceSensor(Entity): def update(self): """Update this sensor from the GeoRSS service.""" - import georss_client status, feed_entries = self._feed.update() - if status == georss_client.UPDATE_OK: + if status == UPDATE_OK: _LOGGER.debug( "Adding events to sensor %s: %s", self.entity_id, feed_entries ) @@ -159,7 +159,7 @@ class GeoRssServiceSensor(Entity): for entry in feed_entries: matrix[entry.title] = f"{entry.distance_to_home:.0f}km" self._state_attributes = matrix - elif status == georss_client.UPDATE_OK_NO_DATA: + elif status == UPDATE_OK_NO_DATA: _LOGGER.debug("Update successful, but no data received from %s", self._feed) # Don't change the state or state attributes. else: diff --git a/homeassistant/components/github/manifest.json b/homeassistant/components/github/manifest.json index 0b5e3c0df9f..02593bf603d 100644 --- a/homeassistant/components/github/manifest.json +++ b/homeassistant/components/github/manifest.json @@ -3,7 +3,7 @@ "name": "Github", "documentation": "https://www.home-assistant.io/integrations/github", "requirements": [ - "PyGithub==1.43.5" + "PyGithub==1.43.8" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/github/sensor.py b/homeassistant/components/github/sensor.py index a85364ebeca..5e8200b41ab 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 from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -148,8 +149,6 @@ class GitHubData: def __init__(self, repository, access_token=None, server_url=None): """Set up GitHub.""" - import github - self._github = github self.setup_error = False diff --git a/homeassistant/components/gitlab_ci/sensor.py b/homeassistant/components/gitlab_ci/sensor.py index d8055c88f30..9edbe9733a8 100644 --- a/homeassistant/components/gitlab_ci/sensor.py +++ b/homeassistant/components/gitlab_ci/sensor.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +from gitlab import Gitlab, GitlabAuthenticationError, GitlabGetError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -141,12 +142,10 @@ class GitLabData: def __init__(self, gitlab_id, priv_token, interval, url): """Fetch data from GitLab API for most recent CI job.""" - import gitlab self._gitlab_id = gitlab_id - self._gitlab = gitlab.Gitlab(url, private_token=priv_token, per_page=1) + self._gitlab = Gitlab(url, private_token=priv_token, per_page=1) self._gitlab.auth() - self._gitlab_exceptions = gitlab.exceptions self.update = Throttle(interval)(self._update) self.available = False @@ -174,9 +173,9 @@ class GitLabData: self.build_id = _last_job.attributes.get("id") self.branch = _last_job.attributes.get("ref") self.available = True - except self._gitlab_exceptions.GitlabAuthenticationError as erra: + except GitlabAuthenticationError as erra: _LOGGER.error("Authentication Error: %s", erra) self.available = False - except self._gitlab_exceptions.GitlabGetError as errg: + except GitlabGetError as errg: _LOGGER.error("Project Not Found: %s", errg) self.available = False diff --git a/homeassistant/components/glances/.translations/ca.json b/homeassistant/components/glances/.translations/ca.json new file mode 100644 index 00000000000..edff236623e --- /dev/null +++ b/homeassistant/components/glances/.translations/ca.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "L'amfitri\u00f3 ja est\u00e0 configurat." + }, + "error": { + "cannot_connect": "No s'ha pogut connectar amb l'amfitri\u00f3", + "wrong_version": "Versi\u00f3 no compatible (2 o 3 necess\u00e0ria)" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3", + "name": "Nom", + "password": "Contrasenya", + "port": "Port", + "ssl": "Utilitza SSL/TLS per connectar-te al sistema Glances", + "username": "Nom d'usuari", + "verify_ssl": "Verifica la certificaci\u00f3 del sistema", + "version": "Versi\u00f3 de l'API de Glances (2 o 3)" + }, + "title": "Configuraci\u00f3 de Glances" + } + }, + "title": "Glances" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Freq\u00fc\u00e8ncia d\u2019actualitzaci\u00f3" + }, + "description": "Opcions de configuraci\u00f3 per a Glances" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/.translations/da.json b/homeassistant/components/glances/.translations/da.json new file mode 100644 index 00000000000..7779c6e40a0 --- /dev/null +++ b/homeassistant/components/glances/.translations/da.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "V\u00e6rten er allerede konfigureret." + }, + "error": { + "cannot_connect": "Kunne ikke oprette forbindelse til v\u00e6rt", + "wrong_version": "Version underst\u00f8ttes ikke (kun 2 eller 3)" + }, + "step": { + "user": { + "data": { + "host": "V\u00e6rt", + "name": "Navn", + "password": "Adgangskode", + "port": "Port", + "ssl": "Brug SSL/TLS til at oprette forbindelse til Glances-systemet", + "username": "Brugernavn", + "verify_ssl": "Bekr\u00e6ft certificering af systemet", + "version": "Glances API version (2 eller 3)" + }, + "title": "Ops\u00e6tning af Glances" + } + }, + "title": "Glances" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Opdateringsfrekvens" + }, + "description": "Konfigurationsindstillinger for Glances" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/.translations/de.json b/homeassistant/components/glances/.translations/de.json new file mode 100644 index 00000000000..04fed0fdc49 --- /dev/null +++ b/homeassistant/components/glances/.translations/de.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Host ist bereits konfiguriert." + }, + "error": { + "cannot_connect": "Verbindung zum Host nicht m\u00f6glich", + "wrong_version": "Version nicht unterst\u00fctzt (nur 2 oder 3)" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Name", + "password": "Passwort", + "port": "Port", + "username": "Benutzername" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Aktualisierungsfrequenz" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/.translations/en.json b/homeassistant/components/glances/.translations/en.json new file mode 100644 index 00000000000..ef1a8fb5e31 --- /dev/null +++ b/homeassistant/components/glances/.translations/en.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Host is already configured." + }, + "error": { + "cannot_connect": "Unable to connect to host", + "wrong_version": "Version not supported (2 or 3 only)" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Name", + "password": "Password", + "port": "Port", + "ssl": "Use SSL/TLS to connect to the Glances system", + "username": "Username", + "verify_ssl": "Verify the certification of the system", + "version": "Glances API Version (2 or 3)" + }, + "title": "Setup Glances" + } + }, + "title": "Glances" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Update frequency" + }, + "description": "Configure options for Glances" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/.translations/es.json b/homeassistant/components/glances/.translations/es.json new file mode 100644 index 00000000000..1b6b0335192 --- /dev/null +++ b/homeassistant/components/glances/.translations/es.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "El host ya est\u00e1 configurado." + }, + "error": { + "cannot_connect": "No se puede conectar al host", + "wrong_version": "Versi\u00f3n no soportada (s\u00f3lo 2 o 3)" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Nombre", + "password": "Contrase\u00f1a", + "port": "Puerto", + "ssl": "Utilice SSL/TLS para conectarse al sistema Glances", + "username": "Nombre de usuario", + "verify_ssl": "Verificar la certificaci\u00f3n del sistema", + "version": "Versi\u00f3n API Glances (2 o 3)" + }, + "title": "Configurar Glances" + } + }, + "title": "Glances" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Frecuencia de actualizaci\u00f3n" + }, + "description": "Configurar opciones para Glances" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/.translations/fi.json b/homeassistant/components/glances/.translations/fi.json new file mode 100644 index 00000000000..43ccf405d14 --- /dev/null +++ b/homeassistant/components/glances/.translations/fi.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Nimi", + "password": "Salasana", + "port": "portti" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/.translations/fr.json b/homeassistant/components/glances/.translations/fr.json new file mode 100644 index 00000000000..0391012c4cd --- /dev/null +++ b/homeassistant/components/glances/.translations/fr.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "L'h\u00f4te est d\u00e9j\u00e0 configur\u00e9." + }, + "error": { + "cannot_connect": "Impossible de se connecter \u00e0 l'h\u00f4te", + "wrong_version": "Version non prise en charge (2 ou 3 uniquement)" + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te", + "name": "Nom", + "password": "Mot de passe", + "port": "Port", + "ssl": "V\u00e9rifier la certification du syst\u00e8me", + "username": "Nom d'utilisateur", + "verify_ssl": "V\u00e9rifier la certification du syst\u00e8me", + "version": "Glances API Version (2 ou 3)" + }, + "title": "Installation de Glances" + } + }, + "title": "Glances" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Fr\u00e9quence de mise \u00e0 jour" + }, + "description": "Configurer les options pour Glances" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/.translations/ko.json b/homeassistant/components/glances/.translations/ko.json new file mode 100644 index 00000000000..ad19b589d5d --- /dev/null +++ b/homeassistant/components/glances/.translations/ko.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "\ud638\uc2a4\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, + "error": { + "cannot_connect": "\ud638\uc2a4\ud2b8\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", + "wrong_version": "\ud574\ub2f9 \ubc84\uc804\uc740 \uc9c0\uc6d0\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4 (2 \ub610\ub294 3\ub9cc \uc9c0\uc6d0)" + }, + "step": { + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "name": "\uc774\ub984", + "password": "\ube44\ubc00\ubc88\ud638", + "port": "\ud3ec\ud2b8", + "ssl": "SSL/TLS \ub97c \uc0ac\uc6a9\ud558\uc5ec Glances \uc2dc\uc2a4\ud15c\uc5d0 \uc5f0\uacb0", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984", + "verify_ssl": "\uc2dc\uc2a4\ud15c \uc778\uc99d \ud655\uc778", + "version": "Glances API \ubc84\uc804 (2 \ub610\ub294 3)" + }, + "title": "Glances \uc124\uce58" + } + }, + "title": "Glances" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\uc5c5\ub370\uc774\ud2b8 \ube48\ub3c4" + }, + "description": "Glances \uc635\uc158 \uad6c\uc131" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/.translations/lb.json b/homeassistant/components/glances/.translations/lb.json new file mode 100644 index 00000000000..06723a4bd12 --- /dev/null +++ b/homeassistant/components/glances/.translations/lb.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert" + }, + "error": { + "cannot_connect": "Kann sech net mam Server verbannen.", + "wrong_version": "Versioun net \u00ebnnerst\u00ebtzt (n\u00ebmmen 2 oder 3)" + }, + "step": { + "user": { + "data": { + "host": "Apparat", + "name": "Numm", + "password": "Passwuert", + "port": "Port", + "ssl": "Benotzt SSL/TLS fir sech mam Usiichte System ze verbannen", + "username": "Benotzernumm", + "verify_ssl": "Zertifikatioun vum System iwwerpr\u00e9iwen", + "version": "API Versioun vun den Usiichten (2 oder 3)" + }, + "title": "Usiichten konfigur\u00e9ieren" + } + }, + "title": "Usiichten" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Intervalle vun de Mise \u00e0 jour" + }, + "description": "Optioune konfigur\u00e9ieren fir d'Usiichten" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/.translations/nl.json b/homeassistant/components/glances/.translations/nl.json new file mode 100644 index 00000000000..7de81bfee98 --- /dev/null +++ b/homeassistant/components/glances/.translations/nl.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Host is al geconfigureerd." + }, + "error": { + "cannot_connect": "Kan geen verbinding maken met host", + "wrong_version": "Versie niet ondersteund (alleen 2 of 3)" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Naam", + "password": "Wachtwoord", + "port": "Poort", + "ssl": "Gebruik SSL / TLS om verbinding te maken met het Glances-systeem", + "username": "Gebruikersnaam", + "verify_ssl": "Controleer de certificering van het systeem", + "version": "Glances API-versie (2 of 3)" + }, + "title": "Glances instellen" + } + }, + "title": "Glances" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Update frequentie" + }, + "description": "Configureer opties voor Glances" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/.translations/no.json b/homeassistant/components/glances/.translations/no.json new file mode 100644 index 00000000000..7cf28cc34d0 --- /dev/null +++ b/homeassistant/components/glances/.translations/no.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Verten er allerede konfigurert." + }, + "error": { + "cannot_connect": "Kan ikke koble til vert", + "wrong_version": "Versjonen st\u00f8ttes ikke (bare 2 eller 3)" + }, + "step": { + "user": { + "data": { + "host": "Vert", + "name": "Navn", + "password": "Passord", + "port": "Port", + "ssl": "Bruk SSL / TLS for \u00e5 koble til Glances-systemet", + "username": "Brukernavn", + "verify_ssl": "Bekreft sertifiseringen av systemet", + "version": "Glances API-versjon (2 eller 3)" + }, + "title": "Oppsett av Glances" + } + }, + "title": "Glances" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Oppdater frekvens" + }, + "description": "Konfigurasjonsalternativer for Glances" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/.translations/pl.json b/homeassistant/components/glances/.translations/pl.json new file mode 100644 index 00000000000..21052c7acdc --- /dev/null +++ b/homeassistant/components/glances/.translations/pl.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Host jest ju\u017c skonfigurowany." + }, + "error": { + "cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z hostem", + "wrong_version": "Wersja nieobs\u0142ugiwana (tylko 2 lub 3)" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Nazwa", + "password": "Has\u0142o", + "port": "Port", + "ssl": "U\u017cyj SSL/TLS, aby po\u0142\u0105czy\u0107 si\u0119 z systemem Glances", + "username": "Nazwa u\u017cytkownika" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/.translations/ru.json b/homeassistant/components/glances/.translations/ru.json new file mode 100644 index 00000000000..8effcc6ab16 --- /dev/null +++ b/homeassistant/components/glances/.translations/ru.json @@ -0,0 +1,37 @@ +{ + "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." + }, + "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 \u0445\u043e\u0441\u0442\u0443.", + "wrong_version": "\u041f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u044e\u0442\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u0432\u0435\u0440\u0441\u0438\u0438 2 \u0438 3." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "ssl": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c SSL / TLS \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f", + "username": "\u041b\u043e\u0433\u0438\u043d", + "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e \u0441\u0438\u0441\u0442\u0435\u043c\u044b", + "version": "\u0412\u0435\u0440\u0441\u0438\u044f API Glances (2 \u0438\u043b\u0438 3)" + }, + "title": "Glances" + } + }, + "title": "Glances" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f" + }, + "description": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b Glances" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/.translations/sl.json b/homeassistant/components/glances/.translations/sl.json new file mode 100644 index 00000000000..b1d0fda94b5 --- /dev/null +++ b/homeassistant/components/glances/.translations/sl.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Gostitelj je \u017ee konfiguriran." + }, + "error": { + "cannot_connect": "Ni mogo\u010de vzpostaviti povezave z gostiteljem", + "wrong_version": "Razli\u010dica ni podprta (samo 2 ali 3)" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Ime", + "password": "Geslo", + "port": "Vrata", + "ssl": "Za povezavo s sistemom Glances uporabite SSL/TLS", + "username": "Uporabni\u0161ko ime", + "verify_ssl": "Preverite veljavnost potrdila sistema", + "version": "Glances API Version (2 ali 3)" + }, + "title": "Nastavite Glances" + } + }, + "title": "Glances" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Pogostost posodabljanja" + }, + "description": "Konfiguracija mo\u017enosti za Glances" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/.translations/th.json b/homeassistant/components/glances/.translations/th.json new file mode 100644 index 00000000000..718c857c490 --- /dev/null +++ b/homeassistant/components/glances/.translations/th.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "\u0e0a\u0e37\u0e48\u0e2d\u0e1c\u0e39\u0e49\u0e43\u0e0a\u0e49" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/.translations/zh-Hant.json b/homeassistant/components/glances/.translations/zh-Hant.json new file mode 100644 index 00000000000..12ba7670355 --- /dev/null +++ b/homeassistant/components/glances/.translations/zh-Hant.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "\u4e3b\u6a5f\u7aef\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002" + }, + "error": { + "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3\u4e3b\u6a5f\u7aef", + "wrong_version": "\u7248\u672c\u4e0d\u652f\u63f4\uff08\u50c5 2 \u6216 3\uff09" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "name": "\u540d\u7a31", + "password": "\u5bc6\u78bc", + "port": "\u901a\u8a0a\u57e0", + "ssl": "\u4f7f\u7528 SSL/TLS \u9023\u7dda\u81f3 Glances \u7cfb\u7d71", + "username": "\u4f7f\u7528\u8005\u540d\u7a31", + "verify_ssl": "\u9a57\u8b49\u7cfb\u7d71\u8a8d\u8b49", + "version": "Glances API \u7248\u672c\uff082 \u6216 3\uff09" + }, + "title": "\u8a2d\u5b9a Glances" + } + }, + "title": "Glances" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u66f4\u65b0\u983b\u7387" + }, + "description": "Glances \u8a2d\u5b9a\u9078\u9805" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/__init__.py b/homeassistant/components/glances/__init__.py index b458d8788fc..d09aa782534 100644 --- a/homeassistant/components/glances/__init__.py +++ b/homeassistant/components/glances/__init__.py @@ -1 +1,174 @@ -"""The glances component.""" +"""The Glances component.""" +from datetime import timedelta +import logging + +from glances_api import Glances, exceptions +import voluptuous as vol + +from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_SCAN_INTERVAL, + CONF_SSL, + CONF_USERNAME, + CONF_VERIFY_SSL, +) +from homeassistant.core import Config, HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.event import async_track_time_interval + +from .const import ( + CONF_VERSION, + DATA_UPDATED, + DEFAULT_HOST, + DEFAULT_NAME, + DEFAULT_PORT, + DEFAULT_SCAN_INTERVAL, + DEFAULT_VERSION, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + +GLANCES_SCHEMA = vol.All( + vol.Schema( + { + vol.Required(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_USERNAME): cv.string, + vol.Optional(CONF_PASSWORD): cv.string, + vol.Optional(CONF_SSL, default=False): cv.boolean, + vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, + vol.Optional(CONF_VERSION, default=DEFAULT_VERSION): vol.In([2, 3]), + } + ) +) + +CONFIG_SCHEMA = vol.Schema( + {DOMAIN: vol.All(cv.ensure_list, [GLANCES_SCHEMA])}, extra=vol.ALLOW_EXTRA +) + + +async def async_setup(hass: HomeAssistant, config: Config) -> bool: + """Configure Glances using config flow only.""" + if DOMAIN in config: + for entry in config[DOMAIN]: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=entry + ) + ) + + return True + + +async def async_setup_entry(hass, config_entry): + """Set up Glances from config entry.""" + client = GlancesData(hass, config_entry) + hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = client + if not await client.async_setup(): + return False + + return True + + +async def async_unload_entry(hass, config_entry): + """Unload a config entry.""" + await hass.config_entries.async_forward_entry_unload(config_entry, "sensor") + hass.data[DOMAIN].pop(config_entry.entry_id) + return True + + +class GlancesData: + """Get the latest data from Glances api.""" + + def __init__(self, hass, config_entry): + """Initialize the Glances data.""" + self.hass = hass + self.config_entry = config_entry + self.api = None + self.unsub_timer = None + self.available = False + + @property + def host(self): + """Return client host.""" + return self.config_entry.data[CONF_HOST] + + async def async_update(self): + """Get the latest data from the Glances REST API.""" + try: + await self.api.get_data() + self.available = True + except exceptions.GlancesApiError: + _LOGGER.error("Unable to fetch data from Glances") + self.available = False + _LOGGER.debug("Glances data updated") + async_dispatcher_send(self.hass, DATA_UPDATED) + + async def async_setup(self): + """Set up the Glances client.""" + try: + self.api = get_api(self.hass, self.config_entry.data) + await self.api.get_data() + self.available = True + _LOGGER.debug("Successfully connected to Glances") + + except exceptions.GlancesApiConnectionError: + _LOGGER.debug("Can not connect to Glances") + raise ConfigEntryNotReady + + self.add_options() + self.set_scan_interval(self.config_entry.options[CONF_SCAN_INTERVAL]) + self.config_entry.add_update_listener(self.async_options_updated) + + self.hass.async_create_task( + self.hass.config_entries.async_forward_entry_setup( + self.config_entry, "sensor" + ) + ) + return True + + def add_options(self): + """Add options for Glances integration.""" + if not self.config_entry.options: + options = {CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL} + self.hass.config_entries.async_update_entry( + self.config_entry, options=options + ) + + def set_scan_interval(self, scan_interval): + """Update scan interval.""" + + async def refresh(event_time): + """Get the latest data from Glances api.""" + await self.async_update() + + if self.unsub_timer is not None: + self.unsub_timer() + self.unsub_timer = async_track_time_interval( + self.hass, refresh, timedelta(seconds=scan_interval) + ) + + @staticmethod + async def async_options_updated(hass, entry): + """Triggered by config entry options updates.""" + hass.data[DOMAIN][entry.entry_id].set_scan_interval( + entry.options[CONF_SCAN_INTERVAL] + ) + + +def get_api(hass, entry): + """Return the api from glances_api.""" + params = entry.copy() + params.pop(CONF_NAME) + verify_ssl = params.pop(CONF_VERIFY_SSL) + session = async_get_clientsession(hass, verify_ssl) + return Glances(hass.loop, session, **params) diff --git a/homeassistant/components/glances/config_flow.py b/homeassistant/components/glances/config_flow.py new file mode 100644 index 00000000000..3c86fae0357 --- /dev/null +++ b/homeassistant/components/glances/config_flow.py @@ -0,0 +1,130 @@ +"""Config flow for Glances.""" +import glances_api +import voluptuous as vol + +from homeassistant import config_entries, core, exceptions +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_SCAN_INTERVAL, + CONF_SSL, + CONF_USERNAME, + CONF_VERIFY_SSL, +) +from homeassistant.core import callback + +from . import get_api +from .const import ( + CONF_VERSION, + DEFAULT_HOST, + DEFAULT_NAME, + DEFAULT_PORT, + DEFAULT_SCAN_INTERVAL, + DEFAULT_VERSION, + DOMAIN, + SUPPORTED_VERSIONS, +) + +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_NAME, default=DEFAULT_NAME): str, + vol.Required(CONF_HOST, default=DEFAULT_HOST): str, + vol.Optional(CONF_USERNAME): str, + vol.Optional(CONF_PASSWORD): str, + vol.Required(CONF_PORT, default=DEFAULT_PORT): int, + vol.Required(CONF_VERSION, default=DEFAULT_VERSION): int, + vol.Optional(CONF_SSL, default=False): bool, + vol.Optional(CONF_VERIFY_SSL, default=False): bool, + } +) + + +async def validate_input(hass: core.HomeAssistant, data): + """Validate the user input allows us to connect.""" + for entry in hass.config_entries.async_entries(DOMAIN): + if entry.data[CONF_HOST] == data[CONF_HOST]: + raise AlreadyConfigured + + if data[CONF_VERSION] not in SUPPORTED_VERSIONS: + raise WrongVersion + try: + api = get_api(hass, data) + await api.get_data() + except glances_api.exceptions.GlancesApiConnectionError: + raise CannotConnect + + +class GlancesFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a Glances config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return GlancesOptionsFlowHandler(config_entry) + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + try: + await validate_input(self.hass, user_input) + return self.async_create_entry( + title=user_input[CONF_NAME], data=user_input + ) + except AlreadyConfigured: + return self.async_abort(reason="already_configured") + except CannotConnect: + errors["base"] = "cannot_connect" + except WrongVersion: + errors[CONF_VERSION] = "wrong_version" + + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) + + async def async_step_import(self, import_config): + """Import from Glances sensor config.""" + + return await self.async_step_user(user_input=import_config) + + +class GlancesOptionsFlowHandler(config_entries.OptionsFlow): + """Handle Glances client options.""" + + def __init__(self, config_entry): + """Initialize Glances options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Manage the Glances options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + options = { + vol.Optional( + CONF_SCAN_INTERVAL, + default=self.config_entry.options.get( + CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL + ), + ): int + } + + return self.async_show_form(step_id="init", data_schema=vol.Schema(options)) + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class AlreadyConfigured(exceptions.HomeAssistantError): + """Error to indicate host is already configured.""" + + +class WrongVersion(exceptions.HomeAssistantError): + """Error to indicate the selected version is wrong.""" diff --git a/homeassistant/components/glances/const.py b/homeassistant/components/glances/const.py new file mode 100644 index 00000000000..e47586ea245 --- /dev/null +++ b/homeassistant/components/glances/const.py @@ -0,0 +1,36 @@ +"""Constants for Glances component.""" +from homeassistant.const import TEMP_CELSIUS + +DOMAIN = "glances" +CONF_VERSION = "version" + +DEFAULT_HOST = "localhost" +DEFAULT_NAME = "Glances" +DEFAULT_PORT = 61208 +DEFAULT_VERSION = 3 +DEFAULT_SCAN_INTERVAL = 60 + +DATA_UPDATED = "glances_data_updated" +SUPPORTED_VERSIONS = [2, 3] + +SENSOR_TYPES = { + "disk_use_percent": ["Disk used percent", "%", "mdi:harddisk"], + "disk_use": ["Disk used", "GiB", "mdi:harddisk"], + "disk_free": ["Disk free", "GiB", "mdi:harddisk"], + "memory_use_percent": ["RAM used percent", "%", "mdi:memory"], + "memory_use": ["RAM used", "MiB", "mdi:memory"], + "memory_free": ["RAM free", "MiB", "mdi:memory"], + "swap_use_percent": ["Swap used percent", "%", "mdi:memory"], + "swap_use": ["Swap used", "GiB", "mdi:memory"], + "swap_free": ["Swap free", "GiB", "mdi:memory"], + "processor_load": ["CPU load", "15 min", "mdi:memory"], + "process_running": ["Running", "Count", "mdi:memory"], + "process_total": ["Total", "Count", "mdi:memory"], + "process_thread": ["Thread", "Count", "mdi:memory"], + "process_sleeping": ["Sleeping", "Count", "mdi:memory"], + "cpu_use_percent": ["CPU used", "%", "mdi:memory"], + "cpu_temp": ["CPU Temp", TEMP_CELSIUS, "mdi:thermometer"], + "docker_active": ["Containers active", "", "mdi:docker"], + "docker_cpu_use": ["Containers CPU used", "%", "mdi:docker"], + "docker_memory_use": ["Containers RAM used", "MiB", "mdi:docker"], +} diff --git a/homeassistant/components/glances/manifest.json b/homeassistant/components/glances/manifest.json index 775d208c1c4..6067b1a9868 100644 --- a/homeassistant/components/glances/manifest.json +++ b/homeassistant/components/glances/manifest.json @@ -1,12 +1,14 @@ { "domain": "glances", "name": "Glances", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/glances", "requirements": [ "glances_api==0.2.0" ], "dependencies": [], "codeowners": [ - "@fabaff" + "@fabaff", + "@engrbm87" ] -} +} \ No newline at end of file diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py index 90b4b386f37..760958f0dee 100644 --- a/homeassistant/components/glances/sensor.py +++ b/homeassistant/components/glances/sensor.py @@ -1,114 +1,31 @@ """Support gathering system information of hosts which are running glances.""" -from datetime import timedelta import logging -import voluptuous as vol - -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - CONF_HOST, - CONF_NAME, - CONF_PORT, - CONF_USERNAME, - CONF_PASSWORD, - CONF_SSL, - CONF_VERIFY_SSL, - CONF_RESOURCES, - STATE_UNAVAILABLE, - 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.const import CONF_NAME, STATE_UNAVAILABLE +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from homeassistant.util import Throttle + +from .const import DATA_UPDATED, DOMAIN, SENSOR_TYPES _LOGGER = logging.getLogger(__name__) -CONF_VERSION = "version" - -DEFAULT_HOST = "localhost" -DEFAULT_NAME = "Glances" -DEFAULT_PORT = "61208" -DEFAULT_VERSION = 2 - -MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1) - -SENSOR_TYPES = { - "disk_use_percent": ["Disk used percent", "%", "mdi:harddisk"], - "disk_use": ["Disk used", "GiB", "mdi:harddisk"], - "disk_free": ["Disk free", "GiB", "mdi:harddisk"], - "memory_use_percent": ["RAM used percent", "%", "mdi:memory"], - "memory_use": ["RAM used", "MiB", "mdi:memory"], - "memory_free": ["RAM free", "MiB", "mdi:memory"], - "swap_use_percent": ["Swap used percent", "%", "mdi:memory"], - "swap_use": ["Swap used", "GiB", "mdi:memory"], - "swap_free": ["Swap free", "GiB", "mdi:memory"], - "processor_load": ["CPU load", "15 min", "mdi:memory"], - "process_running": ["Running", "Count", "mdi:memory"], - "process_total": ["Total", "Count", "mdi:memory"], - "process_thread": ["Thread", "Count", "mdi:memory"], - "process_sleeping": ["Sleeping", "Count", "mdi:memory"], - "cpu_use_percent": ["CPU used", "%", "mdi:memory"], - "cpu_temp": ["CPU Temp", TEMP_CELSIUS, "mdi:thermometer"], - "docker_active": ["Containers active", "", "mdi:docker"], - "docker_cpu_use": ["Containers CPU used", "%", "mdi:docker"], - "docker_memory_use": ["Containers RAM used", "MiB", "mdi:docker"], -} - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_USERNAME): cv.string, - vol.Optional(CONF_PASSWORD): cv.string, - vol.Optional(CONF_SSL, default=False): cv.boolean, - vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, - vol.Optional(CONF_RESOURCES, default=["disk_use"]): vol.All( - cv.ensure_list, [vol.In(SENSOR_TYPES)] - ), - vol.Optional(CONF_VERSION, default=DEFAULT_VERSION): vol.In([2, 3]), - } -) - async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the Glances sensors is done through async_setup_entry.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Glances sensors.""" - from glances_api import Glances - - name = config[CONF_NAME] - host = config[CONF_HOST] - port = config[CONF_PORT] - version = config[CONF_VERSION] - var_conf = config[CONF_RESOURCES] - username = config.get(CONF_USERNAME) - password = config.get(CONF_PASSWORD) - ssl = config[CONF_SSL] - verify_ssl = config[CONF_VERIFY_SSL] - - session = async_get_clientsession(hass, verify_ssl) - glances = GlancesData( - Glances( - hass.loop, - session, - host=host, - port=port, - version=version, - username=username, - password=password, - ssl=ssl, - ) - ) - - await glances.async_update() - - if glances.api.data is None: - raise PlatformNotReady + glances_data = hass.data[DOMAIN][config_entry.entry_id] + name = config_entry.data[CONF_NAME] dev = [] - for resource in var_conf: - dev.append(GlancesSensor(glances, name, resource)) + for sensor_type in SENSOR_TYPES: + dev.append( + GlancesSensor(glances_data, name, SENSOR_TYPES[sensor_type][0], sensor_type) + ) async_add_entities(dev, True) @@ -116,9 +33,10 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class GlancesSensor(Entity): """Implementation of a Glances sensor.""" - def __init__(self, glances, name, sensor_type): + def __init__(self, glances_data, name, sensor_name, sensor_type): """Initialize the sensor.""" - self.glances = glances + self.glances_data = glances_data + self._sensor_name = sensor_name self._name = name self.type = sensor_type self._state = None @@ -127,7 +45,12 @@ class GlancesSensor(Entity): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self._name, SENSOR_TYPES[self.type][0]) + return f"{self._name} {self._sensor_name}" + + @property + def unique_id(self): + """Set unique_id for sensor.""" + return f"{self.glances_data.host}-{self.name}" @property def icon(self): @@ -142,17 +65,31 @@ class GlancesSensor(Entity): @property def available(self): """Could the device be accessed during the last update call.""" - return self.glances.available + return self.glances_data.available @property def state(self): """Return the state of the resources.""" return self._state + @property + def should_poll(self): + """Return the polling requirement for this sensor.""" + return False + + async def async_added_to_hass(self): + """Handle entity which will be added.""" + async_dispatcher_connect( + self.hass, DATA_UPDATED, self._schedule_immediate_update + ) + + @callback + def _schedule_immediate_update(self): + self.async_schedule_update_ha_state(True) + async def async_update(self): """Get the latest data from REST API.""" - await self.glances.async_update() - value = self.glances.api.data + value = self.glances_data.api.data if value is not None: if self.type == "disk_use_percent": @@ -249,24 +186,3 @@ class GlancesSensor(Entity): self._state = round(mem_use / 1024 ** 2, 1) except KeyError: self._state = STATE_UNAVAILABLE - - -class GlancesData: - """The class for handling the data retrieval.""" - - def __init__(self, api): - """Initialize the data object.""" - self.api = api - self.available = True - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - async def async_update(self): - """Get the latest data from the Glances REST API.""" - from glances_api.exceptions import GlancesApiError - - try: - await self.api.get_data() - self.available = True - except GlancesApiError: - _LOGGER.error("Unable to fetch data from Glances") - self.available = False diff --git a/homeassistant/components/glances/strings.json b/homeassistant/components/glances/strings.json new file mode 100644 index 00000000000..1bd7275daef --- /dev/null +++ b/homeassistant/components/glances/strings.json @@ -0,0 +1,37 @@ +{ + "config": { + "title": "Glances", + "step": { + "user": { + "title": "Setup Glances", + "data": { + "name": "Name", + "host": "Host", + "username": "Username", + "password": "Password", + "port": "Port", + "version": "Glances API Version (2 or 3)", + "ssl": "Use SSL/TLS to connect to the Glances system", + "verify_ssl": "Verify the certification of the system" + } + } + }, + "error": { + "cannot_connect": "Unable to connect to host", + "wrong_version": "Version not supported (2 or 3 only)" + }, + "abort": { + "already_configured": "Host is already configured." + } + }, + "options": { + "step": { + "init": { + "description": "Configure options for Glances", + "data": { + "scan_interval": "Update frequency" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gntp/notify.py b/homeassistant/components/gntp/notify.py index 48c02cf0ba8..5c05b097a1f 100644 --- a/homeassistant/components/gntp/notify.py +++ b/homeassistant/components/gntp/notify.py @@ -2,17 +2,18 @@ import logging import os +import gntp.errors +import gntp.notifier import voluptuous as vol -from homeassistant.const import CONF_PASSWORD, CONF_PORT -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, CONF_PORT +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -69,9 +70,6 @@ class GNTPNotificationService(BaseNotificationService): def __init__(self, app_name, app_icon, hostname, password, port): """Initialize the service.""" - import gntp.notifier - import gntp.errors - self.gntp = gntp.notifier.GrowlNotifier( applicationName=app_name, notifications=["Notification"], diff --git a/homeassistant/components/goalfeed/__init__.py b/homeassistant/components/goalfeed/__init__.py index 3a14eb2831d..cdca99e0309 100644 --- a/homeassistant/components/goalfeed/__init__.py +++ b/homeassistant/components/goalfeed/__init__.py @@ -1,11 +1,12 @@ """Component for the Goalfeed service.""" import json +import pysher import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +import homeassistant.helpers.config_validation as cv # Version downgraded due to regression in library # For details: https://github.com/nlsdfnbch/Pysher/issues/38 @@ -30,8 +31,6 @@ GOALFEED_APP_ID = "bfd4ed98c1ff22c04074" def setup(hass, config): """Set up the Goalfeed component.""" - import pysher - conf = config[DOMAIN] username = conf.get(CONF_USERNAME) password = conf.get(CONF_PASSWORD) diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index 62aa2212bb1..9cb9be0fa4f 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -4,6 +4,15 @@ import logging import os import yaml +import httplib2 +from oauth2client.client import ( + OAuth2WebServerFlow, + OAuth2DeviceCodeError, + FlowExchangeError, +) +from oauth2client.file import Storage +from googleapiclient import discovery as google_discovery + import voluptuous as vol from voluptuous.error import Error as VoluptuousError @@ -126,13 +135,6 @@ def do_authentication(hass, hass_config, config): Notify user of user_code and verification_url then poll until we have an access token. """ - from oauth2client.client import ( - OAuth2WebServerFlow, - OAuth2DeviceCodeError, - FlowExchangeError, - ) - from oauth2client.file import Storage - oauth = OAuth2WebServerFlow( client_id=config[CONF_CLIENT_ID], client_secret=config[CONF_CLIENT_SECRET], @@ -341,10 +343,6 @@ class GoogleCalendarService: def get(self): """Get the calendar service from the storage file token.""" - import httplib2 - from oauth2client.file import Storage - from googleapiclient import discovery as google_discovery - credentials = Storage(self.token_file).get() http = credentials.authorize(httplib2.Http()) service = google_discovery.build( diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index 31e9f186a4e..8a6eb644621 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -3,6 +3,8 @@ import copy from datetime import timedelta import logging +from httplib2 import ServerNotFoundError # pylint: disable=import-error + from homeassistant.components.calendar import ( ENTITY_ID_FORMAT, CalendarEventDevice, @@ -126,9 +128,6 @@ class GoogleCalendarData: self.event = None def _prepare_query(self): - # pylint: disable=import-error - from httplib2 import ServerNotFoundError - try: service = self.calendar_service.get() except ServerNotFoundError: diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py index a1252d67fff..ebf906b6f2a 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -28,9 +28,13 @@ from .const import ( CONF_ENTITY_CONFIG, CONF_EXPOSE, CONF_ALIASES, + CONF_REPORT_STATE, CONF_ROOM_HINT, CONF_ALLOW_UNLOCK, CONF_SECURE_DEVICES_PIN, + CONF_SERVICE_ACCOUNT, + CONF_CLIENT_EMAIL, + CONF_PRIVATE_KEY, ) from .const import EVENT_COMMAND_RECEIVED, EVENT_SYNC_RECEIVED # noqa: F401 from .const import EVENT_QUERY_RECEIVED # noqa: F401 @@ -47,6 +51,24 @@ ENTITY_SCHEMA = vol.Schema( } ) +GOOGLE_SERVICE_ACCOUNT = vol.Schema( + { + vol.Required(CONF_PRIVATE_KEY): cv.string, + vol.Required(CONF_CLIENT_EMAIL): cv.string, + }, + extra=vol.ALLOW_EXTRA, +) + + +def _check_report_state(data): + if data[CONF_REPORT_STATE]: + if CONF_SERVICE_ACCOUNT not in data: + raise vol.Invalid( + "If report state is enabled, a service account must exist" + ) + return data + + GOOGLE_ASSISTANT_SCHEMA = vol.All( cv.deprecated(CONF_ALLOW_UNLOCK, invalidation_version="0.95"), vol.Schema( @@ -63,9 +85,12 @@ GOOGLE_ASSISTANT_SCHEMA = vol.All( vol.Optional(CONF_ALLOW_UNLOCK): cv.boolean, # str on purpose, makes sure it is configured correctly. vol.Optional(CONF_SECURE_DEVICES_PIN): str, + vol.Optional(CONF_REPORT_STATE, default=False): cv.boolean, + vol.Optional(CONF_SERVICE_ACCOUNT): GOOGLE_SERVICE_ACCOUNT, }, extra=vol.PREVENT_EXTRA, ), + _check_report_state, ) CONFIG_SCHEMA = vol.Schema({DOMAIN: GOOGLE_ASSISTANT_SCHEMA}, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index 54abd54caaf..03253e244fe 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -32,6 +32,10 @@ CONF_API_KEY = "api_key" CONF_ROOM_HINT = "room" CONF_ALLOW_UNLOCK = "allow_unlock" CONF_SECURE_DEVICES_PIN = "secure_devices_pin" +CONF_REPORT_STATE = "report_state" +CONF_SERVICE_ACCOUNT = "service_account" +CONF_CLIENT_EMAIL = "client_email" +CONF_PRIVATE_KEY = "private_key" DEFAULT_EXPOSE_BY_DEFAULT = True DEFAULT_EXPOSED_DOMAINS = [ @@ -72,7 +76,10 @@ TYPE_ALARM = PREFIX_TYPES + "SECURITYSYSTEM" SERVICE_REQUEST_SYNC = "request_sync" HOMEGRAPH_URL = "https://homegraph.googleapis.com/" +HOMEGRAPH_SCOPE = "https://www.googleapis.com/auth/homegraph" +HOMEGRAPH_TOKEN_URL = "https://accounts.google.com/o/oauth2/token" REQUEST_SYNC_BASE_URL = HOMEGRAPH_URL + "v1/devices:requestSync" +REPORT_STATE_BASE_URL = HOMEGRAPH_URL + "v1/devices:reportStateAndNotification" # Error codes used for SmartHomeError class # https://developers.google.com/actions/reference/smarthome/errors-exceptions diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 933f0c07999..96b9b93d70a 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -1,10 +1,15 @@ """Helper classes for Google Assistant integration.""" from asyncio import gather from collections.abc import Mapping -from typing import List +import logging +import pprint +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 ( CONF_NAME, STATE_UNAVAILABLE, @@ -15,6 +20,7 @@ from homeassistant.const import ( from . import trait from .const import ( + DOMAIN, DOMAIN_TO_GOOGLE_TYPES, CONF_ALIASES, ERR_FUNCTION_NOT_SUPPORTED, @@ -24,6 +30,7 @@ from .const import ( from .error import SmartHomeError SYNC_DELAY = 15 +_LOGGER = logging.getLogger(__name__) class AbstractConfig: @@ -35,6 +42,7 @@ class AbstractConfig: """Initialize abstract config.""" self.hass = hass self._google_sync_unsub = None + self._local_sdk_active = False @property def enabled(self): @@ -61,12 +69,30 @@ class AbstractConfig: """Return if we're actively reporting states.""" return self._unsub_report_state is not None + @property + def is_local_sdk_active(self): + """Return if we're actively accepting local messages.""" + return self._local_sdk_active + @property def should_report_state(self): """Return if states should be proactively reported.""" # pylint: disable=no-self-use return False + @property + def local_sdk_webhook_id(self): + """Return the local SDK webhook ID. + + Return None to disable the local SDK. + """ + return None + + @property + def local_sdk_user_id(self): + """Return the user ID to be used for actions received via the local SDK.""" + raise NotImplementedError + def should_expose(self, state) -> bool: """Return if entity should be exposed.""" raise NotImplementedError @@ -131,15 +157,66 @@ class AbstractConfig: Called when the user disconnects their account from Google. """ + @callback + def async_enable_local_sdk(self): + """Enable the local SDK.""" + webhook_id = self.local_sdk_webhook_id + + if webhook_id is None: + return + + webhook.async_register( + self.hass, DOMAIN, "Local Support", webhook_id, self._handle_local_webhook + ) + + self._local_sdk_active = True + + @callback + def async_disable_local_sdk(self): + """Disable the local SDK.""" + if not self._local_sdk_active: + return + + webhook.async_unregister(self.hass, self.local_sdk_webhook_id) + self._local_sdk_active = False + + async def _handle_local_webhook(self, hass, webhook_id, request): + """Handle an incoming local SDK message.""" + from . import smart_home + + payload = await request.json() + + if _LOGGER.isEnabledFor(logging.DEBUG): + _LOGGER.debug("Received local message:\n%s\n", pprint.pformat(payload)) + + if not self.enabled: + return json_response(smart_home.turned_off_response(payload)) + + result = await smart_home.async_handle_message( + self.hass, self, self.local_sdk_user_id, payload + ) + + if _LOGGER.isEnabledFor(logging.DEBUG): + _LOGGER.debug("Responding to local message:\n%s\n", pprint.pformat(result)) + + return json_response(result) + class RequestData: """Hold data associated with a particular request.""" - def __init__(self, config: AbstractConfig, user_id: str, request_id: str): + def __init__( + self, + config: AbstractConfig, + user_id: str, + request_id: str, + devices: Optional[List[dict]], + ): """Initialize the request data.""" self.config = config self.request_id = request_id self.context = Context(user_id=user_id) + self.devices = devices def get_google_type(domain, device_class): @@ -234,6 +311,15 @@ class GoogleEntity: if aliases: device["name"]["nicknames"] = aliases + if self.config.is_local_sdk_active: + device["otherDeviceIds"] = [{"deviceId": self.entity_id}] + device["customData"] = { + "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, + } + for trt in traits: device["attributes"].update(trt.sync_attributes()) @@ -280,6 +366,11 @@ class GoogleEntity: return attrs + @callback + def reachable_device_serialize(self): + """Serialize entity for a REACHABLE_DEVICE response.""" + return {"verificationId": self.entity_id} + async def execute(self, data, command_payload): """Execute a command. diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index aea226348b8..90fa1ced157 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -1,20 +1,38 @@ """Support for Google Actions Smart Home Control.""" +import asyncio +from datetime import timedelta import logging +from uuid import uuid4 +import jwt +from aiohttp import ClientResponseError, ClientError from aiohttp.web import Request, Response # Typing imports from homeassistant.components.http import HomeAssistantView -from homeassistant.core import callback +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_ENTITY_CONFIG, CONF_EXPOSE, + CONF_REPORT_STATE, CONF_SECURE_DEVICES_PIN, + CONF_SERVICE_ACCOUNT, + CONF_CLIENT_EMAIL, + CONF_PRIVATE_KEY, + DOMAIN, + HOMEGRAPH_TOKEN_URL, + HOMEGRAPH_SCOPE, + REPORT_STATE_BASE_URL, + REQUEST_SYNC_BASE_URL, + SERVICE_REQUEST_SYNC, ) from .smart_home import async_handle_message from .helpers import AbstractConfig @@ -22,6 +40,35 @@ from .helpers import AbstractConfig _LOGGER = logging.getLogger(__name__) +def _get_homegraph_jwt(time, iss, key): + now = int(time.timestamp()) + + jwt_raw = { + "iss": iss, + "scope": HOMEGRAPH_SCOPE, + "aud": HOMEGRAPH_TOKEN_URL, + "iat": now, + "exp": now + 3600, + } + return jwt.encode(jwt_raw, key, algorithm="RS256").decode("utf-8") + + +async def _get_homegraph_token(hass, jwt_signed): + headers = { + "Authorization": "Bearer {}".format(jwt_signed), + "Content-Type": "application/x-www-form-urlencoded", + } + data = { + "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", + "assertion": jwt_signed, + } + + session = async_get_clientsession(hass) + async with session.post(HOMEGRAPH_TOKEN_URL, headers=headers, data=data) as res: + res.raise_for_status() + return await res.json() + + class GoogleConfig(AbstractConfig): """Config for manual setup of Google.""" @@ -29,6 +76,8 @@ class GoogleConfig(AbstractConfig): """Initialize the config.""" super().__init__(hass) self._config = config + self._access_token = None + self._access_token_renew = None @property def enabled(self): @@ -50,6 +99,12 @@ class GoogleConfig(AbstractConfig): """Return entity config.""" return self._config.get(CONF_SECURE_DEVICES_PIN) + @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: """Return if entity should be exposed.""" expose_by_default = self._config.get(CONF_EXPOSE_BY_DEFAULT) @@ -79,11 +134,93 @@ class GoogleConfig(AbstractConfig): """If an entity should have 2FA checked.""" return True + 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") + return + + now = dt_util.utcnow() + if not self._access_token or now > self._access_token_renew or force: + token = await _get_homegraph_token( + self.hass, + _get_homegraph_jwt( + now, + self._config[CONF_SERVICE_ACCOUNT][CONF_CLIENT_EMAIL], + self._config[CONF_SERVICE_ACCOUNT][CONF_PRIVATE_KEY], + ), + ) + self._access_token = token["access_token"] + self._access_token_renew = now + timedelta(seconds=token["expires_in"]) + + async def async_call_homegraph_api(self, url, data): + """Call a homegraph api with authenticaiton.""" + session = async_get_clientsession(self.hass) + + async def _call(): + headers = { + "Authorization": "Bearer {}".format(self._access_token), + "X-GFE-SSL": "yes", + } + async with session.post(url, headers=headers, json=data) as res: + _LOGGER.debug( + "Response on %s with data %s was %s", url, data, await res.text() + ) + res.raise_for_status() + + try: + await self._async_update_token() + try: + 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 + except ClientResponseError as error: + _LOGGER.error("Request for %s failed: %d", url, error.status) + except (asyncio.TimeoutError, ClientError): + _LOGGER.error("Could not contact %s", url) + + async def async_report_state(self, message): + """Send a state report to Google.""" + data = { + "requestId": uuid4().hex, + "agentUserId": (await self.hass.auth.async_get_owner()).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.""" - hass.http.register_view(GoogleAssistantView(GoogleConfig(hass, cfg))) + 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): diff --git a/homeassistant/components/google_assistant/report_state.py b/homeassistant/components/google_assistant/report_state.py index b842a552714..aacb90e9d2b 100644 --- a/homeassistant/components/google_assistant/report_state.py +++ b/homeassistant/components/google_assistant/report_state.py @@ -49,23 +49,23 @@ def async_enable_report_state(hass: HomeAssistant, google_config: AbstractConfig {"devices": {"states": {changed_entity: entity_data}}} ) - async_call_later( - hass, INITIAL_REPORT_DELAY, _async_report_all_states(hass, google_config) - ) + async def inital_report(_now): + """Report initially all states.""" + entities = {} + + for entity in async_get_entities(hass, google_config): + if not entity.should_expose(): + continue + + try: + entities[entity.entity_id] = entity.query_serialize() + except SmartHomeError: + continue + + await google_config.async_report_state({"devices": {"states": entities}}) + + async_call_later(hass, INITIAL_REPORT_DELAY, inital_report) return hass.helpers.event.async_track_state_change( MATCH_ALL, async_entity_state_listener ) - - -async def _async_report_all_states(hass: HomeAssistant, google_config: AbstractConfig): - """Report all states.""" - entities = {} - - for entity in async_get_entities(hass, google_config): - if not entity.should_expose(): - continue - - entities[entity.entity_id] = entity.query_serialize() - - await google_config.async_report_state({"devices": {"states": entities}}) diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index f9b311a3880..0944c9532ef 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -5,7 +5,7 @@ import logging from homeassistant.util.decorator import Registry -from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.const import ATTR_ENTITY_ID, __version__ from .const import ( ERR_PROTOCOL_ERROR, @@ -24,9 +24,7 @@ _LOGGER = logging.getLogger(__name__) async def async_handle_message(hass, config, user_id, message): """Handle incoming API messages.""" - request_id: str = message.get("requestId") - - data = RequestData(config, user_id, request_id) + data = RequestData(config, user_id, message["requestId"], message.get("devices")) response = await _process(hass, data, message) @@ -67,6 +65,7 @@ async def _process(hass, data, message): if result is None: return None + return {"requestId": data.request_id, "payload": result} @@ -74,7 +73,7 @@ async def _process(hass, data, message): async def async_devices_sync(hass, data, payload): """Handle action.devices.SYNC request. - https://developers.google.com/actions/smarthome/create-app#actiondevicessync + https://developers.google.com/assistant/smarthome/develop/process-intents#SYNC """ hass.bus.async_fire( EVENT_SYNC_RECEIVED, {"request_id": data.request_id}, context=data.context @@ -84,7 +83,7 @@ async def async_devices_sync(hass, data, payload): *( entity.sync_serialize() for entity in async_get_entities(hass, data.config) - if data.config.should_expose(entity.state) + if entity.should_expose() ) ) @@ -100,7 +99,7 @@ async def async_devices_sync(hass, data, payload): async def async_devices_query(hass, data, payload): """Handle action.devices.QUERY request. - https://developers.google.com/actions/smarthome/create-app#actiondevicesquery + https://developers.google.com/assistant/smarthome/develop/process-intents#QUERY """ devices = {} for device in payload.get("devices", []): @@ -128,7 +127,7 @@ async def async_devices_query(hass, data, payload): async def handle_devices_execute(hass, data, payload): """Handle action.devices.EXECUTE request. - https://developers.google.com/actions/smarthome/create-app#actiondevicesexecute + https://developers.google.com/assistant/smarthome/develop/process-intents#EXECUTE """ entities = {} results = {} @@ -196,12 +195,50 @@ async def handle_devices_execute(hass, data, payload): async def async_devices_disconnect(hass, data: RequestData, payload): """Handle action.devices.DISCONNECT request. - https://developers.google.com/actions/smarthome/create#actiondevicesdisconnect + https://developers.google.com/assistant/smarthome/develop/process-intents#DISCONNECT """ await data.config.async_deactivate_report_state() return None +@HANDLERS.register("action.devices.IDENTIFY") +async def async_devices_identify(hass, data: RequestData, payload): + """Handle action.devices.IDENTIFY request. + + https://developers.google.com/assistant/smarthome/develop/local#implement_the_identify_handler + """ + return { + "device": { + "id": data.config.agent_user_id, + "isLocalOnly": True, + "isProxy": True, + "deviceInfo": { + "hwVersion": "UNKNOWN_HW_VERSION", + "manufacturer": "Home Assistant", + "model": "Home Assistant", + "swVersion": __version__, + }, + } + } + + +@HANDLERS.register("action.devices.REACHABLE_DEVICES") +async def async_devices_reachable(hass, data: RequestData, payload): + """Handle action.devices.REACHABLE_DEVICES request. + + https://developers.google.com/actions/smarthome/create#actiondevicesdisconnect + """ + google_ids = set(dev["id"] for dev in (data.devices or [])) + + return { + "devices": [ + entity.reachable_device_serialize() + for entity in async_get_entities(hass, data.config) + if entity.entity_id in google_ids and entity.should_expose() + ] + } + + def turned_off_response(message): """Return a device turned off response.""" return { diff --git a/homeassistant/components/google_travel_time/sensor.py b/homeassistant/components/google_travel_time/sensor.py index 32947867958..3ee72928fc1 100644 --- a/homeassistant/components/google_travel_time/sensor.py +++ b/homeassistant/components/google_travel_time/sensor.py @@ -1,25 +1,25 @@ """Support for Google travel time sensors.""" +from datetime import datetime, timedelta import logging -from datetime import datetime -from datetime import timedelta +import googlemaps 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_API_KEY, - CONF_NAME, - EVENT_HOMEASSISTANT_START, + ATTR_ATTRIBUTION, ATTR_LATITUDE, ATTR_LONGITUDE, - ATTR_ATTRIBUTION, + CONF_API_KEY, CONF_MODE, + CONF_NAME, + EVENT_HOMEASSISTANT_START, ) from homeassistant.helpers import location +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__) @@ -203,8 +203,6 @@ class GoogleTravelTimeSensor(Entity): else: self._destination = destination - import googlemaps - self._client = googlemaps.Client(api_key, timeout=10) try: self.update() diff --git a/homeassistant/components/gpsd/sensor.py b/homeassistant/components/gpsd/sensor.py index 197e424ce86..8696dde72cb 100644 --- a/homeassistant/components/gpsd/sensor.py +++ b/homeassistant/components/gpsd/sensor.py @@ -1,6 +1,8 @@ """Support for GPSD.""" import logging +import socket +from gps3.agps3threaded import AGPS3mechanism import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -9,11 +11,11 @@ from homeassistant.const import ( ATTR_LONGITUDE, ATTR_MODE, CONF_HOST, - CONF_PORT, CONF_NAME, + CONF_PORT, ) -from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -50,7 +52,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): # except GPSError: # _LOGGER.warning('Not able to connect to GPSD') # return False - import socket sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: @@ -69,8 +70,6 @@ class GpsdSensor(Entity): def __init__(self, hass, name, host, port): """Initialize the GPSD sensor.""" - from gps3.agps3threaded import AGPS3mechanism - self.hass = hass self._name = name self._host = host diff --git a/homeassistant/components/greenwave/light.py b/homeassistant/components/greenwave/light.py index 5a6ce2c51c2..8b85de598b0 100644 --- a/homeassistant/components/greenwave/light.py +++ b/homeassistant/components/greenwave/light.py @@ -1,7 +1,9 @@ """Support for Greenwave Reality (TCP Connected) lights.""" -import logging from datetime import timedelta +import logging +import os +import greenwavereality as greenwave import voluptuous as vol from homeassistant.components.light import ( @@ -29,9 +31,6 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Greenwave Reality Platform.""" - import greenwavereality as greenwave - import os - host = config.get(CONF_HOST) tokenfile = hass.config.path(".greenwave") if config.get(CONF_VERSION) == 3: @@ -60,8 +59,6 @@ class GreenwaveLight(Light): def __init__(self, light, host, token, gatewaydata): """Initialize a Greenwave Reality Light.""" - import greenwavereality as greenwave - self._did = int(light["did"]) self._name = light["name"] self._state = int(light["state"]) @@ -98,22 +95,16 @@ class GreenwaveLight(Light): def turn_on(self, **kwargs): """Instruct the light to turn on.""" - import greenwavereality as greenwave - temp_brightness = int((kwargs.get(ATTR_BRIGHTNESS, 255) / 255) * 100) greenwave.set_brightness(self._host, self._did, temp_brightness, self._token) greenwave.turn_on(self._host, self._did, self._token) def turn_off(self, **kwargs): """Instruct the light to turn off.""" - import greenwavereality as greenwave - greenwave.turn_off(self._host, self._did, self._token) def update(self): """Fetch new state data for this light.""" - import greenwavereality as greenwave - self._gatewaydata.update() bulbs = self._gatewaydata.greenwave @@ -128,8 +119,6 @@ class GatewayData: def __init__(self, host, token): """Initialize the data object.""" - import greenwavereality as greenwave - self._host = host self._token = token self._greenwave = greenwave.grab_bulbs(host, token) @@ -142,7 +131,5 @@ class GatewayData: @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data from the gateway.""" - import greenwavereality as greenwave - self._greenwave = greenwave.grab_bulbs(self._host, self._token) return self._greenwave diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index 39574a2b03b..29126c82d44 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -36,7 +36,7 @@ from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA from homeassistant.helpers.typing import HomeAssistantType -# mypy: allow-untyped-calls, allow-untyped-defs +# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs DOMAIN = "group" diff --git a/homeassistant/components/group/cover.py b/homeassistant/components/group/cover.py index c5200082f2f..f7a9643e5c8 100644 --- a/homeassistant/components/group/cover.py +++ b/homeassistant/components/group/cover.py @@ -44,6 +44,7 @@ from homeassistant.components.cover import ( # mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs +# mypy: no-check-untyped-defs _LOGGER = logging.getLogger(__name__) @@ -74,7 +75,7 @@ class CoverGroup(CoverDevice): """Initialize a CoverGroup entity.""" self._name = name self._is_closed = False - self._cover_position = 100 + self._cover_position: Optional[int] = 100 self._tilt_position = None self._supported_features = 0 self._assumed_state = True @@ -178,7 +179,7 @@ class CoverGroup(CoverDevice): return self._is_closed @property - def current_cover_position(self): + def current_cover_position(self) -> Optional[int]: """Return current position for all covers.""" return self._cover_position diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index e77c858fc02..85804552494 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -45,6 +45,7 @@ from homeassistant.components.light import ( # mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs +# mypy: no-check-untyped-defs _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/group/notify.py b/homeassistant/components/group/notify.py index 2ffb7fea049..e17990690fa 100644 --- a/homeassistant/components/group/notify.py +++ b/homeassistant/components/group/notify.py @@ -18,7 +18,7 @@ from homeassistant.components.notify import ( ) -# mypy: allow-untyped-calls, allow-untyped-defs +# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/gtfs/sensor.py b/homeassistant/components/gtfs/sensor.py index 086545f0c76..07b450dd33e 100644 --- a/homeassistant/components/gtfs/sensor.py +++ b/homeassistant/components/gtfs/sensor.py @@ -5,6 +5,8 @@ import os import threading from typing import Any, Callable, Optional +import pygtfs +from sqlalchemy.sql import text import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -129,8 +131,6 @@ def get_next_departure( tomorrow = now + datetime.timedelta(days=1) tomorrow_date = tomorrow.strftime(dt_util.DATE_STR_FORMAT) - from sqlalchemy.sql import text - # Fetch all departures for yesterday, today and optionally tomorrow, # up to an overkill maximum in case of a departure every minute for those # days. @@ -353,8 +353,6 @@ def setup_platform( _LOGGER.error("The given GTFS data file/folder was not found") return - import pygtfs - (gtfs_root, _) = os.path.splitext(data) sqlite_file = f"{gtfs_root}.sqlite?check_same_thread=False" @@ -375,7 +373,7 @@ class GTFSDepartureSensor(Entity): def __init__( self, - pygtfs: Any, + gtfs: Any, name: Optional[Any], origin: Any, destination: Any, @@ -383,7 +381,7 @@ class GTFSDepartureSensor(Entity): include_tomorrow: bool, ) -> None: """Initialize the sensor.""" - self._pygtfs = pygtfs + self._pygtfs = gtfs self.origin = origin self.destination = destination self._include_tomorrow = include_tomorrow diff --git a/homeassistant/components/hangouts/.translations/fr.json b/homeassistant/components/hangouts/.translations/fr.json index 0b6dbfcbe44..13142fee513 100644 --- a/homeassistant/components/hangouts/.translations/fr.json +++ b/homeassistant/components/hangouts/.translations/fr.json @@ -14,6 +14,7 @@ "data": { "2fa": "Code PIN d'authentification \u00e0 2 facteurs" }, + "description": "Vide", "title": "Authentification \u00e0 2 facteurs" }, "user": { @@ -22,6 +23,7 @@ "email": "Adresse e-mail", "password": "Mot de passe" }, + "description": "Vide", "title": "Connexion \u00e0 Google Hangouts" } }, diff --git a/homeassistant/components/hangouts/.translations/ko.json b/homeassistant/components/hangouts/.translations/ko.json index 3b1c755b358..385fc128b3b 100644 --- a/homeassistant/components/hangouts/.translations/ko.json +++ b/homeassistant/components/hangouts/.translations/ko.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\uad6c\uae00 \ud589\uc544\uc6c3\uc740 \uc774\ubbf8 \uc124\uc815\ub41c \uc0c1\ud0dc\uc785\ub2c8\ub2e4", + "already_configured": "Google \ud589\uc544\uc6c3\uc740 \uc774\ubbf8 \uc124\uc815\ub41c \uc0c1\ud0dc\uc785\ub2c8\ub2e4", "unknown": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "error": { @@ -24,9 +24,9 @@ "password": "\ube44\ubc00\ubc88\ud638" }, "description": "\uc8c4\uc1a1\ud569\ub2c8\ub2e4. \uad00\ub828 \ub0b4\uc6a9\uc774 \uc544\uc9c1 \uc5c5\ub370\uc774\ud2b8 \ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \ucd94\ud6c4\uc5d0 \ubc18\uc601\ub420 \uc608\uc815\uc774\ub2c8 \uc870\uae08\ub9cc \uae30\ub2e4\ub824\uc8fc\uc138\uc694.", - "title": "\uad6c\uae00 \ud589\uc544\uc6c3 \ub85c\uadf8\uc778" + "title": "Google \ud589\uc544\uc6c3 \ub85c\uadf8\uc778" } }, - "title": "\uad6c\uae00 \ud589\uc544\uc6c3" + "title": "Google \ud589\uc544\uc6c3" } } \ No newline at end of file diff --git a/homeassistant/components/hangouts/.translations/ru.json b/homeassistant/components/hangouts/.translations/ru.json index 6942f683fa6..15d90a672de 100644 --- a/homeassistant/components/hangouts/.translations/ru.json +++ b/homeassistant/components/hangouts/.translations/ru.json @@ -2,7 +2,7 @@ "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.", - "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430" + "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "error": { "invalid_2fa": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f, \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430.", diff --git a/homeassistant/components/hangouts/__init__.py b/homeassistant/components/hangouts/__init__.py index 885ac5d1670..953994d6ac0 100644 --- a/homeassistant/components/hangouts/__init__.py +++ b/homeassistant/components/hangouts/__init__.py @@ -7,6 +7,7 @@ from homeassistant import config_entries 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 @@ -54,8 +55,6 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass, config): """Set up the Hangouts bot component.""" - from homeassistant.components.conversation import create_matcher - config = config.get(DOMAIN) if config is None: hass.data[DOMAIN] = { diff --git a/homeassistant/components/harman_kardon_avr/media_player.py b/homeassistant/components/harman_kardon_avr/media_player.py index 01948943adf..fd7cddcaed9 100644 --- a/homeassistant/components/harman_kardon_avr/media_player.py +++ b/homeassistant/components/harman_kardon_avr/media_player.py @@ -1,18 +1,19 @@ """Support for interface with an Harman/Kardon or JBL AVR.""" import logging +import hkavr 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, + SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, - SUPPORT_TURN_ON, - SUPPORT_SELECT_SOURCE, ) from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, STATE_OFF, STATE_ON +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -38,8 +39,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discover_info=None): """Set up the AVR platform.""" - import hkavr - name = config[CONF_NAME] host = config[CONF_HOST] port = config[CONF_PORT] diff --git a/homeassistant/components/harmony/remote.py b/homeassistant/components/harmony/remote.py index b78f276bf28..118af7fe34a 100644 --- a/homeassistant/components/harmony/remote.py +++ b/homeassistant/components/harmony/remote.py @@ -3,6 +3,12 @@ import asyncio import json import logging +import aioharmony.exceptions as aioexc +from aioharmony.harmonyapi import ( + ClientCallbackType, + HarmonyAPI as HarmonyClient, + SendCommandDevice, +) import voluptuous as vol from homeassistant.components import remote @@ -23,8 +29,8 @@ from homeassistant.const import ( CONF_PORT, EVENT_HOMEASSISTANT_STOP, ) -import homeassistant.helpers.config_validation as cv from homeassistant.exceptions import PlatformNotReady +import homeassistant.helpers.config_validation as cv from homeassistant.util import slugify _LOGGER = logging.getLogger(__name__) @@ -165,8 +171,6 @@ class HarmonyRemote(remote.RemoteDevice): def __init__(self, name, host, port, activity, out_path, delay_secs): """Initialize HarmonyRemote class.""" - from aioharmony.harmonyapi import HarmonyAPI as HarmonyClient - self._name = name self.host = host self.port = port @@ -180,8 +184,6 @@ class HarmonyRemote(remote.RemoteDevice): async def async_added_to_hass(self): """Complete the initialization.""" - from aioharmony.harmonyapi import ClientCallbackType - _LOGGER.debug("%s: Harmony Hub added", self._name) # Register the callbacks self._client.callbacks = ClientCallbackType( @@ -195,8 +197,6 @@ class HarmonyRemote(remote.RemoteDevice): # activity await self.new_config() - import aioharmony.exceptions as aioexc - async def shutdown(_): """Close connection on shutdown.""" _LOGGER.debug("%s: Closing Harmony Hub", self._name) @@ -234,8 +234,6 @@ class HarmonyRemote(remote.RemoteDevice): async def connect(self): """Connect to the Harmony HUB.""" - import aioharmony.exceptions as aioexc - _LOGGER.debug("%s: Connecting", self._name) try: if not await self._client.connect(): @@ -284,8 +282,6 @@ class HarmonyRemote(remote.RemoteDevice): async def async_turn_on(self, **kwargs): """Start an activity from the Harmony device.""" - import aioharmony.exceptions as aioexc - _LOGGER.debug("%s: Turn On", self.name) activity = kwargs.get(ATTR_ACTIVITY, self._default_activity) @@ -314,8 +310,6 @@ class HarmonyRemote(remote.RemoteDevice): async def async_turn_off(self, **kwargs): """Start the PowerOff activity.""" - import aioharmony.exceptions as aioexc - _LOGGER.debug("%s: Turn Off", self.name) try: await self._client.power_off() @@ -325,9 +319,6 @@ class HarmonyRemote(remote.RemoteDevice): # pylint: disable=arguments-differ async def async_send_command(self, command, **kwargs): """Send a list of commands to one device.""" - from aioharmony.harmonyapi import SendCommandDevice - import aioharmony.exceptions as aioexc - _LOGGER.debug("%s: Send Command", self.name) device = kwargs.get(ATTR_DEVICE) if device is None: @@ -390,8 +381,6 @@ class HarmonyRemote(remote.RemoteDevice): async def change_channel(self, channel): """Change the channel using Harmony remote.""" - import aioharmony.exceptions as aioexc - _LOGGER.debug("%s: Changing channel to %s", self.name, channel) try: await self._client.change_channel(channel) @@ -400,8 +389,6 @@ class HarmonyRemote(remote.RemoteDevice): async def sync(self): """Sync the Harmony device with the web service.""" - import aioharmony.exceptions as aioexc - _LOGGER.debug("%s: Syncing hub with Harmony cloud", self.name) try: await self._client.sync() diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 6603728e037..e0c0a57375a 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -269,7 +269,7 @@ async def async_setup(hass, config): if errors: _LOGGER.error(errors) hass.components.persistent_notification.async_create( - "Config error. See dev-info panel for details.", + "Config error. See [the logs](/developer-tools/logs) for details.", "Config validating", f"{HASS_DOMAIN}.check_config", ) diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py index 4ecb9a8419f..53235f80dca 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -167,7 +167,14 @@ def _init_header( # filter flags for name, value in request.headers.items(): - if name in (hdrs.CONTENT_LENGTH, hdrs.CONTENT_ENCODING): + if name in ( + hdrs.CONTENT_LENGTH, + hdrs.CONTENT_ENCODING, + hdrs.SEC_WEBSOCKET_EXTENSIONS, + hdrs.SEC_WEBSOCKET_PROTOCOL, + hdrs.SEC_WEBSOCKET_VERSION, + hdrs.SEC_WEBSOCKET_KEY, + ): continue headers[name] = value diff --git a/homeassistant/components/hassio/services.yaml b/homeassistant/components/hassio/services.yaml index 33574c5dd71..30314c646b0 100644 --- a/homeassistant/components/hassio/services.yaml +++ b/homeassistant/components/hassio/services.yaml @@ -1,37 +1,84 @@ addon_install: - description: Install a HassIO docker addon. + description: Install a Hass.io docker add-on. fields: - addon: {description: Name of addon., example: smb_config} - version: {description: Optional or it will be use the latest version., example: '0.2'} + addon: + description: The add-on slug. + example: core_ssh + version: + description: Optional or it will be use the latest version. + example: "0.2" + addon_start: - description: Start a HassIO docker addon. + description: Start a Hass.io docker add-on. fields: - addon: {description: Name of addon., example: smb_config} + addon: + description: The add-on slug. + example: core_ssh + +addon_restart: + description: Restart a Hass.io docker add-on. + fields: + addon: + description: The add-on slug. + example: core_ssh + +addon_stdin: + description: Write data to a Hass.io docker add-on stdin . + fields: + addon: + description: The add-on slug. + example: core_ssh + addon_stop: - description: Stop a HassIO docker addon. + description: Stop a Hass.io docker add-on. fields: - addon: {description: Name of addon., example: smb_config} + addon: + description: The add-on slug. + example: core_ssh + addon_uninstall: - description: Uninstall a HassIO docker addon. + description: Uninstall a Hass.io docker add-on. fields: - addon: {description: Name of addon., example: smb_config} + addon: + description: The add-on slug. + example: core_ssh + addon_update: - description: Update a HassIO docker addon. + description: Update a Hass.io docker add-on. fields: - addon: {description: Name of addon., example: smb_config} - version: {description: Optional or it will be use the latest version., example: '0.2'} + addon: + description: The add-on slug. + example: core_ssh + version: + description: Optional or it will be use the latest version. + example: "0.2" + homeassistant_update: - description: Update HomeAssistant docker image. + description: Update the Home Assistant docker image. fields: - version: {description: Optional or it will be use the latest version., example: 0.40.1} -host_reboot: {description: Reboot host computer.} -host_shutdown: {description: Poweroff host computer.} + version: + description: Optional or it will be use the latest version. + example: 0.40.1 + +host_reboot: + description: Reboot the host system. + +host_shutdown: + description: Poweroff the host system. + host_update: - description: Update host computer. + description: Update the host system. fields: - version: {description: Optional or it will be use the latest version., example: '0.3'} -supervisor_reload: {description: Reload HassIO supervisor addons/updates/configs.} + version: + description: Optional or it will be use the latest version. + example: "0.3" + +supervisor_reload: + description: Reload the Hass.io supervisor. + supervisor_update: - description: Update HassIO supervisor. + description: Update the Hass.io supervisor. fields: - version: {description: Optional or it will be use the latest version., example: '0.3'} + version: + description: Optional or it will be use the latest version. + example: "0.3" diff --git a/homeassistant/components/heos/.translations/ca.json b/homeassistant/components/heos/.translations/ca.json index 60bd780547c..0987e11430b 100644 --- a/homeassistant/components/heos/.translations/ca.json +++ b/homeassistant/components/heos/.translations/ca.json @@ -12,7 +12,7 @@ "access_token": "Amfitri\u00f3", "host": "Amfitri\u00f3" }, - "description": "Introdueix el nom d'amfitri\u00f3 o l'adre\u00e7a IP d'un dispositiu Heos (preferiblement un connectat a la xarxa per cable).", + "description": "Introdueix el nom de l'amfitri\u00f3 o l'adre\u00e7a IP d'un dispositiu Heos (preferiblement un connectat a la xarxa per cable).", "title": "Connexi\u00f3 amb Heos" } }, diff --git a/homeassistant/components/heos/.translations/pt.json b/homeassistant/components/heos/.translations/pt.json index 33c83fdc738..099d1978436 100644 --- a/homeassistant/components/heos/.translations/pt.json +++ b/homeassistant/components/heos/.translations/pt.json @@ -3,7 +3,8 @@ "step": { "user": { "data": { - "access_token": "Servidor" + "access_token": "Servidor", + "host": "Servidor" } } }, diff --git a/homeassistant/components/heos/.translations/ru.json b/homeassistant/components/heos/.translations/ru.json index f19b5e52064..8aacc8e165d 100644 --- a/homeassistant/components/heos/.translations/ru.json +++ b/homeassistant/components/heos/.translations/ru.json @@ -4,7 +4,7 @@ "already_setup": "\u041d\u0443\u0436\u043d\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u043e \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u043e\u043d\u043e \u0431\u0443\u0434\u0435\u0442 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0442\u044c \u0432\u0441\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 HEOS \u0432 \u0441\u0435\u0442\u0438." }, "error": { - "connection_failure": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u043e\u043c\u0443 \u0445\u043e\u0441\u0442\u0443" + "connection_failure": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u043e\u043c\u0443 \u0445\u043e\u0441\u0442\u0443." }, "step": { "user": { diff --git a/homeassistant/components/hikvision/manifest.json b/homeassistant/components/hikvision/manifest.json index 78917a5351b..11775ed3ae0 100644 --- a/homeassistant/components/hikvision/manifest.json +++ b/homeassistant/components/hikvision/manifest.json @@ -3,7 +3,7 @@ "name": "Hikvision", "documentation": "https://www.home-assistant.io/integrations/hikvision", "requirements": [ - "pyhik==0.2.3" + "pyhik==0.2.4" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/hikvisioncam/switch.py b/homeassistant/components/hikvisioncam/switch.py index 05bce5f4eac..020b894c0f7 100644 --- a/homeassistant/components/hikvisioncam/switch.py +++ b/homeassistant/components/hikvisioncam/switch.py @@ -1,20 +1,22 @@ """Support turning on/off motion detection on Hikvision cameras.""" import logging +import hikvision.api +from hikvision.error import HikvisionError, MissingParamError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, CONF_HOST, + CONF_NAME, CONF_PASSWORD, - CONF_USERNAME, CONF_PORT, + CONF_USERNAME, STATE_OFF, STATE_ON, ) -from homeassistant.helpers.entity import ToggleEntity import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import ToggleEntity # This is the last working version, please test before updating @@ -38,9 +40,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up Hikvision camera.""" - import hikvision.api - from hikvision.error import HikvisionError, MissingParamError - host = config.get(CONF_HOST) port = config.get(CONF_PORT) name = config.get(CONF_NAME) diff --git a/homeassistant/components/hipchat/__init__.py b/homeassistant/components/hipchat/__init__.py deleted file mode 100644 index 8b79982fa43..00000000000 --- a/homeassistant/components/hipchat/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The hipchat component.""" diff --git a/homeassistant/components/hipchat/manifest.json b/homeassistant/components/hipchat/manifest.json deleted file mode 100644 index 9d563719a2e..00000000000 --- a/homeassistant/components/hipchat/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "domain": "hipchat", - "name": "Hipchat", - "documentation": "https://www.home-assistant.io/integrations/hipchat", - "requirements": [ - "hipnotify==1.0.8" - ], - "dependencies": [], - "codeowners": [] -} diff --git a/homeassistant/components/hipchat/notify.py b/homeassistant/components/hipchat/notify.py deleted file mode 100644 index 03556db386a..00000000000 --- a/homeassistant/components/hipchat/notify.py +++ /dev/null @@ -1,108 +0,0 @@ -"""HipChat platform for notify component.""" -import logging - -import voluptuous as vol - -from homeassistant.const import CONF_HOST, CONF_ROOM, CONF_TOKEN -import homeassistant.helpers.config_validation as cv - -from homeassistant.components.notify import ( - ATTR_DATA, - ATTR_TARGET, - PLATFORM_SCHEMA, - BaseNotificationService, -) - -_LOGGER = logging.getLogger(__name__) - -CONF_COLOR = "color" -CONF_NOTIFY = "notify" -CONF_FORMAT = "format" - -DEFAULT_COLOR = "yellow" -DEFAULT_FORMAT = "text" -DEFAULT_HOST = "https://api.hipchat.com/" -DEFAULT_NOTIFY = False - -VALID_COLORS = {"yellow", "green", "red", "purple", "gray", "random"} -VALID_FORMATS = {"text", "html"} - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_ROOM): vol.Coerce(int), - vol.Required(CONF_TOKEN): cv.string, - vol.Optional(CONF_COLOR, default=DEFAULT_COLOR): vol.In(VALID_COLORS), - vol.Optional(CONF_FORMAT, default=DEFAULT_FORMAT): vol.In(VALID_FORMATS), - vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Optional(CONF_NOTIFY, default=DEFAULT_NOTIFY): cv.boolean, - } -) - - -def get_service(hass, config, discovery_info=None): - """Get the HipChat notification service.""" - return HipchatNotificationService( - config[CONF_TOKEN], - config[CONF_ROOM], - config[CONF_COLOR], - config[CONF_NOTIFY], - config[CONF_FORMAT], - config[CONF_HOST], - ) - - -class HipchatNotificationService(BaseNotificationService): - """Implement the notification service for HipChat.""" - - def __init__( - self, token, default_room, default_color, default_notify, default_format, host - ): - """Initialize the service.""" - self._token = token - self._default_room = default_room - self._default_color = default_color - self._default_notify = default_notify - self._default_format = default_format - self._host = host - - self._rooms = {} - self._get_room(self._default_room) - - def _get_room(self, room): - """Get Room object, creating it if necessary.""" - from hipnotify import Room - - if room not in self._rooms: - self._rooms[room] = Room( - token=self._token, room_id=room, endpoint_url=self._host - ) - return self._rooms[room] - - def send_message(self, message="", **kwargs): - """Send a message.""" - color = self._default_color - notify = self._default_notify - message_format = self._default_format - - if kwargs.get(ATTR_DATA) is not None: - data = kwargs.get(ATTR_DATA) - if (data.get(CONF_COLOR) is not None) and ( - data.get(CONF_COLOR) in VALID_COLORS - ): - color = data.get(CONF_COLOR) - if (data.get(CONF_NOTIFY) is not None) and isinstance( - data.get(CONF_NOTIFY), bool - ): - notify = data.get(CONF_NOTIFY) - if (data.get(CONF_FORMAT) is not None) and ( - data.get(CONF_FORMAT) in VALID_FORMATS - ): - message_format = data.get(CONF_FORMAT) - - targets = kwargs.get(ATTR_TARGET, [self._default_room]) - - for target in targets: - room = self._get_room(target) - room.notify( - msg=message, color=color, notify=notify, message_format=message_format - ) diff --git a/homeassistant/components/hive/__init__.py b/homeassistant/components/hive/__init__.py index 3301097bab7..976821513b6 100644 --- a/homeassistant/components/hive/__init__.py +++ b/homeassistant/components/hive/__init__.py @@ -82,6 +82,7 @@ class HiveSession: switch = None weather = None attributes = None + trv = None def setup(hass, config): diff --git a/homeassistant/components/hive/climate.py b/homeassistant/components/hive/climate.py index 1fb77ce6cb9..ed13e3019ce 100644 --- a/homeassistant/components/hive/climate.py +++ b/homeassistant/components/hive/climate.py @@ -8,6 +8,9 @@ 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 @@ -26,6 +29,12 @@ HASS_TO_HIVE_STATE = { HVAC_MODE_OFF: "OFF", } +HIVE_TO_HASS_HVAC_ACTION = { + "UNKNOWN": CURRENT_HVAC_OFF, + False: CURRENT_HVAC_IDLE, + True: CURRENT_HVAC_HEAT, +} + SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE SUPPORT_HVAC = [HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_OFF] SUPPORT_PRESET = [PRESET_NONE, PRESET_BOOST] @@ -71,7 +80,11 @@ class HiveClimateEntity(HiveEntity, ClimateDevice): """Return the name of the Climate device.""" friendly_name = "Heating" if self.node_name is not None: - friendly_name = f"{self.node_name} {friendly_name}" + if self.device_type == "TRV": + friendly_name = self.node_name + else: + friendly_name = f"{self.node_name} {friendly_name}" + return friendly_name @property @@ -95,6 +108,13 @@ class HiveClimateEntity(HiveEntity, ClimateDevice): """ return HIVE_TO_HASS_STATE[self.session.heating.get_mode(self.node_id)] + @property + def hvac_action(self): + """Return current HVAC action.""" + return HIVE_TO_HASS_HVAC_ACTION[ + self.session.heating.operational_status(self.node_id, self.device_type) + ] + @property def temperature_unit(self): """Return the unit of measurement.""" @@ -123,7 +143,10 @@ class HiveClimateEntity(HiveEntity, ClimateDevice): @property def preset_mode(self): """Return the current preset mode, e.g., home, away, temp.""" - if self.session.heating.get_boost(self.node_id) == "ON": + if ( + self.device_type == "Heating" + and self.session.heating.get_boost(self.node_id) == "ON" + ): return PRESET_BOOST return None diff --git a/homeassistant/components/hive/manifest.json b/homeassistant/components/hive/manifest.json index 4164283f9f8..e87e3387a62 100644 --- a/homeassistant/components/hive/manifest.json +++ b/homeassistant/components/hive/manifest.json @@ -3,7 +3,7 @@ "name": "Hive", "documentation": "https://www.home-assistant.io/integrations/hive", "requirements": [ - "pyhiveapi==0.2.19.2" + "pyhiveapi==0.2.19.3" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/homeassistant/__init__.py b/homeassistant/components/homeassistant/__init__.py index 02e53d1de10..d2d6abdadb5 100644 --- a/homeassistant/components/homeassistant/__init__.py +++ b/homeassistant/components/homeassistant/__init__.py @@ -108,7 +108,7 @@ async def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]: if errors: _LOGGER.error(errors) hass.components.persistent_notification.async_create( - "Config error. See dev-info panel for details.", + "Config error. See [the logs](/developer-tools/logs) for details.", "Config validating", f"{ha.DOMAIN}.check_config", ) diff --git a/homeassistant/components/homeassistant/scene.py b/homeassistant/components/homeassistant/scene.py index 66b04109640..39b04f6d3ea 100644 --- a/homeassistant/components/homeassistant/scene.py +++ b/homeassistant/components/homeassistant/scene.py @@ -26,6 +26,36 @@ from homeassistant.helpers import ( from homeassistant.helpers.state import HASS_DOMAIN, async_reproduce_state from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN, STATES, Scene + +def _convert_states(states): + """Convert state definitions to State objects.""" + result = {} + + for entity_id in states: + entity_id = cv.entity_id(entity_id) + + if isinstance(states[entity_id], dict): + entity_attrs = states[entity_id].copy() + state = entity_attrs.pop(ATTR_STATE, None) + attributes = entity_attrs + else: + state = states[entity_id] + attributes = {} + + # YAML translates 'on' to a boolean + # http://yaml.org/type/bool.html + if isinstance(state, bool): + state = STATE_ON if state else STATE_OFF + elif not isinstance(state, str): + raise vol.Invalid(f"State for {entity_id} should be a string") + + result[entity_id] = State(entity_id, state, attributes) + + return result + + +STATES_SCHEMA = vol.All(dict, _convert_states) + PLATFORM_SCHEMA = vol.Schema( { vol.Required(CONF_PLATFORM): HASS_DOMAIN, @@ -34,9 +64,7 @@ PLATFORM_SCHEMA = vol.Schema( [ { vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_ENTITIES): { - cv.entity_id: vol.Any(str, bool, dict) - }, + vol.Required(CONF_ENTITIES): STATES_SCHEMA, } ], ), @@ -44,6 +72,7 @@ PLATFORM_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) +SERVICE_APPLY = "apply" SCENECONFIG = namedtuple("SceneConfig", [CONF_NAME, STATES]) _LOGGER = logging.getLogger(__name__) @@ -87,6 +116,19 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= SCENE_DOMAIN, SERVICE_RELOAD, reload_config ) + async def apply_service(call): + """Apply a scene.""" + await async_reproduce_state( + hass, call.data[CONF_ENTITIES].values(), blocking=True, context=call.context + ) + + hass.services.async_register( + SCENE_DOMAIN, + SERVICE_APPLY, + apply_service, + vol.Schema({vol.Required(CONF_ENTITIES): STATES_SCHEMA}), + ) + def _process_scenes_config(hass, async_add_entities, config): """Process multiple scenes and add them.""" @@ -97,41 +139,11 @@ def _process_scenes_config(hass, async_add_entities, config): return async_add_entities( - HomeAssistantScene(hass, _process_scene_config(scene)) for scene in scene_config + HomeAssistantScene(hass, SCENECONFIG(scene[CONF_NAME], scene[CONF_ENTITIES])) + for scene in scene_config ) -def _process_scene_config(scene_config): - """Process passed in config into a format to work with. - - Async friendly. - """ - name = scene_config.get(CONF_NAME) - - states = {} - c_entities = dict(scene_config.get(CONF_ENTITIES, {})) - - for entity_id in c_entities: - if isinstance(c_entities[entity_id], dict): - entity_attrs = c_entities[entity_id].copy() - state = entity_attrs.pop(ATTR_STATE, None) - attributes = entity_attrs - else: - state = c_entities[entity_id] - attributes = {} - - # YAML translates 'on' to a boolean - # http://yaml.org/type/bool.html - if isinstance(state, bool): - state = STATE_ON if state else STATE_OFF - else: - state = str(state) - - states[entity_id.lower()] = State(entity_id, state, attributes) - - return SCENECONFIG(name, states) - - class HomeAssistantScene(Scene): """A scene is a group of entities and the states we want them to be.""" @@ -148,8 +160,13 @@ class HomeAssistantScene(Scene): @property def device_state_attributes(self): """Return the scene state attributes.""" - return {ATTR_ENTITY_ID: list(self.scene_config.states.keys())} + return {ATTR_ENTITY_ID: list(self.scene_config.states)} async def async_activate(self): """Activate scene. Try to get entities into requested state.""" - await async_reproduce_state(self.hass, self.scene_config.states.values(), True) + await async_reproduce_state( + self.hass, + self.scene_config.states.values(), + blocking=True, + context=self._context, + ) diff --git a/homeassistant/components/homeassistant/services.yaml b/homeassistant/components/homeassistant/services.yaml index 2219564abb8..cb3efb0d524 100644 --- a/homeassistant/components/homeassistant/services.yaml +++ b/homeassistant/components/homeassistant/services.yaml @@ -7,6 +7,16 @@ reload_core_config: restart: description: Restart the Home Assistant service. +set_location: + description: Update the Home Assistant location. + fields: + latitude: + description: Latitude of your location + example: 32.87336 + longitude: + description: Longitude of your location + example: 117.22743 + stop: description: Stop the Home Assistant service. diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index d8aafb8e238..4c300e0a934 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -31,6 +31,7 @@ from homeassistant.util.decorator import Registry from .const import ( BRIDGE_NAME, + CONF_ADVERTISE_IP, CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FEATURE_LIST, @@ -89,6 +90,9 @@ CONFIG_SCHEMA = vol.Schema( ), vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_IP_ADDRESS): vol.All(ipaddress.ip_address, cv.string), + vol.Optional(CONF_ADVERTISE_IP): vol.All( + ipaddress.ip_address, cv.string + ), vol.Optional(CONF_AUTO_START, default=DEFAULT_AUTO_START): cv.boolean, vol.Optional(CONF_SAFE_MODE, default=DEFAULT_SAFE_MODE): cv.boolean, vol.Optional(CONF_FILTER, default={}): FILTER_SCHEMA, @@ -112,13 +116,21 @@ async def async_setup(hass, config): name = conf[CONF_NAME] port = conf[CONF_PORT] ip_address = conf.get(CONF_IP_ADDRESS) + advertise_ip = conf.get(CONF_ADVERTISE_IP) auto_start = conf[CONF_AUTO_START] safe_mode = conf[CONF_SAFE_MODE] entity_filter = conf[CONF_FILTER] entity_config = conf[CONF_ENTITY_CONFIG] homekit = HomeKit( - hass, name, port, ip_address, entity_filter, entity_config, safe_mode + hass, + name, + port, + ip_address, + entity_filter, + entity_config, + safe_mode, + advertise_ip, ) await hass.async_add_executor_job(homekit.setup) @@ -265,7 +277,15 @@ class HomeKit: """Class to handle all actions between HomeKit and Home Assistant.""" def __init__( - self, hass, name, port, ip_address, entity_filter, entity_config, safe_mode + self, + hass, + name, + port, + ip_address, + entity_filter, + entity_config, + safe_mode, + advertise_ip=None, ): """Initialize a HomeKit object.""" self.hass = hass @@ -275,6 +295,7 @@ class HomeKit: self._filter = entity_filter self._config = entity_config self._safe_mode = safe_mode + self._advertise_ip = advertise_ip self.status = STATUS_READY self.bridge = None @@ -289,7 +310,11 @@ class HomeKit: ip_addr = self._ip_address or get_local_ip() path = self.hass.config.path(HOMEKIT_FILE) self.driver = HomeDriver( - self.hass, address=ip_addr, port=self._port, persist_file=path + self.hass, + address=ip_addr, + port=self._port, + persist_file=path, + advertised_address=self._advertise_ip, ) self.bridge = HomeBridge(self.hass, self.driver, self._name) if self._safe_mode: diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index d225225237f..82ec296da4b 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -10,6 +10,7 @@ ATTR_DISPLAY_NAME = "display_name" ATTR_VALUE = "value" # #### Config #### +CONF_ADVERTISE_IP = "advertise_ip" CONF_AUTO_START = "auto_start" CONF_ENTITY_CONFIG = "entity_config" CONF_FEATURE = "feature" diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py index 87c8d5247a5..a1450518e0c 100644 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -96,7 +96,7 @@ class TemperatureSensor(HomeAccessory): temperature = temperature_to_homekit(temperature, unit) self.char_temp.set_value(temperature) _LOGGER.debug( - "%s: Current temperature set to %d°C", self.entity_id, temperature + "%s: Current temperature set to %.1f°C", self.entity_id, temperature ) diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index d60c94d420d..608c9a974e5 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -235,7 +235,7 @@ def convert_to_float(state): def temperature_to_homekit(temperature, unit): """Convert temperature to Celsius for HomeKit.""" - return round(temp_util.convert(temperature, unit, TEMP_CELSIUS) * 2) / 2 + return round(temp_util.convert(temperature, unit, TEMP_CELSIUS), 1) def temperature_to_states(temperature, unit): diff --git a/homeassistant/components/homekit_controller/.translations/ru.json b/homeassistant/components/homekit_controller/.translations/ru.json index c7770c6a064..44a57a1eb25 100644 --- a/homeassistant/components/homekit_controller/.translations/ru.json +++ b/homeassistant/components/homekit_controller/.translations/ru.json @@ -24,14 +24,14 @@ "data": { "pairing_code": "\u041a\u043e\u0434 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f HomeKit (\u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 XXX-XX-XXX), \u0447\u0442\u043e\u0431\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u044d\u0442\u043e\u0442 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440", + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f HomeKit (\u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 XXX-XX-XXX), \u0447\u0442\u043e\u0431\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u044d\u0442\u043e\u0442 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440.", "title": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0441 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u043e\u043c HomeKit" }, "user": { "data": { "device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e, \u0441 \u043a\u043e\u0442\u043e\u0440\u044b\u043c \u043d\u0443\u0436\u043d\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435", + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e, \u0441 \u043a\u043e\u0442\u043e\u0440\u044b\u043c \u043d\u0443\u0436\u043d\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435.", "title": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0441 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u043e\u043c HomeKit" } }, diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 008e0f8566d..40bf87d6f0a 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -122,7 +122,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): _LOGGER.debug("Discovered device %s (%s - %s)", name, model, hkid) - # pylint: disable=unsupported-assignment-operation + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context["hkid"] = hkid self.context["title_placeholders"] = {"name": name} diff --git a/homeassistant/components/homematic/light.py b/homeassistant/components/homematic/light.py index 971a8a9cac0..32fa0bb358e 100644 --- a/homeassistant/components/homematic/light.py +++ b/homeassistant/components/homematic/light.py @@ -54,9 +54,12 @@ class HMLight(HMDevice, Light): @property def supported_features(self): """Flag supported features.""" + features = SUPPORT_BRIGHTNESS if "COLOR" in self._hmdevice.WRITENODE: - return SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_EFFECT - return SUPPORT_BRIGHTNESS + features |= SUPPORT_COLOR + if "PROGRAM" in self._hmdevice.WRITENODE: + features |= SUPPORT_EFFECT + return features @property def hs_color(self): @@ -110,4 +113,6 @@ class HMLight(HMDevice, Light): self._data[self._state] = None if self.supported_features & SUPPORT_COLOR: - self._data.update({"COLOR": None, "PROGRAM": None}) + self._data.update({"COLOR": None}) + if self.supported_features & SUPPORT_EFFECT: + self._data.update({"PROGRAM": None}) diff --git a/homeassistant/components/homematic/manifest.json b/homeassistant/components/homematic/manifest.json index 260e54e65c4..5db547e3f0a 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.60" + "pyhomematic==0.1.61" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/homematicip_cloud/.translations/ru.json b/homeassistant/components/homematicip_cloud/.translations/ru.json index 5155a42c4c3..57ab265d1c2 100644 --- a/homeassistant/components/homematicip_cloud/.translations/ru.json +++ b/homeassistant/components/homematicip_cloud/.translations/ru.json @@ -1,15 +1,15 @@ { "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.", - "connection_aborted": "\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 HMIP", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0442\u043e\u0447\u043a\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "connection_aborted": "\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 HMIP.", "unknown": "\u0412\u043e\u0437\u043d\u0438\u043a\u043b\u0430 \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "error": { "invalid_pin": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 PIN-\u043a\u043e\u0434, \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430.", "press_the_button": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u0441\u0438\u043d\u044e\u044e \u043a\u043d\u043e\u043f\u043a\u0443.", - "register_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430", - "timeout_button": "\u0412\u044b \u043d\u0435 \u043d\u0430\u0436\u0430\u043b\u0438 \u0441\u0438\u043d\u044e\u044e \u043a\u043d\u043e\u043f\u043a\u0443 \u0432 \u043f\u0440\u0435\u0434\u0435\u043b\u0430\u0445 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u043e\u0433\u043e \u0432\u0440\u0435\u043c\u0435\u043d\u0438, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430" + "register_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430.", + "timeout_button": "\u0412\u044b \u043d\u0435 \u043d\u0430\u0436\u0430\u043b\u0438 \u0441\u0438\u043d\u044e\u044e \u043a\u043d\u043e\u043f\u043a\u0443 \u0432 \u043f\u0440\u0435\u0434\u0435\u043b\u0430\u0445 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u043e\u0433\u043e \u0432\u0440\u0435\u043c\u0435\u043d\u0438, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430." }, "step": { "init": { diff --git a/homeassistant/components/homematicip_cloud/__init__.py b/homeassistant/components/homematicip_cloud/__init__.py index c8fb31998ef..9a0eb65aa3f 100644 --- a/homeassistant/components/homematicip_cloud/__init__.py +++ b/homeassistant/components/homematicip_cloud/__init__.py @@ -1,15 +1,16 @@ """Support for HomematicIP Cloud devices.""" import logging +from homematicip.aio.group import AsyncHeatingGroup import voluptuous as vol from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME -from homeassistant.core import HomeAssistant +from homeassistant.const import ATTR_ENTITY_ID, CONF_NAME from homeassistant.helpers import device_registry as dr import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.config_validation import comp_entity_ids +from homeassistant.helpers.typing import ConfigType, HomeAssistantType from .config_flow import configured_haps from .const import ( @@ -25,6 +26,7 @@ from .hap import HomematicipAuth, HomematicipHAP # noqa: F401 _LOGGER = logging.getLogger(__name__) +ATTR_CLIMATE_PROFILE_INDEX = "climate_profile_index" ATTR_DURATION = "duration" ATTR_ENDTIME = "endtime" ATTR_TEMPERATURE = "temperature" @@ -35,6 +37,7 @@ SERVICE_ACTIVATE_ECO_MODE_WITH_PERIOD = "activate_eco_mode_with_period" SERVICE_ACTIVATE_VACATION = "activate_vacation" SERVICE_DEACTIVATE_ECO_MODE = "deactivate_eco_mode" SERVICE_DEACTIVATE_VACATION = "deactivate_vacation" +SERVICE_SET_ACTIVE_CLIMATE_PROFILE = "set_active_climate_profile" CONFIG_SCHEMA = vol.Schema( { @@ -86,8 +89,15 @@ SCHEMA_DEACTIVATE_VACATION = vol.Schema( {vol.Optional(ATTR_ACCESSPOINT_ID): vol.All(str, vol.Length(min=24, max=24))} ) +SCHEMA_SET_ACTIVE_CLIMATE_PROFILE = vol.Schema( + { + vol.Required(ATTR_ENTITY_ID): comp_entity_ids, + vol.Required(ATTR_CLIMATE_PROFILE_INDEX): cv.positive_int, + } +) -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + +async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: """Set up the HomematicIP Cloud component.""" hass.data[DOMAIN] = {} @@ -117,9 +127,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if home: await home.activate_absence_with_duration(duration) else: - for hapid in hass.data[DOMAIN]: - home = hass.data[DOMAIN][hapid].home - await home.activate_absence_with_duration(duration) + for hap in hass.data[DOMAIN].values(): + await hap.home.activate_absence_with_duration(duration) hass.services.async_register( DOMAIN, @@ -138,9 +147,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if home: await home.activate_absence_with_period(endtime) else: - for hapid in hass.data[DOMAIN]: - home = hass.data[DOMAIN][hapid].home - await home.activate_absence_with_period(endtime) + for hap in hass.data[DOMAIN].values(): + await hap.home.activate_absence_with_period(endtime) hass.services.async_register( DOMAIN, @@ -160,9 +168,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if home: await home.activate_vacation(endtime, temperature) else: - for hapid in hass.data[DOMAIN]: - home = hass.data[DOMAIN][hapid].home - await home.activate_vacation(endtime, temperature) + for hap in hass.data[DOMAIN].values(): + await hap.home.activate_vacation(endtime, temperature) hass.services.async_register( DOMAIN, @@ -180,9 +187,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if home: await home.deactivate_absence() else: - for hapid in hass.data[DOMAIN]: - home = hass.data[DOMAIN][hapid].home - await home.deactivate_absence() + for hap in hass.data[DOMAIN].values(): + await hap.home.deactivate_absence() hass.services.async_register( DOMAIN, @@ -200,9 +206,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if home: await home.deactivate_vacation() else: - for hapid in hass.data[DOMAIN]: - home = hass.data[DOMAIN][hapid].home - await home.deactivate_vacation() + for hap in hass.data[DOMAIN].values(): + await hap.home.deactivate_vacation() hass.services.async_register( DOMAIN, @@ -211,17 +216,42 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: schema=SCHEMA_DEACTIVATE_VACATION, ) + async def _set_active_climate_profile(service): + """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 + + for hap in hass.data[DOMAIN].values(): + if entity_id_list != "all": + for entity_id in entity_id_list: + group = hap.hmip_device_by_entity_id.get(entity_id) + if group: + await group.set_active_profile(climate_profile_index) + else: + for group in hap.home.groups: + if isinstance(group, AsyncHeatingGroup): + await group.set_active_profile(climate_profile_index) + + hass.services.async_register( + DOMAIN, + SERVICE_SET_ACTIVE_CLIMATE_PROFILE, + _set_active_climate_profile, + schema=SCHEMA_SET_ACTIVE_CLIMATE_PROFILE, + ) + def _get_home(hapid: str): """Return a HmIP home.""" - hap = hass.data[DOMAIN][hapid] + hap = hass.data[DOMAIN].get(hapid) if hap: return hap.home + + _LOGGER.info("No matching access point found for access point id %s", hapid) return None return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: """Set up an access point from a config entry.""" hap = HomematicipHAP(hass, entry) hapid = entry.data[HMIPC_HAPID].replace("-", "").upper() diff --git a/homeassistant/components/homematicip_cloud/alarm_control_panel.py b/homeassistant/components/homematicip_cloud/alarm_control_panel.py index 592d234225c..f61bf6f6b56 100644 --- a/homeassistant/components/homematicip_cloud/alarm_control_panel.py +++ b/homeassistant/components/homematicip_cloud/alarm_control_panel.py @@ -2,7 +2,6 @@ import logging from homematicip.aio.group import AsyncSecurityZoneGroup -from homematicip.aio.home import AsyncHome from homematicip.base.enums import WindowState from homeassistant.components.alarm_control_panel import AlarmControlPanel @@ -13,9 +12,10 @@ from homeassistant.const import ( STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, ) -from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import HomeAssistantType from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID +from .hap import HomematicipHAP _LOGGER = logging.getLogger(__name__) @@ -28,18 +28,18 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities + hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP alrm control panel from a config entry.""" - home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home + hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] devices = [] security_zones = [] - for group in home.groups: + for group in hap.home.groups: if isinstance(group, AsyncSecurityZoneGroup): security_zones.append(group) if security_zones: - devices.append(HomematicipAlarmControlPanel(home, security_zones)) + devices.append(HomematicipAlarmControlPanel(hap, security_zones)) if devices: async_add_entities(devices) @@ -48,9 +48,9 @@ async def async_setup_entry( class HomematicipAlarmControlPanel(AlarmControlPanel): """Representation of an alarm control panel.""" - def __init__(self, home: AsyncHome, security_zones) -> None: + def __init__(self, hap: HomematicipHAP, security_zones) -> None: """Initialize the alarm control panel.""" - self._home = home + self._home = hap.home self.alarm_state = STATE_ALARM_DISARMED for security_zone in security_zones: @@ -59,6 +59,17 @@ class HomematicipAlarmControlPanel(AlarmControlPanel): else: self._external_alarm_zone = security_zone + @property + def device_info(self): + """Return device specific attributes.""" + return { + "identifiers": {(HMIPC_DOMAIN, f"ACP {self._home.id}")}, + "name": self.name, + "manufacturer": "eQ-3", + "model": CONST_ALARM_CONTROL_PANEL_NAME, + "via_device": (HMIPC_DOMAIN, self._home.id), + } + @property def state(self) -> str: """Return the state of the device.""" diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index 4ac4614379b..e308f96c208 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -2,6 +2,7 @@ import logging from homematicip.aio.device import ( + AsyncAccelerationSensor, AsyncContactInterface, AsyncDevice, AsyncFullFlushContactInterface, @@ -19,7 +20,6 @@ from homematicip.aio.device import ( AsyncWeatherSensorPro, ) from homematicip.aio.group import AsyncSecurityGroup, AsyncSecurityZoneGroup -from homematicip.aio.home import AsyncHome from homematicip.base.enums import SmokeDetectorAlarmType, WindowState from homeassistant.components.binary_sensor import ( @@ -28,6 +28,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_LIGHT, DEVICE_CLASS_MOISTURE, DEVICE_CLASS_MOTION, + DEVICE_CLASS_MOVING, DEVICE_CLASS_OPENING, DEVICE_CLASS_PRESENCE, DEVICE_CLASS_SAFETY, @@ -35,13 +36,18 @@ from homeassistant.components.binary_sensor import ( BinarySensorDevice, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import HomeAssistantType from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice -from .device import ATTR_GROUP_MEMBER_UNREACHABLE, ATTR_IS_GROUP, ATTR_MODEL_TYPE +from .device import ATTR_GROUP_MEMBER_UNREACHABLE +from .hap import HomematicipHAP _LOGGER = logging.getLogger(__name__) +ATTR_ACCELERATION_SENSOR_MODE = "acceleration_sensor_mode" +ATTR_ACCELERATION_SENSOR_NEUTRAL_POSITION = "acceleration_sensor_neutral_position" +ATTR_ACCELERATION_SENSOR_SENSITIVITY = "acceleration_sensor_sensitivity" +ATTR_ACCELERATION_SENSOR_TRIGGER_ANGLE = "acceleration_sensor_trigger_angle" ATTR_LOW_BATTERY = "low_battery" ATTR_MOISTURE_DETECTED = "moisture_detected" ATTR_MOTION_DETECTED = "motion_detected" @@ -54,7 +60,6 @@ ATTR_WINDOW_STATE = "window_state" GROUP_ATTRIBUTES = { "lowBat": ATTR_LOW_BATTERY, - "modelType": ATTR_MODEL_TYPE, "moistureDetected": ATTR_MOISTURE_DETECTED, "motionDetected": ATTR_MOTION_DETECTED, "powerMainsFailure": ATTR_POWER_MAINS_FAILURE, @@ -63,6 +68,13 @@ GROUP_ATTRIBUTES = { "waterlevelDetected": ATTR_WATER_LEVEL_DETECTED, } +SAM_DEVICE_ATTRIBUTES = { + "accelerationSensorNeutralPosition": ATTR_ACCELERATION_SENSOR_NEUTRAL_POSITION, + "accelerationSensorMode": ATTR_ACCELERATION_SENSOR_MODE, + "accelerationSensorSensitivity": ATTR_ACCELERATION_SENSOR_SENSITIVITY, + "accelerationSensorTriggerAngle": ATTR_ACCELERATION_SENSOR_TRIGGER_ANGLE, +} + async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the HomematicIP Cloud binary sensor devices.""" @@ -70,19 +82,21 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities + hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP Cloud binary sensor from a config entry.""" - home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home + hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] devices = [] - for device in home.devices: + for device in hap.home.devices: + if isinstance(device, AsyncAccelerationSensor): + devices.append(HomematicipAccelerationSensor(hap, device)) if isinstance(device, (AsyncContactInterface, AsyncFullFlushContactInterface)): - devices.append(HomematicipContactInterface(home, device)) + devices.append(HomematicipContactInterface(hap, device)) if isinstance( device, (AsyncShutterContact, AsyncShutterContactMagnetic, AsyncRotaryHandleSensor), ): - devices.append(HomematicipShutterContact(home, device)) + devices.append(HomematicipShutterContact(hap, device)) if isinstance( device, ( @@ -91,33 +105,59 @@ async def async_setup_entry( AsyncMotionDetectorPushButton, ), ): - devices.append(HomematicipMotionDetector(home, device)) + devices.append(HomematicipMotionDetector(hap, device)) if isinstance(device, AsyncPresenceDetectorIndoor): - devices.append(HomematicipPresenceDetector(home, device)) + devices.append(HomematicipPresenceDetector(hap, device)) if isinstance(device, AsyncSmokeDetector): - devices.append(HomematicipSmokeDetector(home, device)) + devices.append(HomematicipSmokeDetector(hap, device)) if isinstance(device, AsyncWaterSensor): - devices.append(HomematicipWaterDetector(home, device)) + devices.append(HomematicipWaterDetector(hap, device)) if isinstance(device, (AsyncWeatherSensorPlus, AsyncWeatherSensorPro)): - devices.append(HomematicipRainSensor(home, device)) + devices.append(HomematicipRainSensor(hap, device)) if isinstance( device, (AsyncWeatherSensor, AsyncWeatherSensorPlus, AsyncWeatherSensorPro) ): - devices.append(HomematicipStormSensor(home, device)) - devices.append(HomematicipSunshineSensor(home, device)) + devices.append(HomematicipStormSensor(hap, device)) + devices.append(HomematicipSunshineSensor(hap, device)) if isinstance(device, AsyncDevice) and device.lowBat is not None: - devices.append(HomematicipBatterySensor(home, device)) + devices.append(HomematicipBatterySensor(hap, device)) - for group in home.groups: + for group in hap.home.groups: if isinstance(group, AsyncSecurityGroup): - devices.append(HomematicipSecuritySensorGroup(home, group)) + devices.append(HomematicipSecuritySensorGroup(hap, group)) elif isinstance(group, AsyncSecurityZoneGroup): - devices.append(HomematicipSecurityZoneSensorGroup(home, group)) + devices.append(HomematicipSecurityZoneSensorGroup(hap, group)) if devices: async_add_entities(devices) +class HomematicipAccelerationSensor(HomematicipGenericDevice, BinarySensorDevice): + """Representation of a HomematicIP Cloud acceleration sensor.""" + + @property + def device_class(self) -> str: + """Return the class of this sensor.""" + return DEVICE_CLASS_MOVING + + @property + def is_on(self) -> bool: + """Return true if acceleration is detected.""" + return self._device.accelerationSensorTriggered + + @property + def device_state_attributes(self): + """Return the state attributes of the acceleration sensor.""" + state_attr = super().device_state_attributes + + for attr, attr_key in SAM_DEVICE_ATTRIBUTES.items(): + attr_value = getattr(self._device, attr, None) + if attr_value: + state_attr[attr_key] = attr_value + + return state_attr + + class HomematicipContactInterface(HomematicipGenericDevice, BinarySensorDevice): """Representation of a HomematicIP Cloud contact interface.""" @@ -209,9 +249,9 @@ class HomematicipWaterDetector(HomematicipGenericDevice, BinarySensorDevice): class HomematicipStormSensor(HomematicipGenericDevice, BinarySensorDevice): """Representation of a HomematicIP Cloud storm sensor.""" - def __init__(self, home: AsyncHome, device) -> None: + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize storm sensor.""" - super().__init__(home, device, "Storm") + super().__init__(hap, device, "Storm") @property def icon(self) -> str: @@ -227,9 +267,9 @@ class HomematicipStormSensor(HomematicipGenericDevice, BinarySensorDevice): class HomematicipRainSensor(HomematicipGenericDevice, BinarySensorDevice): """Representation of a HomematicIP Cloud rain sensor.""" - def __init__(self, home: AsyncHome, device) -> None: + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize rain sensor.""" - super().__init__(home, device, "Raining") + super().__init__(hap, device, "Raining") @property def device_class(self) -> str: @@ -245,9 +285,9 @@ class HomematicipRainSensor(HomematicipGenericDevice, BinarySensorDevice): class HomematicipSunshineSensor(HomematicipGenericDevice, BinarySensorDevice): """Representation of a HomematicIP Cloud sunshine sensor.""" - def __init__(self, home: AsyncHome, device) -> None: + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize sunshine sensor.""" - super().__init__(home, device, "Sunshine") + super().__init__(hap, device, "Sunshine") @property def device_class(self) -> str: @@ -274,9 +314,9 @@ class HomematicipSunshineSensor(HomematicipGenericDevice, BinarySensorDevice): class HomematicipBatterySensor(HomematicipGenericDevice, BinarySensorDevice): """Representation of a HomematicIP Cloud low battery sensor.""" - def __init__(self, home: AsyncHome, device) -> None: + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize battery sensor.""" - super().__init__(home, device, "Battery") + super().__init__(hap, device, "Battery") @property def device_class(self) -> str: @@ -292,10 +332,10 @@ class HomematicipBatterySensor(HomematicipGenericDevice, BinarySensorDevice): class HomematicipSecurityZoneSensorGroup(HomematicipGenericDevice, BinarySensorDevice): """Representation of a HomematicIP Cloud security zone group.""" - def __init__(self, home: AsyncHome, device, post: str = "SecurityZone") -> None: + def __init__(self, hap: HomematicipHAP, device, post: str = "SecurityZone") -> None: """Initialize security zone group.""" device.modelType = f"HmIP-{post}" - super().__init__(home, device, post) + super().__init__(hap, device, post) @property def device_class(self) -> str: @@ -312,7 +352,7 @@ class HomematicipSecurityZoneSensorGroup(HomematicipGenericDevice, BinarySensorD @property def device_state_attributes(self): """Return the state attributes of the security zone group.""" - state_attr = {ATTR_MODEL_TYPE: self._device.modelType, ATTR_IS_GROUP: True} + state_attr = super().device_state_attributes for attr, attr_key in GROUP_ATTRIBUTES.items(): attr_value = getattr(self._device, attr, None) @@ -349,9 +389,9 @@ class HomematicipSecuritySensorGroup( ): """Representation of a HomematicIP security group.""" - def __init__(self, home: AsyncHome, device) -> None: + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize security group.""" - super().__init__(home, device, "Sensors") + super().__init__(hap, device, "Sensors") @property def device_state_attributes(self): diff --git a/homeassistant/components/homematicip_cloud/climate.py b/homeassistant/components/homematicip_cloud/climate.py index 794a8b44cbc..74d647c8c33 100644 --- a/homeassistant/components/homematicip_cloud/climate.py +++ b/homeassistant/components/homematicip_cloud/climate.py @@ -4,8 +4,7 @@ from typing import Awaitable from homematicip.aio.device import AsyncHeatingThermostat, AsyncHeatingThermostatCompact from homematicip.aio.group import AsyncHeatingGroup -from homematicip.aio.home import AsyncHome -from homematicip.base.enums import AbsenceType +from homematicip.base.enums import AbsenceType, GroupType from homematicip.functionalHomes import IndoorClimateHome from homeassistant.components.climate import ClimateDevice @@ -21,9 +20,13 @@ from homeassistant.components.climate.const import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS -from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import HomeAssistantType from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice +from .hap import HomematicipHAP + +HEATING_PROFILES = {"PROFILE_1": 0, "PROFILE_2": 1, "PROFILE_3": 2} +COOLING_PROFILES = {"PROFILE_4": 3, "PROFILE_5": 4, "PROFILE_6": 5} _LOGGER = logging.getLogger(__name__) @@ -38,14 +41,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities + hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP climate from a config entry.""" - home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home + hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] devices = [] - for device in home.groups: + for device in hap.home.groups: if isinstance(device, AsyncHeatingGroup): - devices.append(HomematicipHeatingGroup(home, device)) + devices.append(HomematicipHeatingGroup(hap, device)) if devices: async_add_entities(devices) @@ -54,13 +57,24 @@ async def async_setup_entry( class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): """Representation of a HomematicIP heating group.""" - def __init__(self, home: AsyncHome, device) -> None: + def __init__(self, hap: HomematicipHAP, device: AsyncHeatingGroup) -> None: """Initialize heating group.""" - device.modelType = "Group-Heating" + device.modelType = "HmIP-Heating-Group" self._simple_heating = None if device.actualTemperature is None: self._simple_heating = _get_first_heating_thermostat(device) - super().__init__(home, device) + super().__init__(hap, device) + + @property + def device_info(self): + """Return device specific attributes.""" + return { + "identifiers": {(HMIPC_DOMAIN, self._device.id)}, + "name": self._device.label, + "manufacturer": "eQ-3", + "model": self._device.modelType, + "via_device": (HMIPC_DOMAIN, self._device.homeId), + } @property def temperature_unit(self) -> str: @@ -96,7 +110,7 @@ class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): Need to be one of HVAC_MODE_*. """ if self._device.boostMode: - return HVAC_MODE_AUTO + return HVAC_MODE_HEAT if self._device.controlMode == HMIP_MANUAL_CM: return HVAC_MODE_HEAT @@ -118,6 +132,8 @@ class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): """ if self._device.boostMode: return PRESET_BOOST + if self.hvac_mode == HVAC_MODE_HEAT: + return PRESET_NONE if self._device.controlMode == HMIP_ECO_CM: absence_type = self._home.get_functionalHome(IndoorClimateHome).absenceType if absence_type == AbsenceType.VACATION: @@ -129,15 +145,15 @@ class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): ]: return PRESET_ECO - return PRESET_NONE + if self._device.activeProfile: + return self._device.activeProfile.name @property def preset_modes(self): - """Return a list of available preset modes. - - Requires SUPPORT_PRESET_MODE. - """ - return [PRESET_NONE, PRESET_BOOST] + """Return a list of available preset modes incl profiles.""" + presets = [PRESET_NONE, PRESET_BOOST] + presets.extend(self._device_profile_names) + return presets @property def min_temp(self) -> float: @@ -169,6 +185,46 @@ class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): await self._device.set_boost(False) if preset_mode == PRESET_BOOST: await self._device.set_boost() + if preset_mode in self._device_profile_names: + profile_idx = self._get_profile_idx_by_name(preset_mode) + await self.async_set_hvac_mode(HVAC_MODE_AUTO) + await self._device.set_active_profile(profile_idx) + + @property + def _device_profiles(self): + """Return the relevant profiles of the device.""" + return [ + profile + for profile in self._device.profiles + if profile.visible + and profile.name != "" + and profile.index in self._relevant_profile_group + ] + + @property + def _device_profile_names(self): + """Return a collection of profile names.""" + return [profile.name for profile in self._device_profiles] + + def _get_profile_idx_by_name(self, profile_name): + """Return a profile index by name.""" + relevant_index = self._relevant_profile_group + index_name = [ + profile.index + for profile in self._device_profiles + if profile.name == profile_name + ] + + return relevant_index[index_name[0]] + + @property + def _relevant_profile_group(self): + """Return the relevant profile groups.""" + return ( + HEATING_PROFILES + if self._device.groupType == GroupType.HEATING + else COOLING_PROFILES + ) def _get_first_heating_thermostat(heating_group: AsyncHeatingGroup): @@ -176,4 +232,3 @@ def _get_first_heating_thermostat(heating_group: AsyncHeatingGroup): for device in heating_group.devices: if isinstance(device, (AsyncHeatingThermostat, AsyncHeatingThermostatCompact)): return device - return None diff --git a/homeassistant/components/homematicip_cloud/config_flow.py b/homeassistant/components/homematicip_cloud/config_flow.py index a94ea7b53f1..1488f02f13b 100644 --- a/homeassistant/components/homematicip_cloud/config_flow.py +++ b/homeassistant/components/homematicip_cloud/config_flow.py @@ -4,7 +4,8 @@ from typing import Set import voluptuous as vol from homeassistant import config_entries -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import callback +from homeassistant.helpers.typing import HomeAssistantType from .const import ( _LOGGER, @@ -18,7 +19,7 @@ from .hap import HomematicipAuth @callback -def configured_haps(hass: HomeAssistant) -> Set[str]: +def configured_haps(hass: HomeAssistantType) -> Set[str]: """Return a set of the configured access points.""" return set( entry.data[HMIPC_HAPID] diff --git a/homeassistant/components/homematicip_cloud/cover.py b/homeassistant/components/homematicip_cloud/cover.py index 9252c4322d9..63ac6f7310c 100644 --- a/homeassistant/components/homematicip_cloud/cover.py +++ b/homeassistant/components/homematicip_cloud/cover.py @@ -10,7 +10,7 @@ from homeassistant.components.cover import ( CoverDevice, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import HomeAssistantType from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice @@ -28,16 +28,16 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities + hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP cover from a config entry.""" - home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home + hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] devices = [] - for device in home.devices: + for device in hap.home.devices: if isinstance(device, AsyncFullFlushBlind): - devices.append(HomematicipCoverSlats(home, device)) + devices.append(HomematicipCoverSlats(hap, device)) elif isinstance(device, AsyncFullFlushShutter): - devices.append(HomematicipCoverShutter(home, device)) + devices.append(HomematicipCoverShutter(hap, device)) if devices: async_add_entities(devices) diff --git a/homeassistant/components/homematicip_cloud/device.py b/homeassistant/components/homematicip_cloud/device.py index 1273278189d..b05c0e06928 100644 --- a/homeassistant/components/homematicip_cloud/device.py +++ b/homeassistant/components/homematicip_cloud/device.py @@ -3,13 +3,15 @@ import logging from typing import Optional from homematicip.aio.device import AsyncDevice -from homematicip.aio.home import AsyncHome +from homematicip.aio.group import AsyncGroup -from homeassistant.components import homematicip_cloud from homeassistant.core import callback from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity import Entity +from .const import DOMAIN as HMIPC_DOMAIN +from .hap import HomematicipHAP + _LOGGER = logging.getLogger(__name__) ATTR_MODEL_TYPE = "model_type" @@ -35,22 +37,25 @@ DEVICE_ATTRIBUTE_ICONS = { DEVICE_ATTRIBUTES = { "modelType": ATTR_MODEL_TYPE, - "id": ATTR_ID, "sabotage": ATTR_SABOTAGE, "rssiDeviceValue": ATTR_RSSI_DEVICE, "rssiPeerValue": ATTR_RSSI_PEER, "deviceOverheated": ATTR_DEVICE_OVERHEATED, "deviceOverloaded": ATTR_DEVICE_OVERLOADED, "deviceUndervoltage": ATTR_DEVICE_UNTERVOLTAGE, + "id": ATTR_ID, } +GROUP_ATTRIBUTES = {"modelType": ATTR_MODEL_TYPE} + class HomematicipGenericDevice(Entity): """Representation of an HomematicIP generic device.""" - def __init__(self, home: AsyncHome, device, post: Optional[str] = None) -> None: + def __init__(self, hap: HomematicipHAP, device, post: Optional[str] = None) -> None: """Initialize the generic device.""" - self._home = home + self._hap = hap + self._home = hap.home self._device = device self.post = post # Marker showing that the HmIP device hase been removed. @@ -65,18 +70,19 @@ class HomematicipGenericDevice(Entity): return { "identifiers": { # Serial numbers of Homematic IP device - (homematicip_cloud.DOMAIN, self._device.id) + (HMIPC_DOMAIN, self._device.id) }, "name": self._device.label, "manufacturer": self._device.oem, "model": self._device.modelType, "sw_version": self._device.firmwareVersion, - "via_device": (homematicip_cloud.DOMAIN, self._device.homeId), + "via_device": (HMIPC_DOMAIN, self._device.homeId), } return None async def async_added_to_hass(self): """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) @@ -100,6 +106,7 @@ class HomematicipGenericDevice(Entity): # Only go further if the device/entity should be removed from registries # due to a removal of the HmIP device. if self.hmip_device_removed: + del self._hap.hmip_device_by_entity_id[self.entity_id] await self.async_remove_from_registries() async def async_remove_from_registries(self) -> None: @@ -173,6 +180,7 @@ class HomematicipGenericDevice(Entity): def device_state_attributes(self): """Return the state attributes of the generic device.""" state_attr = {} + if isinstance(self._device, AsyncDevice): for attr, attr_key in DEVICE_ATTRIBUTES.items(): attr_value = getattr(self._device, attr, None) @@ -181,4 +189,12 @@ class HomematicipGenericDevice(Entity): state_attr[ATTR_IS_GROUP] = False + if isinstance(self._device, AsyncGroup): + for attr, attr_key in GROUP_ATTRIBUTES.items(): + attr_value = getattr(self._device, attr, None) + if attr_value: + state_attr[attr_key] = attr_value + + state_attr[ATTR_IS_GROUP] = True + return state_attr diff --git a/homeassistant/components/homematicip_cloud/hap.py b/homeassistant/components/homematicip_cloud/hap.py index abba183d339..bef04180c6f 100644 --- a/homeassistant/components/homematicip_cloud/hap.py +++ b/homeassistant/components/homematicip_cloud/hap.py @@ -8,9 +8,10 @@ from homematicip.base.base_connection import HmipConnectionError from homematicip.base.enums import EventType from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.typing import HomeAssistantType from .const import COMPONENTS, HMIPC_AUTHTOKEN, HMIPC_HAPID, HMIPC_NAME, HMIPC_PIN from .errors import HmipcConnectionError @@ -53,7 +54,7 @@ class HomematicipAuth: except HmipConnectionError: return False - async def get_auth(self, hass, hapid, pin): + async def get_auth(self, hass: HomeAssistantType, hapid, pin): """Create a HomematicIP access point object.""" auth = AsyncAuth(hass.loop, async_get_clientsession(hass)) try: @@ -69,7 +70,7 @@ class HomematicipAuth: class HomematicipHAP: """Manages HomematicIP HTTP and WebSocket connection.""" - def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: + def __init__(self, hass: HomeAssistantType, config_entry: ConfigEntry) -> None: """Initialize HomematicIP Cloud connection.""" self.hass = hass self.config_entry = config_entry @@ -79,6 +80,7 @@ class HomematicipHAP: self._retry_task = None self._tries = 0 self._accesspoint_connected = True + self.hmip_device_by_entity_id = {} async def async_setup(self, tries: int = 0): """Initialize connection.""" @@ -219,10 +221,11 @@ class HomematicipHAP: await self.hass.config_entries.async_forward_entry_unload( self.config_entry, component ) + self.hmip_device_by_entity_id = {} return True async def get_hap( - self, hass: HomeAssistant, hapid: str, authtoken: str, name: str + self, hass: HomeAssistantType, hapid: str, authtoken: str, name: str ) -> AsyncHome: """Create a HomematicIP access point object.""" home = AsyncHome(hass.loop, async_get_clientsession(hass)) diff --git a/homeassistant/components/homematicip_cloud/light.py b/homeassistant/components/homematicip_cloud/light.py index 42ff6d30478..46a8d95729f 100644 --- a/homeassistant/components/homematicip_cloud/light.py +++ b/homeassistant/components/homematicip_cloud/light.py @@ -9,7 +9,6 @@ from homematicip.aio.device import ( AsyncFullFlushDimmer, AsyncPluggableDimmer, ) -from homematicip.aio.home import AsyncHome from homematicip.base.enums import RGBColorState from homematicip.base.functionalChannels import NotificationLightChannel @@ -22,9 +21,10 @@ from homeassistant.components.light import ( Light, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import HomeAssistantType from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice +from .hap import HomematicipHAP _LOGGER = logging.getLogger(__name__) @@ -38,29 +38,29 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities + hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP Cloud lights from a config entry.""" - home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home + hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] devices = [] - for device in home.devices: + for device in hap.home.devices: if isinstance(device, AsyncBrandSwitchMeasuring): - devices.append(HomematicipLightMeasuring(home, device)) + devices.append(HomematicipLightMeasuring(hap, device)) elif isinstance(device, AsyncBrandSwitchNotificationLight): - devices.append(HomematicipLight(home, device)) + devices.append(HomematicipLight(hap, device)) devices.append( - HomematicipNotificationLight(home, device, device.topLightChannelIndex) + HomematicipNotificationLight(hap, device, device.topLightChannelIndex) ) devices.append( HomematicipNotificationLight( - home, device, device.bottomLightChannelIndex + hap, device, device.bottomLightChannelIndex ) ) elif isinstance( device, (AsyncDimmer, AsyncPluggableDimmer, AsyncBrandDimmer, AsyncFullFlushDimmer), ): - devices.append(HomematicipDimmer(home, device)) + devices.append(HomematicipDimmer(hap, device)) if devices: async_add_entities(devices) @@ -69,9 +69,9 @@ async def async_setup_entry( class HomematicipLight(HomematicipGenericDevice, Light): """Representation of a HomematicIP Cloud light device.""" - def __init__(self, home: AsyncHome, device) -> None: + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the light device.""" - super().__init__(home, device) + super().__init__(hap, device) @property def is_on(self) -> bool: @@ -107,9 +107,9 @@ class HomematicipLightMeasuring(HomematicipLight): class HomematicipDimmer(HomematicipGenericDevice, Light): """Representation of HomematicIP Cloud dimmer light device.""" - def __init__(self, home: AsyncHome, device) -> None: + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the dimmer light device.""" - super().__init__(home, device) + super().__init__(hap, device) @property def is_on(self) -> bool: @@ -119,9 +119,7 @@ class HomematicipDimmer(HomematicipGenericDevice, Light): @property def brightness(self) -> int: """Return the brightness of this light between 0..255.""" - if self._device.dimLevel: - return int(self._device.dimLevel * 255) - return 0 + return int((self._device.dimLevel or 0.0) * 255) @property def supported_features(self) -> int: @@ -143,13 +141,13 @@ class HomematicipDimmer(HomematicipGenericDevice, Light): class HomematicipNotificationLight(HomematicipGenericDevice, Light): """Representation of HomematicIP Cloud dimmer light device.""" - def __init__(self, home: AsyncHome, device, channel: int) -> None: + def __init__(self, hap: HomematicipHAP, device, channel: int) -> None: """Initialize the dimmer light device.""" self.channel = channel if self.channel == 2: - super().__init__(home, device, "Top") + super().__init__(hap, device, "Top") else: - super().__init__(home, device, "Bottom") + super().__init__(hap, device, "Bottom") self._color_switcher = { RGBColorState.WHITE: [0.0, 0.0], @@ -176,9 +174,7 @@ class HomematicipNotificationLight(HomematicipGenericDevice, Light): @property def brightness(self) -> int: """Return the brightness of this light between 0..255.""" - if self._func_channel.dimLevel: - return int(self._func_channel.dimLevel * 255) - return 0 + return int((self._func_channel.dimLevel or 0.0) * 255) @property def hs_color(self) -> tuple: diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index 40c8c7c3598..4feef19c8da 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.12" + "homematicip==0.10.13" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index 770921288b9..9caa72ba15f 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -20,7 +20,6 @@ from homematicip.aio.device import ( AsyncWeatherSensorPlus, AsyncWeatherSensorPro, ) -from homematicip.aio.home import AsyncHome from homematicip.base.enums import ValveState from homeassistant.config_entries import ConfigEntry @@ -32,10 +31,11 @@ from homeassistant.const import ( POWER_WATT, TEMP_CELSIUS, ) -from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import HomeAssistantType from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice from .device import ATTR_IS_GROUP, ATTR_MODEL_TYPE +from .hap import HomematicipHAP _LOGGER = logging.getLogger(__name__) @@ -52,15 +52,15 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities + hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP Cloud sensors from a config entry.""" - home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home - devices = [HomematicipAccesspointStatus(home)] - for device in home.devices: + hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] + devices = [HomematicipAccesspointStatus(hap)] + for device in hap.home.devices: if isinstance(device, (AsyncHeatingThermostat, AsyncHeatingThermostatCompact)): - devices.append(HomematicipHeatingThermostat(home, device)) - devices.append(HomematicipTemperatureSensor(home, device)) + devices.append(HomematicipHeatingThermostat(hap, device)) + devices.append(HomematicipTemperatureSensor(hap, device)) if isinstance( device, ( @@ -72,8 +72,8 @@ async def async_setup_entry( AsyncWeatherSensorPro, ), ): - devices.append(HomematicipTemperatureSensor(home, device)) - devices.append(HomematicipHumiditySensor(home, device)) + devices.append(HomematicipTemperatureSensor(hap, device)) + devices.append(HomematicipHumiditySensor(hap, device)) if isinstance( device, ( @@ -87,7 +87,7 @@ async def async_setup_entry( AsyncWeatherSensorPro, ), ): - devices.append(HomematicipIlluminanceSensor(home, device)) + devices.append(HomematicipIlluminanceSensor(hap, device)) if isinstance( device, ( @@ -96,15 +96,15 @@ async def async_setup_entry( AsyncFullFlushSwitchMeasuring, ), ): - devices.append(HomematicipPowerSensor(home, device)) + devices.append(HomematicipPowerSensor(hap, device)) if isinstance( device, (AsyncWeatherSensor, AsyncWeatherSensorPlus, AsyncWeatherSensorPro) ): - devices.append(HomematicipWindspeedSensor(home, device)) + devices.append(HomematicipWindspeedSensor(hap, device)) if isinstance(device, (AsyncWeatherSensorPlus, AsyncWeatherSensorPro)): - devices.append(HomematicipTodayRainSensor(home, device)) + devices.append(HomematicipTodayRainSensor(hap, device)) if isinstance(device, AsyncPassageDetector): - devices.append(HomematicipPassageDetectorDeltaCounter(home, device)) + devices.append(HomematicipPassageDetectorDeltaCounter(hap, device)) if devices: async_add_entities(devices) @@ -113,9 +113,9 @@ async def async_setup_entry( class HomematicipAccesspointStatus(HomematicipGenericDevice): """Representation of an HomeMaticIP Cloud access point.""" - def __init__(self, home: AsyncHome) -> None: + def __init__(self, hap: HomematicipHAP) -> None: """Initialize access point device.""" - super().__init__(home, home) + super().__init__(hap, hap.home) @property def device_info(self): @@ -151,15 +151,20 @@ class HomematicipAccesspointStatus(HomematicipGenericDevice): @property def device_state_attributes(self): """Return the state attributes of the access point.""" - return {ATTR_MODEL_TYPE: self._device.modelType, ATTR_IS_GROUP: False} + state_attr = super().device_state_attributes + + state_attr[ATTR_MODEL_TYPE] = "HmIP-HAP" + state_attr[ATTR_IS_GROUP] = False + + return state_attr class HomematicipHeatingThermostat(HomematicipGenericDevice): """Representation of a HomematicIP heating thermostat device.""" - def __init__(self, home: AsyncHome, device) -> None: + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize heating thermostat device.""" - super().__init__(home, device, "Heating") + super().__init__(hap, device, "Heating") @property def icon(self) -> str: @@ -186,9 +191,9 @@ class HomematicipHeatingThermostat(HomematicipGenericDevice): class HomematicipHumiditySensor(HomematicipGenericDevice): """Representation of a HomematicIP Cloud humidity device.""" - def __init__(self, home: AsyncHome, device) -> None: + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the thermometer device.""" - super().__init__(home, device, "Humidity") + super().__init__(hap, device, "Humidity") @property def device_class(self) -> str: @@ -209,9 +214,9 @@ class HomematicipHumiditySensor(HomematicipGenericDevice): class HomematicipTemperatureSensor(HomematicipGenericDevice): """Representation of a HomematicIP Cloud thermometer device.""" - def __init__(self, home: AsyncHome, device) -> None: + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the thermometer device.""" - super().__init__(home, device, "Temperature") + super().__init__(hap, device, "Temperature") @property def device_class(self) -> str: @@ -246,9 +251,9 @@ class HomematicipTemperatureSensor(HomematicipGenericDevice): class HomematicipIlluminanceSensor(HomematicipGenericDevice): """Representation of a HomematicIP Illuminance device.""" - def __init__(self, home: AsyncHome, device) -> None: + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the device.""" - super().__init__(home, device, "Illuminance") + super().__init__(hap, device, "Illuminance") @property def device_class(self) -> str: @@ -272,9 +277,9 @@ class HomematicipIlluminanceSensor(HomematicipGenericDevice): class HomematicipPowerSensor(HomematicipGenericDevice): """Representation of a HomematicIP power measuring device.""" - def __init__(self, home: AsyncHome, device) -> None: + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the device.""" - super().__init__(home, device, "Power") + super().__init__(hap, device, "Power") @property def device_class(self) -> str: @@ -295,9 +300,9 @@ class HomematicipPowerSensor(HomematicipGenericDevice): class HomematicipWindspeedSensor(HomematicipGenericDevice): """Representation of a HomematicIP wind speed sensor.""" - def __init__(self, home: AsyncHome, device) -> None: + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the device.""" - super().__init__(home, device, "Windspeed") + super().__init__(hap, device, "Windspeed") @property def state(self) -> float: @@ -315,7 +320,7 @@ class HomematicipWindspeedSensor(HomematicipGenericDevice): state_attr = super().device_state_attributes wind_direction = getattr(self._device, "windDirection", None) - if wind_direction: + if wind_direction is not None: state_attr[ATTR_WIND_DIRECTION] = _get_wind_direction(wind_direction) wind_direction_variation = getattr(self._device, "windDirectionVariation", None) @@ -328,9 +333,9 @@ class HomematicipWindspeedSensor(HomematicipGenericDevice): class HomematicipTodayRainSensor(HomematicipGenericDevice): """Representation of a HomematicIP rain counter of a day sensor.""" - def __init__(self, home: AsyncHome, device) -> None: + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the device.""" - super().__init__(home, device, "Today Rain") + super().__init__(hap, device, "Today Rain") @property def state(self) -> float: @@ -346,10 +351,6 @@ class HomematicipTodayRainSensor(HomematicipGenericDevice): class HomematicipPassageDetectorDeltaCounter(HomematicipGenericDevice): """Representation of a HomematicIP passage detector delta counter.""" - def __init__(self, home: AsyncHome, device) -> None: - """Initialize the device.""" - super().__init__(home, device) - @property def state(self) -> int: """Representation of the HomematicIP passage detector delta counter value.""" diff --git a/homeassistant/components/homematicip_cloud/services.yaml b/homeassistant/components/homematicip_cloud/services.yaml index cf93b3065ee..f426c9b5d22 100644 --- a/homeassistant/components/homematicip_cloud/services.yaml +++ b/homeassistant/components/homematicip_cloud/services.yaml @@ -7,7 +7,7 @@ activate_eco_mode_with_duration: description: The duration of eco mode in minutes. example: 60 accesspoint_id: - description: The ID of the Homematic IP Access Point + description: The ID of the Homematic IP Access Point (optional) example: 3014xxxxxxxxxxxxxxxxxxxx activate_eco_mode_with_period: @@ -17,7 +17,7 @@ activate_eco_mode_with_period: description: The time when the eco mode should automatically be disabled. example: 2019-02-17 14:00 accesspoint_id: - description: The ID of the Homematic IP Access Point + description: The ID of the Homematic IP Access Point (optional) example: 3014xxxxxxxxxxxxxxxxxxxx activate_vacation: @@ -30,20 +30,31 @@ activate_vacation: description: the set temperature during the vacation mode. example: 18.5 accesspoint_id: - description: The ID of the Homematic IP Access Point + description: The ID of the Homematic IP Access Point (optional) example: 3014xxxxxxxxxxxxxxxxxxxx deactivate_eco_mode: description: Deactivates the eco mode immediately. fields: accesspoint_id: - description: The ID of the Homematic IP Access Point + description: The ID of the Homematic IP Access Point (optional) example: 3014xxxxxxxxxxxxxxxxxxxx deactivate_vacation: description: Deactivates the vacation mode immediately. fields: accesspoint_id: - description: The ID of the Homematic IP Access Point + description: The ID of the Homematic IP Access Point (optional) example: 3014xxxxxxxxxxxxxxxxxxxx +set_active_climate_profile: + description: Set the active climate profile index. + fields: + entity_id: + description: The ID of the climte entity. Use 'all' keyword to switch the profile for all entities. + example: climate.livingroom + climate_profile_index: + description: The index of the climate profile (1 based) + example: 1 + + diff --git a/homeassistant/components/homematicip_cloud/switch.py b/homeassistant/components/homematicip_cloud/switch.py index ababf793f0c..dae6019b378 100644 --- a/homeassistant/components/homematicip_cloud/switch.py +++ b/homeassistant/components/homematicip_cloud/switch.py @@ -12,14 +12,14 @@ from homematicip.aio.device import ( AsyncPrintedCircuitBoardSwitchBattery, ) from homematicip.aio.group import AsyncSwitchingGroup -from homematicip.aio.home import AsyncHome from homeassistant.components.switch import SwitchDevice from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import HomeAssistantType from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice -from .device import ATTR_GROUP_MEMBER_UNREACHABLE, ATTR_IS_GROUP +from .device import ATTR_GROUP_MEMBER_UNREACHABLE +from .hap import HomematicipHAP _LOGGER = logging.getLogger(__name__) @@ -30,12 +30,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities + hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP switch from a config entry.""" - home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home + hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] devices = [] - for device in home.devices: + for device in hap.home.devices: if isinstance(device, AsyncBrandSwitchMeasuring): # BrandSwitchMeasuring inherits PlugableSwitchMeasuring # This device is implemented in the light platform and will @@ -44,24 +44,24 @@ async def async_setup_entry( elif isinstance( device, (AsyncPlugableSwitchMeasuring, AsyncFullFlushSwitchMeasuring) ): - devices.append(HomematicipSwitchMeasuring(home, device)) + devices.append(HomematicipSwitchMeasuring(hap, device)) elif isinstance( device, (AsyncPlugableSwitch, AsyncPrintedCircuitBoardSwitchBattery) ): - devices.append(HomematicipSwitch(home, device)) + devices.append(HomematicipSwitch(hap, device)) elif isinstance(device, AsyncOpenCollector8Module): for channel in range(1, 9): - devices.append(HomematicipMultiSwitch(home, device, channel)) + devices.append(HomematicipMultiSwitch(hap, device, channel)) elif isinstance(device, AsyncMultiIOBox): for channel in range(1, 3): - devices.append(HomematicipMultiSwitch(home, device, channel)) + devices.append(HomematicipMultiSwitch(hap, device, channel)) elif isinstance(device, AsyncPrintedCircuitBoardSwitch2): for channel in range(1, 3): - devices.append(HomematicipMultiSwitch(home, device, channel)) + devices.append(HomematicipMultiSwitch(hap, device, channel)) - for group in home.groups: + for group in hap.home.groups: if isinstance(group, AsyncSwitchingGroup): - devices.append(HomematicipGroupSwitch(home, group)) + devices.append(HomematicipGroupSwitch(hap, group)) if devices: async_add_entities(devices) @@ -70,9 +70,9 @@ async def async_setup_entry( class HomematicipSwitch(HomematicipGenericDevice, SwitchDevice): """representation of a HomematicIP Cloud switch device.""" - def __init__(self, home: AsyncHome, device) -> None: + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the switch device.""" - super().__init__(home, device) + super().__init__(hap, device) @property def is_on(self) -> bool: @@ -91,10 +91,10 @@ class HomematicipSwitch(HomematicipGenericDevice, SwitchDevice): class HomematicipGroupSwitch(HomematicipGenericDevice, SwitchDevice): """representation of a HomematicIP switching group.""" - def __init__(self, home: AsyncHome, device, post: str = "Group") -> None: + def __init__(self, hap: HomematicipHAP, device, post: str = "Group") -> None: """Initialize switching group.""" device.modelType = f"HmIP-{post}" - super().__init__(home, device, post) + super().__init__(hap, device, post) @property def is_on(self) -> bool: @@ -113,9 +113,11 @@ class HomematicipGroupSwitch(HomematicipGenericDevice, SwitchDevice): @property def device_state_attributes(self): """Return the state attributes of the switch-group.""" - state_attr = {ATTR_IS_GROUP: True} + state_attr = super().device_state_attributes + if self._device.unreach: state_attr[ATTR_GROUP_MEMBER_UNREACHABLE] = True + return state_attr async def async_turn_on(self, **kwargs): @@ -146,10 +148,10 @@ class HomematicipSwitchMeasuring(HomematicipSwitch): class HomematicipMultiSwitch(HomematicipGenericDevice, SwitchDevice): """Representation of a HomematicIP Cloud multi switch device.""" - def __init__(self, home: AsyncHome, device, channel: int): + def __init__(self, hap: HomematicipHAP, device, channel: int): """Initialize the multi switch device.""" self.channel = channel - super().__init__(home, device, f"Channel{channel}") + super().__init__(hap, device, f"Channel{channel}") @property def unique_id(self) -> str: diff --git a/homeassistant/components/homematicip_cloud/weather.py b/homeassistant/components/homematicip_cloud/weather.py index ed9098559a3..5aa3f28c45d 100644 --- a/homeassistant/components/homematicip_cloud/weather.py +++ b/homeassistant/components/homematicip_cloud/weather.py @@ -6,15 +6,15 @@ from homematicip.aio.device import ( AsyncWeatherSensorPlus, AsyncWeatherSensorPro, ) -from homematicip.aio.home import AsyncHome from homematicip.base.enums import WeatherCondition from homeassistant.components.weather import WeatherEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import TEMP_CELSIUS -from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import HomeAssistantType from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice +from .hap import HomematicipHAP _LOGGER = logging.getLogger(__name__) @@ -43,18 +43,18 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities + hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP weather sensor from a config entry.""" - home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home + hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] devices = [] - for device in home.devices: + for device in hap.home.devices: if isinstance(device, AsyncWeatherSensorPro): - devices.append(HomematicipWeatherSensorPro(home, device)) + devices.append(HomematicipWeatherSensorPro(hap, device)) elif isinstance(device, (AsyncWeatherSensor, AsyncWeatherSensorPlus)): - devices.append(HomematicipWeatherSensor(home, device)) + devices.append(HomematicipWeatherSensor(hap, device)) - devices.append(HomematicipHomeWeather(home)) + devices.append(HomematicipHomeWeather(hap)) if devices: async_add_entities(devices) @@ -63,9 +63,9 @@ async def async_setup_entry( class HomematicipWeatherSensor(HomematicipGenericDevice, WeatherEntity): """representation of a HomematicIP Cloud weather sensor plus & basic.""" - def __init__(self, home: AsyncHome, device) -> None: + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the weather sensor.""" - super().__init__(home, device) + super().__init__(hap, device) @property def name(self) -> str: @@ -121,10 +121,10 @@ class HomematicipWeatherSensorPro(HomematicipWeatherSensor): class HomematicipHomeWeather(HomematicipGenericDevice, WeatherEntity): """representation of a HomematicIP Cloud home weather.""" - def __init__(self, home: AsyncHome) -> None: + def __init__(self, hap: HomematicipHAP) -> None: """Initialize the home weather.""" - home.weather.modelType = "HmIP-Home-Weather" - super().__init__(home, home) + hap.home.modelType = "HmIP-Home-Weather" + super().__init__(hap, hap.home) @property def available(self) -> bool: diff --git a/homeassistant/components/hp_ilo/sensor.py b/homeassistant/components/hp_ilo/sensor.py index cf95c21a8d1..04c715dc010 100644 --- a/homeassistant/components/hp_ilo/sensor.py +++ b/homeassistant/components/hp_ilo/sensor.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +import hpilo import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -180,8 +181,6 @@ class HpIloData: @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data from HP iLO.""" - import hpilo - try: self.data = hpilo.Ilo( hostname=self._host, diff --git a/homeassistant/components/html5/notify.py b/homeassistant/components/html5/notify.py index ac76911b9f6..18b7ff27ab4 100644 --- a/homeassistant/components/html5/notify.py +++ b/homeassistant/components/html5/notify.py @@ -9,6 +9,9 @@ import time import uuid from aiohttp.hdrs import AUTHORIZATION +import jwt +from pywebpush import WebPusher +from py_vapid import Vapid import voluptuous as vol from voluptuous.humanize import humanize_error @@ -56,7 +59,7 @@ def gcm_api_deprecated(value): "Configuring html5_push_notifications via the GCM api" " has been deprecated and will stop working after April 11," " 2019. Use the VAPID configuration instead. For instructions," - " see https://www.home-assistant.io/components/notify.html5/" + " see https://www.home-assistant.io/integrations/html5/" ) return value @@ -311,7 +314,6 @@ class HTML5PushCallbackView(HomeAssistantView): def decode_jwt(self, token): """Find the registration that signed this JWT and return it.""" - import jwt # 1. Check claims w/o verifying to see if a target is in there. # 2. If target in claims, attempt to verify against the given name. @@ -335,7 +337,6 @@ class HTML5PushCallbackView(HomeAssistantView): # https://auth0.com/docs/quickstart/backend/python def check_authorization_header(self, request): """Check the authorization header.""" - import jwt auth = request.headers.get(AUTHORIZATION, None) if not auth: @@ -491,7 +492,6 @@ class HTML5NotificationService(BaseNotificationService): def _push_message(self, payload, **kwargs): """Send the message.""" - from pywebpush import WebPusher timestamp = int(time.time()) ttl = int(kwargs.get(ATTR_TTL, DEFAULT_TTL)) @@ -550,7 +550,6 @@ class HTML5NotificationService(BaseNotificationService): def add_jwt(timestamp, target, tag, jwt_secret): """Create JWT json to put into payload.""" - import jwt jwt_exp = datetime.fromtimestamp(timestamp) + timedelta(days=JWT_VALID_DAYS) jwt_claims = { @@ -565,7 +564,6 @@ def add_jwt(timestamp, target, tag, jwt_secret): def create_vapid_headers(vapid_email, subscription_info, vapid_private_key): """Create encrypted headers to send to WebPusher.""" - from py_vapid import Vapid if vapid_email and vapid_private_key and ATTR_ENDPOINT in subscription_info: url = urlparse(subscription_info.get(ATTR_ENDPOINT)) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index a8aaa3390a7..4df606a3c1b 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -17,7 +17,6 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv import homeassistant.util as hass_util from homeassistant.util import ssl as ssl_util -from homeassistant.util.logging import HideSensitiveDataFilter from .auth import setup_auth from .ban import setup_bans @@ -32,7 +31,6 @@ from .view import HomeAssistantView # noqa DOMAIN = "http" -CONF_API_PASSWORD = "api_password" CONF_SERVER_HOST = "server_host" CONF_SERVER_PORT = "server_port" CONF_BASE_URL = "base_url" @@ -42,7 +40,6 @@ CONF_SSL_KEY = "ssl_key" CONF_CORS_ORIGINS = "cors_allowed_origins" CONF_USE_X_FORWARDED_FOR = "use_x_forwarded_for" CONF_TRUSTED_PROXIES = "trusted_proxies" -CONF_TRUSTED_NETWORKS = "trusted_networks" CONF_LOGIN_ATTEMPTS_THRESHOLD = "login_attempts_threshold" CONF_IP_BAN_ENABLED = "ip_ban_enabled" CONF_SSL_PROFILE = "ssl_profile" @@ -59,37 +56,8 @@ DEFAULT_CORS = "https://cast.home-assistant.io" NO_LOGIN_ATTEMPT_THRESHOLD = -1 -def trusted_networks_deprecated(value): - """Warn user trusted_networks config is deprecated.""" - if not value: - return value - - _LOGGER.warning( - "Configuring trusted_networks via the http integration has been" - " deprecated. Use the trusted networks auth provider instead." - " For instructions, see https://www.home-assistant.io/docs/" - "authentication/providers/#trusted-networks" - ) - return value - - -def api_password_deprecated(value): - """Warn user api_password config is deprecated.""" - if not value: - return value - - _LOGGER.warning( - "Configuring api_password via the http integration has been" - " deprecated. Use the legacy api password auth provider instead." - " For instructions, see https://www.home-assistant.io/docs/" - "authentication/providers/#legacy-api-password" - ) - return value - - HTTP_SCHEMA = vol.Schema( { - vol.Optional(CONF_API_PASSWORD): vol.All(cv.string, api_password_deprecated), vol.Optional(CONF_SERVER_HOST, default=DEFAULT_SERVER_HOST): cv.string, vol.Optional(CONF_SERVER_PORT, default=SERVER_PORT): cv.port, vol.Optional(CONF_BASE_URL): cv.string, @@ -103,9 +71,6 @@ HTTP_SCHEMA = vol.Schema( vol.Inclusive(CONF_TRUSTED_PROXIES, "proxy"): vol.All( cv.ensure_list, [ip_network] ), - vol.Optional(CONF_TRUSTED_NETWORKS, default=[]): vol.All( - cv.ensure_list, [ip_network], trusted_networks_deprecated - ), vol.Optional( CONF_LOGIN_ATTEMPTS_THRESHOLD, default=NO_LOGIN_ATTEMPT_THRESHOLD ): vol.Any(cv.positive_int, NO_LOGIN_ATTEMPT_THRESHOLD), @@ -128,6 +93,7 @@ class ApiConfig: """Initialize a new API config object.""" self.host = host self.port = port + self.use_ssl = use_ssl host = host.rstrip("/") if host.startswith(("http://", "https://")): @@ -148,7 +114,6 @@ async def async_setup(hass, config): if conf is None: conf = HTTP_SCHEMA({}) - api_password = conf.get(CONF_API_PASSWORD) server_host = conf[CONF_SERVER_HOST] server_port = conf[CONF_SERVER_PORT] ssl_certificate = conf.get(CONF_SSL_CERTIFICATE) @@ -161,11 +126,6 @@ async def async_setup(hass, config): login_threshold = conf[CONF_LOGIN_ATTEMPTS_THRESHOLD] ssl_profile = conf[CONF_SSL_PROFILE] - if api_password is not None: - logging.getLogger("aiohttp.access").addFilter( - HideSensitiveDataFilter(api_password) - ) - server = HomeAssistantHTTP( hass, server_host=server_host, diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index 4ff581aef02..97bd9b7d4bc 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -1,14 +1,11 @@ """Authentication for HTTP component.""" -import base64 import logging from aiohttp import hdrs from aiohttp.web import middleware import jwt -from homeassistant.auth.providers import legacy_api_password from homeassistant.auth.util import generate_secret -from homeassistant.const import HTTP_HEADER_HA_AUTH from homeassistant.core import callback from homeassistant.util import dt as dt_util @@ -52,16 +49,6 @@ def async_sign_path(hass, refresh_token_id, path, expiration): @callback def setup_auth(hass, app): """Create auth middleware for the app.""" - old_auth_warning = set() - - support_legacy = hass.auth.support_legacy - if support_legacy: - _LOGGER.warning("legacy_api_password support has been enabled.") - - trusted_networks = [] - for prv in hass.auth.auth_providers: - if prv.type == "trusted_networks": - trusted_networks += prv.trusted_networks async def async_validate_auth_header(request): """ @@ -75,40 +62,16 @@ def setup_auth(hass, app): # If no space in authorization header return False - if auth_type == "Bearer": - refresh_token = await hass.auth.async_validate_access_token(auth_val) - if refresh_token is None: - return False + if auth_type != "Bearer": + return False - request[KEY_HASS_USER] = refresh_token.user - return True + refresh_token = await hass.auth.async_validate_access_token(auth_val) - if auth_type == "Basic" and support_legacy: - decoded = base64.b64decode(auth_val).decode("utf-8") - try: - username, password = decoded.split(":", 1) - except ValueError: - # If no ':' in decoded - return False + if refresh_token is None: + return False - if username != "homeassistant": - return False - - user = await legacy_api_password.async_validate_password(hass, password) - if user is None: - return False - - request[KEY_HASS_USER] = user - _LOGGER.info( - "Basic auth with api_password is going to deprecate," - " please use a bearer token to access %s from %s", - request.path, - request[KEY_REAL_IP], - ) - old_auth_warning.add(request.path) - return True - - return False + request[KEY_HASS_USER] = refresh_token.user + return True async def async_validate_signed_request(request): """Validate a signed request.""" @@ -140,50 +103,16 @@ def setup_auth(hass, app): request[KEY_HASS_USER] = refresh_token.user return True - async def async_validate_trusted_networks(request): - """Test if request is from a trusted ip.""" - ip_addr = request[KEY_REAL_IP] - - if not any(ip_addr in trusted_network for trusted_network in trusted_networks): - return False - - user = await hass.auth.async_get_owner() - if user is None: - return False - - request[KEY_HASS_USER] = user - return True - - async def async_validate_legacy_api_password(request, password): - """Validate api_password.""" - user = await legacy_api_password.async_validate_password(hass, password) - if user is None: - return False - - request[KEY_HASS_USER] = user - return True - @middleware async def auth_middleware(request, handler): """Authenticate as middleware.""" authenticated = False - if HTTP_HEADER_HA_AUTH in request.headers or DATA_API_PASSWORD in request.query: - if request.path not in old_auth_warning: - _LOGGER.log( - logging.INFO if support_legacy else logging.WARNING, - "api_password is going to deprecate. You need to use a" - " bearer token to access %s from %s", - request.path, - request[KEY_REAL_IP], - ) - old_auth_warning.add(request.path) - if hdrs.AUTHORIZATION in request.headers and await async_validate_auth_header( request ): - # it included both use_auth and api_password Basic auth authenticated = True + auth_type = "bearer token" # We first start with a string check to avoid parsing query params # for every request. @@ -193,39 +122,15 @@ def setup_auth(hass, app): and await async_validate_signed_request(request) ): authenticated = True + auth_type = "signed request" - elif trusted_networks and await async_validate_trusted_networks(request): - if request.path not in old_auth_warning: - # When removing this, don't forget to remove the print logic - # in http/view.py - request["deprecate_warning_message"] = ( - "Access from trusted networks without auth token is " - "going to be removed in Home Assistant 0.96. Configure " - "the trusted networks auth provider or use long-lived " - "access tokens to access {} from {}".format( - request.path, request[KEY_REAL_IP] - ) - ) - old_auth_warning.add(request.path) - authenticated = True - - elif ( - support_legacy - and HTTP_HEADER_HA_AUTH in request.headers - and await async_validate_legacy_api_password( - request, request.headers[HTTP_HEADER_HA_AUTH] + if authenticated: + _LOGGER.debug( + "Authenticated %s for %s using %s", + request[KEY_REAL_IP], + request.path, + auth_type, ) - ): - authenticated = True - - elif ( - support_legacy - and DATA_API_PASSWORD in request.query - and await async_validate_legacy_api_password( - request, request.query[DATA_API_PASSWORD] - ) - ): - authenticated = True request[KEY_AUTHENTICATED] = authenticated return await handler(request) diff --git a/homeassistant/components/http/cors.py b/homeassistant/components/http/cors.py index 39ff45fd4e4..de4547f4782 100644 --- a/homeassistant/components/http/cors.py +++ b/homeassistant/components/http/cors.py @@ -2,7 +2,7 @@ from aiohttp.web_urldispatcher import Resource, ResourceRoute, StaticResource from aiohttp.hdrs import ACCEPT, CONTENT_TYPE, ORIGIN, AUTHORIZATION -from homeassistant.const import HTTP_HEADER_HA_AUTH, HTTP_HEADER_X_REQUESTED_WITH +from homeassistant.const import HTTP_HEADER_X_REQUESTED_WITH from homeassistant.core import callback @@ -13,7 +13,6 @@ ALLOWED_CORS_HEADERS = [ ACCEPT, HTTP_HEADER_X_REQUESTED_WITH, CONTENT_TYPE, - HTTP_HEADER_HA_AUTH, AUTHORIZATION, ] VALID_CORS_TYPES = (Resource, ResourceRoute, StaticResource) @@ -22,6 +21,8 @@ VALID_CORS_TYPES = (Resource, ResourceRoute, StaticResource) @callback 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. import aiohttp_cors cors = aiohttp_cors.setup( diff --git a/homeassistant/components/http/view.py b/homeassistant/components/http/view.py index 66864eba55e..804c90d4f96 100644 --- a/homeassistant/components/http/view.py +++ b/homeassistant/components/http/view.py @@ -17,7 +17,6 @@ from homeassistant.const import CONTENT_TYPE_JSON from homeassistant.core import Context, is_callback from homeassistant.helpers.json import JSONEncoder -from .ban import process_success_login from .const import KEY_AUTHENTICATED, KEY_HASS, KEY_REAL_IP _LOGGER = logging.getLogger(__name__) @@ -106,13 +105,8 @@ def request_handler_factory(view, handler): authenticated = request.get(KEY_AUTHENTICATED, False) - if view.requires_auth: - if authenticated: - if "deprecate_warning_message" in request: - _LOGGER.warning(request["deprecate_warning_message"]) - await process_success_login(request) - else: - raise HTTPUnauthorized() + if view.requires_auth and not authenticated: + raise HTTPUnauthorized() _LOGGER.debug( "Serving %s to %s (auth: %s)", diff --git a/homeassistant/components/htu21d/sensor.py b/homeassistant/components/htu21d/sensor.py index f94b11d5ada..954ba60abbf 100644 --- a/homeassistant/components/htu21d/sensor.py +++ b/homeassistant/components/htu21d/sensor.py @@ -3,11 +3,13 @@ from datetime import timedelta from functools import partial import logging +from i2csense.htu21d import HTU21D # 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, TEMP_FAHRENHEIT +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle from homeassistant.util.temperature import celsius_to_fahrenheit @@ -34,9 +36,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the HTU21D sensor.""" - import smbus # pylint: disable=import-error - from i2csense.htu21d import HTU21D # pylint: disable=import-error - name = config.get(CONF_NAME) bus_number = config.get(CONF_I2C_BUS) temp_unit = hass.config.units.temperature_unit diff --git a/homeassistant/components/hue/.translations/pl.json b/homeassistant/components/hue/.translations/pl.json index 9062e427a27..33b1ffbfe86 100644 --- a/homeassistant/components/hue/.translations/pl.json +++ b/homeassistant/components/hue/.translations/pl.json @@ -12,7 +12,7 @@ }, "error": { "linking": "Wyst\u0105pi\u0142 nieznany b\u0142\u0105d w trakcie \u0142\u0105czenia.", - "register_failed": "Nie uda\u0142o si\u0119 zarejestrowa\u0107. Prosz\u0119 spr\u00f3bowa\u0107 ponownie." + "register_failed": "Nie uda\u0142o si\u0119 zarejestrowa\u0107. Spr\u00f3buj ponownie." }, "step": { "init": { diff --git a/homeassistant/components/hue/.translations/ru.json b/homeassistant/components/hue/.translations/ru.json index 79a46e1861b..c749a498e44 100644 --- a/homeassistant/components/hue/.translations/ru.json +++ b/homeassistant/components/hue/.translations/ru.json @@ -2,17 +2,17 @@ "config": { "abort": { "all_configured": "\u0412\u0441\u0435 Philips Hue \u0448\u043b\u044e\u0437\u044b \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b.", - "already_configured": "\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.", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\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 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u043d\u0430\u0447\u0430\u0442\u0430.", - "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0448\u043b\u044e\u0437\u0443", - "discover_timeout": "\u0428\u043b\u044e\u0437 Philips Hue \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d", - "no_bridges": "\u0428\u043b\u044e\u0437\u044b Philips Hue \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b", - "not_hue_bridge": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0448\u043b\u044e\u0437\u043e\u043c Hue", - "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430" + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0448\u043b\u044e\u0437\u0443.", + "discover_timeout": "\u0428\u043b\u044e\u0437 Philips Hue \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d.", + "no_bridges": "\u0428\u043b\u044e\u0437\u044b Philips Hue \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b.", + "not_hue_bridge": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0448\u043b\u044e\u0437\u043e\u043c Hue.", + "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "error": { - "linking": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f", - "register_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430" + "linking": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f.", + "register_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430." }, "step": { "init": { @@ -23,7 +23,7 @@ }, "link": { "description": "\u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u043d\u0430 \u0448\u043b\u044e\u0437\u0435 \u0434\u043b\u044f \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 Philips Hue \u0432 Home Assistant.\n\n![\u0420\u0430\u0441\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u043a\u043d\u043e\u043f\u043a\u0438 \u043d\u0430 \u0448\u043b\u044e\u0437\u0435](/static/images/config_philips_hue.jpg)", - "title": "\u0421\u0432\u044f\u0437\u044c \u0441 \u0445\u0430\u0431\u043e\u043c" + "title": "Philips Hue" } }, "title": "Philips Hue" diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index 064e18e7a81..027ec205195 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -8,11 +8,11 @@ from homeassistant import config_entries from homeassistant.const import CONF_FILENAME, CONF_HOST from homeassistant.helpers import config_validation as cv, device_registry as dr -from .const import DOMAIN from .bridge import HueBridge - -# Loading the config flow file will register the flow -from .config_flow import configured_hosts +from .config_flow import ( + configured_hosts, +) # Loading the config flow file will register the flow +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/hue/binary_sensor.py b/homeassistant/components/hue/binary_sensor.py index 9c84cb5d61c..e4b7dd85e37 100644 --- a/homeassistant/components/hue/binary_sensor.py +++ b/homeassistant/components/hue/binary_sensor.py @@ -1,19 +1,31 @@ """Hue binary sensor entities.""" + +from aiohue.sensors import TYPE_ZLL_PRESENCE + from homeassistant.components.binary_sensor import ( - BinarySensorDevice, DEVICE_CLASS_MOTION, + BinarySensorDevice, ) from homeassistant.components.hue.sensor_base import ( GenericZLLSensor, + SensorManager, async_setup_entry as shared_async_setup_entry, ) - PRESENCE_NAME_FORMAT = "{} motion" async def async_setup_entry(hass, config_entry, async_add_entities): """Defer binary sensor setup to the shared sensor module.""" + SensorManager.sensor_config_map.update( + { + TYPE_ZLL_PRESENCE: { + "binary": True, + "name_format": PRESENCE_NAME_FORMAT, + "class": HuePresence, + } + } + ) await shared_async_setup_entry(hass, config_entry, async_add_entities, binary=True) diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index 9c0e94bc3bd..ebd71ba7c1c 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -50,6 +50,8 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + def __init__(self): """Initialize the Hue flow.""" self.host = None diff --git a/homeassistant/components/hue/helpers.py b/homeassistant/components/hue/helpers.py new file mode 100644 index 00000000000..971509ab647 --- /dev/null +++ b/homeassistant/components/hue/helpers.py @@ -0,0 +1,33 @@ +"""Helper functions for Philips Hue.""" +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 .const import DOMAIN + + +async def remove_devices(hass, config_entry, api_ids, current): + """Get items that are removed from api.""" + removed_items = [] + + for item_id in current: + if item_id in api_ids: + continue + + # Device is removed from Hue, so we remove it from Home Assistant + entity = current[item_id] + removed_items.append(item_id) + await entity.async_remove() + ent_registry = await get_ent_reg(hass) + if entity.entity_id in ent_registry.entities: + ent_registry.async_remove(entity.entity_id) + dev_registry = await get_dev_reg(hass) + device = dev_registry.async_get_device( + identifiers={(DOMAIN, entity.device_id)}, connections=set() + ) + if device is not None: + dev_registry.async_update_device( + device.id, remove_config_entry_id=config_entry.entry_id + ) + + for item_id in removed_items: + del current[item_id] diff --git a/homeassistant/components/hue/light.py b/homeassistant/components/hue/light.py index 5a3379f71ce..041eb76c1d3 100644 --- a/homeassistant/components/hue/light.py +++ b/homeassistant/components/hue/light.py @@ -2,37 +2,36 @@ import asyncio from datetime import timedelta import logging -from time import monotonic import random +from time import monotonic import aiohue import async_timeout -from homeassistant.helpers.entity_registry import async_get_registry as get_ent_reg -from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg - from homeassistant.components import hue from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, - ATTR_TRANSITION, ATTR_HS_COLOR, + ATTR_TRANSITION, EFFECT_COLORLOOP, EFFECT_RANDOM, FLASH_LONG, FLASH_SHORT, SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, - SUPPORT_COLOR, SUPPORT_TRANSITION, Light, ) from homeassistant.util import color +from .helpers import remove_devices + SCAN_INTERVAL = timedelta(seconds=5) _LOGGER = logging.getLogger(__name__) @@ -226,7 +225,6 @@ async def async_update_items( bridge.available = True new_items = [] - removed_items = [] for item_id in api: if item_id not in current: @@ -238,31 +236,11 @@ async def async_update_items( elif item_id not in progress_waiting: current[item_id].async_schedule_update_ha_state() - for item_id in current: - if item_id in api: - continue - - # Device is removed from Hue, so we remove it from Home Assistant - entity = current[item_id] - removed_items.append(item_id) - await entity.async_remove() - ent_registry = await get_ent_reg(hass) - if entity.entity_id in ent_registry.entities: - ent_registry.async_remove(entity.entity_id) - dev_registry = await get_dev_reg(hass) - device = dev_registry.async_get_device( - identifiers={(hue.DOMAIN, entity.unique_id)}, connections=set() - ) - dev_registry.async_update_device( - device.id, remove_config_entry_id=config_entry.entry_id - ) + await remove_devices(hass, config_entry, api, current) if new_items: async_add_entities(new_items) - for item_id in removed_items: - del current[item_id] - class HueLight(Light): """Representation of a Hue light.""" @@ -300,9 +278,14 @@ class HueLight(Light): @property def unique_id(self): - """Return the ID of this Hue light.""" + """Return the unique ID of this Hue light.""" return self.light.uniqueid + @property + def device_id(self): + """Return the ID of this Hue light.""" + return self.unique_id + @property def name(self): """Return the name of the Hue light.""" @@ -384,7 +367,7 @@ class HueLight(Light): return None return { - "identifiers": {(hue.DOMAIN, self.unique_id)}, + "identifiers": {(hue.DOMAIN, self.device_id)}, "name": self.name, "manufacturer": self.light.manufacturername, # productname added in Hue Bridge API 1.24 diff --git a/homeassistant/components/hue/sensor.py b/homeassistant/components/hue/sensor.py index 457ed761202..f2e02d49ecf 100644 --- a/homeassistant/components/hue/sensor.py +++ b/homeassistant/components/hue/sensor.py @@ -1,15 +1,17 @@ """Hue sensor entities.""" +from aiohue.sensors import TYPE_ZLL_LIGHTLEVEL, TYPE_ZLL_TEMPERATURE + +from homeassistant.components.hue.sensor_base import ( + GenericZLLSensor, + SensorManager, + async_setup_entry as shared_async_setup_entry, +) from homeassistant.const import ( DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, ) from homeassistant.helpers.entity import Entity -from homeassistant.components.hue.sensor_base import ( - GenericZLLSensor, - async_setup_entry as shared_async_setup_entry, -) - LIGHT_LEVEL_NAME_FORMAT = "{} light level" TEMPERATURE_NAME_FORMAT = "{} temperature" @@ -17,6 +19,20 @@ TEMPERATURE_NAME_FORMAT = "{} temperature" async def async_setup_entry(hass, config_entry, async_add_entities): """Defer sensor setup to the shared sensor module.""" + SensorManager.sensor_config_map.update( + { + TYPE_ZLL_LIGHTLEVEL: { + "binary": False, + "name_format": LIGHT_LEVEL_NAME_FORMAT, + "class": HueLightLevel, + }, + TYPE_ZLL_TEMPERATURE: { + "binary": False, + "name_format": TEMPERATURE_NAME_FORMAT, + "class": HueTemperature, + }, + } + ) await shared_async_setup_entry(hass, config_entry, async_add_entities, binary=False) diff --git a/homeassistant/components/hue/sensor_base.py b/homeassistant/components/hue/sensor_base.py index 96b9b8bf5d6..7236dfbd886 100644 --- a/homeassistant/components/hue/sensor_base.py +++ b/homeassistant/components/hue/sensor_base.py @@ -4,6 +4,8 @@ from datetime import timedelta import logging from time import monotonic +from aiohue import AiohueException +from aiohue.sensors import TYPE_ZLL_PRESENCE import async_timeout from homeassistant.components import hue @@ -11,6 +13,7 @@ from homeassistant.exceptions import NoEntitySpecifiedError from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util.dt import utcnow +from .helpers import remove_devices CURRENT_SENSORS = "current_sensors" SENSOR_MANAGER_FORMAT = "{}_sensor_manager" @@ -34,7 +37,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities, binary=False sm_key = SENSOR_MANAGER_FORMAT.format(config_entry.data["host"]) manager = hass.data[hue.DOMAIN].get(sm_key) if manager is None: - manager = SensorManager(hass, bridge) + manager = SensorManager(hass, bridge, config_entry) hass.data[hue.DOMAIN][sm_key] = manager manager.register_component(binary, async_add_entities) @@ -50,42 +53,14 @@ class SensorManager: SCAN_INTERVAL = timedelta(seconds=5) sensor_config_map = {} - def __init__(self, hass, bridge): + def __init__(self, hass, bridge, config_entry): """Initialize the sensor manager.""" - import aiohue - from .binary_sensor import HuePresence, PRESENCE_NAME_FORMAT - from .sensor import ( - HueLightLevel, - HueTemperature, - LIGHT_LEVEL_NAME_FORMAT, - TEMPERATURE_NAME_FORMAT, - ) - self.hass = hass self.bridge = bridge + self.config_entry = config_entry self._component_add_entities = {} self._started = False - self.sensor_config_map.update( - { - aiohue.sensors.TYPE_ZLL_LIGHTLEVEL: { - "binary": False, - "name_format": LIGHT_LEVEL_NAME_FORMAT, - "class": HueLightLevel, - }, - aiohue.sensors.TYPE_ZLL_TEMPERATURE: { - "binary": False, - "name_format": TEMPERATURE_NAME_FORMAT, - "class": HueTemperature, - }, - aiohue.sensors.TYPE_ZLL_PRESENCE: { - "binary": True, - "name_format": PRESENCE_NAME_FORMAT, - "class": HuePresence, - }, - } - ) - def register_component(self, binary, async_add_entities): """Register async_add_entities methods for components.""" self._component_add_entities[binary] = async_add_entities @@ -115,15 +90,13 @@ class SensorManager: async def async_update_items(self): """Update sensors from the bridge.""" - import aiohue - api = self.bridge.api.sensors try: start = monotonic() with async_timeout.timeout(4): await api.update() - except (asyncio.TimeoutError, aiohue.AiohueException) as err: + except (asyncio.TimeoutError, AiohueException) as err: _LOGGER.debug("Failed to fetch sensor: %s", err) if not self.bridge.available: @@ -162,7 +135,7 @@ class SensorManager: # finding the remaining ones that may or may not be related to the # presence sensors. for item_id in api: - if api[item_id].type != aiohue.sensors.TYPE_ZLL_PRESENCE: + if api[item_id].type != TYPE_ZLL_PRESENCE: continue primary_sensor_devices[_device_id(api[item_id])] = api[item_id] @@ -194,6 +167,13 @@ class SensorManager: else: new_sensors.append(current[api[item_id].uniqueid]) + await remove_devices( + self.hass, + self.config_entry, + [value.uniqueid for value in api.values()], + current, + ) + async_add_sensor_entities = self._component_add_entities.get(False) async_add_binary_entities = self._component_add_entities.get(True) if new_sensors and async_add_sensor_entities: diff --git a/homeassistant/components/hydroquebec/__init__.py b/homeassistant/components/hydroquebec/__init__.py deleted file mode 100644 index 08a12f7955e..00000000000 --- a/homeassistant/components/hydroquebec/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The hydroquebec component.""" diff --git a/homeassistant/components/hydroquebec/manifest.json b/homeassistant/components/hydroquebec/manifest.json deleted file mode 100644 index dbe8af0b41b..00000000000 --- a/homeassistant/components/hydroquebec/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "domain": "hydroquebec", - "name": "Hydroquebec", - "documentation": "https://www.home-assistant.io/integrations/hydroquebec", - "requirements": [ - "pyhydroquebec==2.2.2" - ], - "dependencies": [], - "codeowners": [] -} diff --git a/homeassistant/components/hydroquebec/sensor.py b/homeassistant/components/hydroquebec/sensor.py deleted file mode 100644 index c3ad79c1c98..00000000000 --- a/homeassistant/components/hydroquebec/sensor.py +++ /dev/null @@ -1,227 +0,0 @@ -""" -Support for HydroQuebec. - -Get data from 'My Consumption Profile' page: -https://www.hydroquebec.com/portail/en/group/clientele/portrait-de-consommation - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.hydroquebec/ -""" -import logging -from datetime import timedelta - -import voluptuous as vol - -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - CONF_USERNAME, - CONF_PASSWORD, - ENERGY_KILO_WATT_HOUR, - CONF_NAME, - CONF_MONITORED_VARIABLES, - TEMP_CELSIUS, -) -from homeassistant.helpers.entity import Entity -from homeassistant.util import Throttle -import homeassistant.helpers.config_validation as cv - -_LOGGER = logging.getLogger(__name__) - -KILOWATT_HOUR = ENERGY_KILO_WATT_HOUR -PRICE = "CAD" -DAYS = "days" -CONF_CONTRACT = "contract" - -DEFAULT_NAME = "HydroQuebec" - -REQUESTS_TIMEOUT = 15 -MIN_TIME_BETWEEN_UPDATES = timedelta(hours=1) -SCAN_INTERVAL = timedelta(hours=1) - -SENSOR_TYPES = { - "balance": ["Balance", PRICE, "mdi:square-inc-cash"], - "period_total_bill": ["Period total bill", PRICE, "mdi:square-inc-cash"], - "period_length": ["Period length", DAYS, "mdi:calendar-today"], - "period_total_days": ["Period total days", DAYS, "mdi:calendar-today"], - "period_mean_daily_bill": ["Period mean daily bill", PRICE, "mdi:square-inc-cash"], - "period_mean_daily_consumption": [ - "Period mean daily consumption", - KILOWATT_HOUR, - "mdi:flash", - ], - "period_total_consumption": [ - "Period total consumption", - KILOWATT_HOUR, - "mdi:flash", - ], - "period_lower_price_consumption": [ - "Period lower price consumption", - KILOWATT_HOUR, - "mdi:flash", - ], - "period_higher_price_consumption": [ - "Period higher price consumption", - KILOWATT_HOUR, - "mdi:flash", - ], - "yesterday_total_consumption": [ - "Yesterday total consumption", - KILOWATT_HOUR, - "mdi:flash", - ], - "yesterday_lower_price_consumption": [ - "Yesterday lower price consumption", - KILOWATT_HOUR, - "mdi:flash", - ], - "yesterday_higher_price_consumption": [ - "Yesterday higher price consumption", - KILOWATT_HOUR, - "mdi:flash", - ], - "yesterday_average_temperature": [ - "Yesterday average temperature", - TEMP_CELSIUS, - "mdi:thermometer", - ], - "period_average_temperature": [ - "Period average temperature", - TEMP_CELSIUS, - "mdi:thermometer", - ], -} - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_MONITORED_VARIABLES): vol.All( - cv.ensure_list, [vol.In(SENSOR_TYPES)] - ), - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_CONTRACT): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - } -) - -HOST = "https://www.hydroquebec.com" -HOME_URL = f"{HOST}/portail/web/clientele/authentification" -PROFILE_URL = "{}/portail/fr/group/clientele/" "portrait-de-consommation".format(HOST) -MONTHLY_MAP = ( - ("period_total_bill", "montantFacturePeriode"), - ("period_length", "nbJourLecturePeriode"), - ("period_total_days", "nbJourPrevuPeriode"), - ("period_mean_daily_bill", "moyenneDollarsJourPeriode"), - ("period_mean_daily_consumption", "moyenneKwhJourPeriode"), - ("period_total_consumption", "consoTotalPeriode"), - ("period_lower_price_consumption", "consoRegPeriode"), - ("period_higher_price_consumption", "consoHautPeriode"), -) -DAILY_MAP = ( - ("yesterday_total_consumption", "consoTotalQuot"), - ("yesterday_lower_price_consumption", "consoRegQuot"), - ("yesterday_higher_price_consumption", "consoHautQuot"), -) - - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the HydroQuebec sensor.""" - # Create a data fetcher to support all of the configured sensors. Then make - # the first call to init the data. - - username = config.get(CONF_USERNAME) - password = config.get(CONF_PASSWORD) - contract = config.get(CONF_CONTRACT) - - httpsession = hass.helpers.aiohttp_client.async_get_clientsession() - hydroquebec_data = HydroquebecData(username, password, httpsession, contract) - contracts = await hydroquebec_data.get_contract_list() - if not contracts: - return - _LOGGER.info("Contract list: %s", ", ".join(contracts)) - - name = config.get(CONF_NAME) - - sensors = [] - for variable in config[CONF_MONITORED_VARIABLES]: - sensors.append(HydroQuebecSensor(hydroquebec_data, variable, name)) - - async_add_entities(sensors, True) - - -class HydroQuebecSensor(Entity): - """Implementation of a HydroQuebec sensor.""" - - def __init__(self, hydroquebec_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.hydroquebec_data = hydroquebec_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 unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return self._unit_of_measurement - - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return self._icon - - async def async_update(self): - """Get the latest data from Hydroquebec and update the state.""" - await self.hydroquebec_data.async_update() - if self.hydroquebec_data.data.get(self.type) is not None: - self._state = round(self.hydroquebec_data.data[self.type], 2) - - -class HydroquebecData: - """Get data from HydroQuebec.""" - - def __init__(self, username, password, httpsession, contract=None): - """Initialize the data object.""" - from pyhydroquebec import HydroQuebecClient - - self.client = HydroQuebecClient( - username, password, REQUESTS_TIMEOUT, httpsession - ) - self._contract = contract - self.data = {} - - async def get_contract_list(self): - """Return the contract list.""" - # Fetch data - ret = await self._fetch_data() - if ret: - return self.client.get_contracts() - return [] - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - async def _fetch_data(self): - """Fetch latest data from HydroQuebec.""" - from pyhydroquebec.client import PyHydroQuebecError - - try: - await self.client.fetch_data() - except PyHydroQuebecError as exp: - _LOGGER.error("Error on receive last Hydroquebec data: %s", exp) - return False - return True - - async def async_update(self): - """Return the latest collected data from HydroQuebec.""" - await self._fetch_data() - self.data = self.client.get_data(self._contract)[self._contract] diff --git a/homeassistant/components/iaqualink/.translations/de.json b/homeassistant/components/iaqualink/.translations/de.json new file mode 100644 index 00000000000..d929022c905 --- /dev/null +++ b/homeassistant/components/iaqualink/.translations/de.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_setup": "Es kann nur eine einzige iAqualink-Verbindung konfiguriert werden." + }, + "error": { + "connection_failure": "Die Verbindung zu iAqualink ist nicht m\u00f6glich. Bitte \u00fcberpr\u00fcfe den Benutzernamen und das Passwort." + }, + "step": { + "user": { + "data": { + "password": "Passwort", + "username": "Benutzername/E-Mail-Adresse" + }, + "description": "Bitte geben Sie den Benutzernamen und das Passwort f\u00fcr Ihr iAqualink-Konto ein.", + "title": "Mit iAqualink verbinden" + } + }, + "title": "Jandy iAqualink" + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/.translations/ru.json b/homeassistant/components/iaqualink/.translations/ru.json index 35444dd422b..9a93c19ef20 100644 --- a/homeassistant/components/iaqualink/.translations/ru.json +++ b/homeassistant/components/iaqualink/.translations/ru.json @@ -10,7 +10,7 @@ "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u041b\u043e\u0433\u0438\u043d / \u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b" + "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.", "title": "Jandy iAqualink" diff --git a/homeassistant/components/ifttt/.translations/ca.json b/homeassistant/components/ifttt/.translations/ca.json index 597328a2ee4..979ed3cd71f 100644 --- a/homeassistant/components/ifttt/.translations/ca.json +++ b/homeassistant/components/ifttt/.translations/ca.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia." }, "create_entry": { - "default": "Per enviar esdeveniments a Home Assistant, necessitar\u00e0s utilitzar l'acci\u00f3 \"Make a web resquest\" de [IFTTT Webhook applet]({applet_url}). \n\nCompleta la seg\u00fcent informaci\u00f3: \n\n- URL: `{webhook_url}` \n- Method: POST \n- Content Type: application/json \n\nConsulta la [documentaci\u00f3]({docs_url}) sobre com configurar els automatismes per gestionar dades entrants." + "default": "Per enviar esdeveniments a Home Assistant, necessitar\u00e0s utilitzar l'acci\u00f3 \"Make a web resquest\" de [IFTTT Webhook applet]({applet_url}). \n\nCompleta la seg\u00fcent informaci\u00f3: \n\n- URL: `{webhook_url}` \n- Method: POST \n- Content Type: application/json \n\nConsulta la [documentaci\u00f3]({docs_url}) sobre com configurar les automatitzacions per gestionar dades entrants." }, "step": { "user": { diff --git a/homeassistant/components/ifttt/__init__.py b/homeassistant/components/ifttt/__init__.py index bed0cb45b1d..05d773e9fd6 100644 --- a/homeassistant/components/ifttt/__init__.py +++ b/homeassistant/components/ifttt/__init__.py @@ -2,6 +2,7 @@ import json import logging +import pyfttt import requests import voluptuous as vol @@ -69,7 +70,6 @@ async def async_setup(hass, config): target_keys[target] = api_keys[target] try: - import pyfttt for target, key in target_keys.items(): res = pyfttt.send_event(key, event, value1, value2, value3) diff --git a/homeassistant/components/ign_sismologia/geo_location.py b/homeassistant/components/ign_sismologia/geo_location.py index 057d832b4fa..8ad045c9f7a 100644 --- a/homeassistant/components/ign_sismologia/geo_location.py +++ b/homeassistant/components/ign_sismologia/geo_location.py @@ -19,8 +19,6 @@ 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 -REQUIREMENTS = ["georss_ign_sismologia_client==0.2"] - _LOGGER = logging.getLogger(__name__) ATTR_EXTERNAL_ID = "external_id" diff --git a/homeassistant/components/image_processing/manifest.json b/homeassistant/components/image_processing/manifest.json index 4a96e9828cb..6a88a358f1d 100644 --- a/homeassistant/components/image_processing/manifest.json +++ b/homeassistant/components/image_processing/manifest.json @@ -3,7 +3,7 @@ "name": "Image processing", "documentation": "https://www.home-assistant.io/integrations/image_processing", "requirements": [ - "pillow==6.1.0" + "pillow==6.2.0" ], "dependencies": [ "camera" diff --git a/homeassistant/components/imap/sensor.py b/homeassistant/components/imap/sensor.py index 0ae79d34cf0..a10fefa1b16 100644 --- a/homeassistant/components/imap/sensor.py +++ b/homeassistant/components/imap/sensor.py @@ -2,6 +2,7 @@ import asyncio import logging +from aioimaplib import IMAP4_SSL, AioImapException import async_timeout import voluptuous as vol @@ -107,24 +108,20 @@ class ImapSensor(Entity): async def connection(self): """Return a connection to the server, establishing it if necessary.""" - import aioimaplib - if self._connection is None: try: - self._connection = aioimaplib.IMAP4_SSL(self._server, self._port) + self._connection = IMAP4_SSL(self._server, self._port) await self._connection.wait_hello_from_server() await self._connection.login(self._user, self._password) await self._connection.select(self._folder) self._does_push = self._connection.has_capability("IDLE") - except (aioimaplib.AioImapException, asyncio.TimeoutError): + except (AioImapException, asyncio.TimeoutError): self._connection = None return self._connection async def idle_loop(self): """Wait for data pushed from server.""" - import aioimaplib - while True: try: if await self.connection(): @@ -138,17 +135,15 @@ class ImapSensor(Entity): await idle else: await self.async_update_ha_state() - except (aioimaplib.AioImapException, asyncio.TimeoutError): + except (AioImapException, asyncio.TimeoutError): self.disconnected() async def async_update(self): """Periodic polling of state.""" - import aioimaplib - try: if await self.connection(): await self.refresh_email_count() - except (aioimaplib.AioImapException, asyncio.TimeoutError): + except (AioImapException, asyncio.TimeoutError): self.disconnected() async def refresh_email_count(self): diff --git a/homeassistant/components/imap_email_content/sensor.py b/homeassistant/components/imap_email_content/sensor.py index c5171cde646..62dceae0dad 100644 --- a/homeassistant/components/imap_email_content/sensor.py +++ b/homeassistant/components/imap_email_content/sensor.py @@ -4,6 +4,7 @@ import datetime import email from collections import deque +import imaplib import voluptuous as vol from homeassistant.helpers.entity import Entity @@ -88,8 +89,6 @@ class EmailReader: def connect(self): """Login and setup the connection.""" - import imaplib - try: self.connection = imaplib.IMAP4_SSL(self._server, self._port) self.connection.login(self._user, self._password) @@ -110,8 +109,6 @@ class EmailReader: def read_next(self): """Read the next email from the email server.""" - import imaplib - try: self.connection.select(self._folder, readonly=True) diff --git a/homeassistant/components/incomfort/__init__.py b/homeassistant/components/incomfort/__init__.py index d6f72209f06..adf57e35093 100644 --- a/homeassistant/components/incomfort/__init__.py +++ b/homeassistant/components/incomfort/__init__.py @@ -1,14 +1,18 @@ """Support for an Intergas boiler via an InComfort/Intouch Lan2RF gateway.""" import logging +from typing import Optional from aiohttp import ClientResponseError -import voluptuous as vol from incomfortclient import Gateway as InComfortGateway +import voluptuous as vol from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +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.discovery import async_load_platform +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -53,3 +57,38 @@ async def async_setup(hass, hass_config): ) return True + + +class IncomfortEntity(Entity): + """Base class for all InComfort entities.""" + + def __init__(self) -> None: + """Initialize the class.""" + self._unique_id = self._name = None + + @property + def unique_id(self) -> Optional[str]: + """Return a unique ID.""" + return self._unique_id + + @property + def name(self) -> Optional[str]: + """Return the name of the sensor.""" + return self._name + + +class IncomfortChild(IncomfortEntity): + """Base class for all InComfort entities (excluding the boiler).""" + + async def async_added_to_hass(self) -> None: + """Set up a listener when this entity is added to HA.""" + async_dispatcher_connect(self.hass, DOMAIN, self._refresh) + + @callback + def _refresh(self) -> None: + self.async_schedule_update_ha_state(force_refresh=True) + + @property + def should_poll(self) -> bool: + """Return False as this device should never be polled.""" + return False diff --git a/homeassistant/components/incomfort/binary_sensor.py b/homeassistant/components/incomfort/binary_sensor.py index 39a45429cb1..b5dbd8e223d 100644 --- a/homeassistant/components/incomfort/binary_sensor.py +++ b/homeassistant/components/incomfort/binary_sensor.py @@ -1,11 +1,9 @@ """Support for an Intergas heater via an InComfort/InTouch Lan2RF gateway.""" from typing import Any, Dict, Optional -from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.components.binary_sensor import ENTITY_ID_FORMAT, BinarySensorDevice -from . import DOMAIN +from . import DOMAIN, IncomfortChild async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -18,34 +16,20 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) -class IncomfortFailed(BinarySensorDevice): +class IncomfortFailed(IncomfortChild, BinarySensorDevice): """Representation of an InComfort Failed sensor.""" def __init__(self, client, heater) -> None: """Initialize the binary sensor.""" + super().__init__() + self._unique_id = f"{heater.serial_no}_failed" + self.entity_id = ENTITY_ID_FORMAT.format(f"{DOMAIN}_failed") + self._name = "Boiler Fault" self._client = client self._heater = heater - async def async_added_to_hass(self) -> None: - """Set up a listener when this entity is added to HA.""" - async_dispatcher_connect(self.hass, DOMAIN, self._refresh) - - @callback - def _refresh(self) -> None: - self.async_schedule_update_ha_state(force_refresh=True) - - @property - def unique_id(self) -> Optional[str]: - """Return a unique ID.""" - return self._unique_id - - @property - def name(self) -> Optional[str]: - """Return the name of the sensor.""" - return "Fault state" - @property def is_on(self) -> bool: """Return the status of the sensor.""" @@ -55,8 +39,3 @@ class IncomfortFailed(BinarySensorDevice): def device_state_attributes(self) -> Optional[Dict[str, Any]]: """Return the device state attributes.""" return {"fault_code": self._heater.status["fault_code"]} - - @property - def should_poll(self) -> bool: - """Return False as this device should never be polled.""" - return False diff --git a/homeassistant/components/incomfort/climate.py b/homeassistant/components/incomfort/climate.py index 3918244d4e8..95ccf186372 100644 --- a/homeassistant/components/incomfort/climate.py +++ b/homeassistant/components/incomfort/climate.py @@ -1,16 +1,14 @@ """Support for an Intergas boiler via an InComfort/InTouch Lan2RF gateway.""" from typing import Any, Dict, List, Optional -from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate import ENTITY_ID_FORMAT, ClimateDevice from homeassistant.components.climate.const import ( HVAC_MODE_HEAT, SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from . import DOMAIN +from . import DOMAIN, IncomfortChild async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -24,39 +22,19 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([InComfortClimate(client, heater, r) for r in heater.rooms]) -class InComfortClimate(ClimateDevice): +class InComfortClimate(IncomfortChild, ClimateDevice): """Representation of an InComfort/InTouch climate device.""" def __init__(self, client, heater, room) -> None: """Initialize the climate device.""" + super().__init__() + self._unique_id = f"{heater.serial_no}_{room.room_no}" + self.entity_id = ENTITY_ID_FORMAT.format(f"{DOMAIN}_{room.room_no}") + self._name = f"Thermostat {room.room_no}" self._client = client self._room = room - self._name = f"Room {room.room_no}" - - async def async_added_to_hass(self) -> None: - """Set up a listener when this entity is added to HA.""" - async_dispatcher_connect(self.hass, DOMAIN, self._refresh) - - @callback - def _refresh(self) -> None: - self.async_schedule_update_ha_state(force_refresh=True) - - @property - def should_poll(self) -> bool: - """Return False as this device should never be polled.""" - return False - - @property - def unique_id(self) -> Optional[str]: - """Return a unique ID.""" - return self._unique_id - - @property - def name(self) -> str: - """Return the name of the climate device.""" - return self._name @property def device_state_attributes(self) -> Dict[str, Any]: diff --git a/homeassistant/components/incomfort/sensor.py b/homeassistant/components/incomfort/sensor.py index 772b5dab183..f3170b7b9bb 100644 --- a/homeassistant/components/incomfort/sensor.py +++ b/homeassistant/components/incomfort/sensor.py @@ -1,18 +1,16 @@ """Support for an Intergas heater via an InComfort/InTouch Lan2RF gateway.""" from typing import Any, Dict, Optional +from homeassistant.components.sensor import ENTITY_ID_FORMAT from homeassistant.const import ( - PRESSURE_BAR, - TEMP_CELSIUS, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, + PRESSURE_BAR, + TEMP_CELSIUS, ) -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity from homeassistant.util import slugify -from . import DOMAIN +from . import DOMAIN, IncomfortChild INCOMFORT_HEATER_TEMP = "CV Temp" INCOMFORT_PRESSURE = "CV Pressure" @@ -42,42 +40,28 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) -class IncomfortSensor(Entity): +class IncomfortSensor(IncomfortChild): """Representation of an InComfort/InTouch sensor device.""" def __init__(self, client, heater, name) -> None: """Initialize the sensor.""" + super().__init__() + self._client = client self._heater = heater self._unique_id = f"{heater.serial_no}_{slugify(name)}" + self.entity_id = ENTITY_ID_FORMAT.format(f"{DOMAIN}_{slugify(name)}") + self._name = f"Boiler {name}" - self._name = name self._device_class = None + self._state_attr = INCOMFORT_MAP_ATTRS[name][0] self._unit_of_measurement = None - async def async_added_to_hass(self) -> None: - """Set up a listener when this entity is added to HA.""" - async_dispatcher_connect(self.hass, DOMAIN, self._refresh) - - @callback - def _refresh(self) -> None: - self.async_schedule_update_ha_state(force_refresh=True) - - @property - def unique_id(self) -> Optional[str]: - """Return a unique ID.""" - return self._unique_id - - @property - def name(self) -> Optional[str]: - """Return the name of the sensor.""" - return self._name - @property def state(self) -> Optional[str]: """Return the state of the sensor.""" - return self._heater.status[INCOMFORT_MAP_ATTRS[self._name][0]] + return self._heater.status[self._state_attr] @property def device_class(self) -> Optional[str]: @@ -89,11 +73,6 @@ class IncomfortSensor(Entity): """Return the unit of measurement of the sensor.""" return self._unit_of_measurement - @property - def should_poll(self) -> bool: - """Return False as this device should never be polled.""" - return False - class IncomfortPressure(IncomfortSensor): """Representation of an InTouch CV Pressure sensor.""" @@ -113,11 +92,11 @@ class IncomfortTemperature(IncomfortSensor): """Initialize the signal strength sensor.""" super().__init__(client, heater, name) + self._attr = INCOMFORT_MAP_ATTRS[name][1] self._device_class = DEVICE_CLASS_TEMPERATURE self._unit_of_measurement = TEMP_CELSIUS @property def device_state_attributes(self) -> Optional[Dict[str, Any]]: """Return the device state attributes.""" - key = INCOMFORT_MAP_ATTRS[self._name][1] - return {key: self._heater.status[key]} + return {self._attr: self._heater.status[self._attr]} diff --git a/homeassistant/components/incomfort/water_heater.py b/homeassistant/components/incomfort/water_heater.py index 70423611705..0015107b40f 100644 --- a/homeassistant/components/incomfort/water_heater.py +++ b/homeassistant/components/incomfort/water_heater.py @@ -1,14 +1,15 @@ """Support for an Intergas boiler via an InComfort/Intouch Lan2RF gateway.""" import asyncio import logging -from typing import Any, Dict, Optional +from typing import Any, Dict from aiohttp import ClientResponseError -from homeassistant.components.water_heater import WaterHeaterDevice + +from homeassistant.components.water_heater import ENTITY_ID_FORMAT, WaterHeaterDevice from homeassistant.const import TEMP_CELSIUS from homeassistant.helpers.dispatcher import async_dispatcher_send -from . import DOMAIN +from . import DOMAIN, IncomfortEntity _LOGGER = logging.getLogger(__name__) @@ -26,26 +27,20 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([IncomfortWaterHeater(client, heater)]) -class IncomfortWaterHeater(WaterHeaterDevice): +class IncomfortWaterHeater(IncomfortEntity, WaterHeaterDevice): """Representation of an InComfort/Intouch water_heater device.""" def __init__(self, client, heater) -> None: """Initialize the water_heater device.""" + super().__init__() + self._unique_id = f"{heater.serial_no}" + self.entity_id = ENTITY_ID_FORMAT.format(DOMAIN) + self._name = "Boiler" self._client = client self._heater = heater - @property - def unique_id(self) -> Optional[str]: - """Return a unique ID.""" - return self._unique_id - - @property - def name(self) -> str: - """Return the name of the water_heater device.""" - return "Boiler" - @property def icon(self) -> str: """Return the icon of the water_heater device.""" diff --git a/homeassistant/components/influxdb/__init__.py b/homeassistant/components/influxdb/__init__.py index 2bb5207aa85..86d489621ea 100644 --- a/homeassistant/components/influxdb/__init__.py +++ b/homeassistant/components/influxdb/__init__.py @@ -353,7 +353,11 @@ class InfluxThread(threading.Thread): _LOGGER.debug("Wrote %d events", len(json)) break - except (exceptions.InfluxDBClientError, IOError) as err: + except ( + exceptions.InfluxDBClientError, + exceptions.InfluxDBServerError, + IOError, + ) as err: if retry < self.max_tries: time.sleep(RETRY_DELAY) else: diff --git a/homeassistant/components/input_datetime/reproduce_state.py b/homeassistant/components/input_datetime/reproduce_state.py new file mode 100644 index 00000000000..09a30e65210 --- /dev/null +++ b/homeassistant/components/input_datetime/reproduce_state.py @@ -0,0 +1,111 @@ +"""Reproduce an Input datetime state.""" +import asyncio +import logging +from typing import Iterable, Optional + +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import Context, State +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.util import dt as dt_util + +from . import ( + ATTR_DATE, + ATTR_DATETIME, + ATTR_TIME, + CONF_HAS_DATE, + CONF_HAS_TIME, + DOMAIN, + SERVICE_SET_DATETIME, +) + +_LOGGER = logging.getLogger(__name__) + + +def is_valid_datetime(string: str) -> bool: + """Test if string dt is a valid datetime.""" + try: + return dt_util.parse_datetime(string) is not None + except ValueError: + return False + + +def is_valid_date(string: str) -> bool: + """Test if string dt is a valid date.""" + try: + return dt_util.parse_date(string) is not None + except ValueError: + return False + + +def is_valid_time(string: str) -> bool: + """Test if string dt is a valid time.""" + try: + return dt_util.parse_time(string) is not None + except ValueError: + return False + + +async def _async_reproduce_state( + hass: HomeAssistantType, state: State, context: Optional[Context] = None +) -> None: + """Reproduce a single state.""" + cur_state = hass.states.get(state.entity_id) + + if cur_state is None: + _LOGGER.warning("Unable to find entity %s", state.entity_id) + return + + if not ( + ( + is_valid_datetime(state.state) + and cur_state.attributes.get(CONF_HAS_DATE) + and cur_state.attributes.get(CONF_HAS_TIME) + ) + or ( + is_valid_date(state.state) + and cur_state.attributes.get(CONF_HAS_DATE) + and not cur_state.attributes.get(CONF_HAS_TIME) + ) + or ( + is_valid_time(state.state) + and cur_state.attributes.get(CONF_HAS_TIME) + and not cur_state.attributes.get(CONF_HAS_DATE) + ) + ): + _LOGGER.warning( + "Invalid state specified for %s: %s", state.entity_id, state.state + ) + return + + # Return if we are already at the right state. + if cur_state.state == state.state: + return + + service = SERVICE_SET_DATETIME + service_data = {ATTR_ENTITY_ID: state.entity_id} + + has_time = cur_state.attributes.get(CONF_HAS_TIME) + has_date = cur_state.attributes.get(CONF_HAS_DATE) + + if has_time and has_date: + service_data[ATTR_DATETIME] = state.state + elif has_time: + service_data[ATTR_TIME] = state.state + elif has_date: + service_data[ATTR_DATE] = state.state + else: + _LOGGER.warning("input_datetime needs either has_date or has_time or both") + return + + await hass.services.async_call( + DOMAIN, service, service_data, context=context, blocking=True + ) + + +async def async_reproduce_states( + hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None +) -> None: + """Reproduce Input datetime states.""" + await asyncio.gather( + *(_async_reproduce_state(hass, state, context) for state in states) + ) diff --git a/homeassistant/components/input_number/reproduce_state.py b/homeassistant/components/input_number/reproduce_state.py new file mode 100644 index 00000000000..97a4837d371 --- /dev/null +++ b/homeassistant/components/input_number/reproduce_state.py @@ -0,0 +1,52 @@ +"""Reproduce an Input number state.""" +import asyncio +import logging +from typing import Iterable, Optional + +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 + +_LOGGER = logging.getLogger(__name__) + + +async def _async_reproduce_state( + hass: HomeAssistantType, state: State, context: Optional[Context] = None +) -> None: + """Reproduce a single state.""" + cur_state = hass.states.get(state.entity_id) + + if cur_state is None: + _LOGGER.warning("Unable to find entity %s", state.entity_id) + return + + try: + float(state.state) + except ValueError: + _LOGGER.warning( + "Invalid state specified for %s: %s", state.entity_id, state.state + ) + return + + # Return if we are already at the right state. + if cur_state.state == state.state: + return + + service = SERVICE_SET_VALUE + service_data = {ATTR_ENTITY_ID: state.entity_id, ATTR_VALUE: state.state} + + await hass.services.async_call( + DOMAIN, service, service_data, context=context, blocking=True + ) + + +async def async_reproduce_states( + hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None +) -> None: + """Reproduce Input number states.""" + # Reproduce states in parallel. + await asyncio.gather( + *(_async_reproduce_state(hass, state, context) for state in states) + ) diff --git a/homeassistant/components/input_select/reproduce_state.py b/homeassistant/components/input_select/reproduce_state.py new file mode 100644 index 00000000000..657f518cd3d --- /dev/null +++ b/homeassistant/components/input_select/reproduce_state.py @@ -0,0 +1,80 @@ +"""Reproduce an Input select state.""" +import asyncio +import logging +from types import MappingProxyType +from typing import Iterable, Optional + +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import Context, State +from homeassistant.helpers.typing import HomeAssistantType + +from . import ( + DOMAIN, + SERVICE_SELECT_OPTION, + SERVICE_SET_OPTIONS, + ATTR_OPTION, + ATTR_OPTIONS, +) + +ATTR_GROUP = [ATTR_OPTION, ATTR_OPTIONS] + +_LOGGER = logging.getLogger(__name__) + + +async def _async_reproduce_state( + hass: HomeAssistantType, state: State, context: Optional[Context] = None +) -> None: + """Reproduce a single state.""" + cur_state = hass.states.get(state.entity_id) + + # Return if we can't find entity + if cur_state is None: + _LOGGER.warning("Unable to find entity %s", state.entity_id) + return + + # Return if we are already at the right state. + if cur_state.state == state.state and all( + check_attr_equal(cur_state.attributes, state.attributes, attr) + for attr in ATTR_GROUP + ): + return + + # Set service data + service_data = {ATTR_ENTITY_ID: state.entity_id} + + # If options are specified, call SERVICE_SET_OPTIONS + if ATTR_OPTIONS in state.attributes: + service = SERVICE_SET_OPTIONS + service_data[ATTR_OPTIONS] = state.attributes[ATTR_OPTIONS] + + await hass.services.async_call( + DOMAIN, service, service_data, context=context, blocking=True + ) + + # Remove ATTR_OPTIONS from service_data so we can reuse service_data in next call + del service_data[ATTR_OPTIONS] + + # Call SERVICE_SELECT_OPTION + service = SERVICE_SELECT_OPTION + service_data[ATTR_OPTION] = state.state + + await hass.services.async_call( + DOMAIN, service, service_data, context=context, blocking=True + ) + + +async def async_reproduce_states( + hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None +) -> None: + """Reproduce Input select states.""" + # Reproduce states in parallel. + await asyncio.gather( + *(_async_reproduce_state(hass, state, context) for state in states) + ) + + +def check_attr_equal( + attr1: MappingProxyType, attr2: MappingProxyType, attr_str: str +) -> bool: + """Return true if the given attributes are equal.""" + return attr1.get(attr_str) == attr2.get(attr_str) diff --git a/homeassistant/components/input_text/reproduce_state.py b/homeassistant/components/input_text/reproduce_state.py new file mode 100644 index 00000000000..f64c5c019f6 --- /dev/null +++ b/homeassistant/components/input_text/reproduce_state.py @@ -0,0 +1,46 @@ +"""Reproduce an Input text state.""" +import asyncio +import logging +from typing import Iterable, Optional + +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 + +_LOGGER = logging.getLogger(__name__) + + +async def _async_reproduce_state( + hass: HomeAssistantType, state: State, context: Optional[Context] = None +) -> None: + """Reproduce a single state.""" + cur_state = hass.states.get(state.entity_id) + + # Return if we can't find the entity + if cur_state is None: + _LOGGER.warning("Unable to find entity %s", state.entity_id) + return + + # Return if we are already at the right state. + if cur_state.state == state.state: + return + + # Call service + service = SERVICE_SET_VALUE + service_data = {ATTR_ENTITY_ID: state.entity_id, ATTR_VALUE: state.state} + + await hass.services.async_call( + DOMAIN, service, service_data, context=context, blocking=True + ) + + +async def async_reproduce_states( + hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None +) -> None: + """Reproduce Input text states.""" + # Reproduce states in parallel. + await asyncio.gather( + *(_async_reproduce_state(hass, state, context) for state in states) + ) diff --git a/homeassistant/components/insteon/__init__.py b/homeassistant/components/insteon/__init__.py index 4015d472ce8..11f224dbfcc 100644 --- a/homeassistant/components/insteon/__init__.py +++ b/homeassistant/components/insteon/__init__.py @@ -3,6 +3,38 @@ import collections import logging from typing import Dict +import insteonplm +from insteonplm.devices import ALDBStatus +from insteonplm.states.cover import Cover +from insteonplm.states.dimmable import ( + DimmableKeypadA, + DimmableRemote, + DimmableSwitch, + DimmableSwitch_Fan, +) +from insteonplm.states.onOff import ( + OnOffKeypad, + OnOffKeypadA, + OnOffSwitch, + OnOffSwitch_OutletBottom, + OnOffSwitch_OutletTop, + OpenClosedRelay, +) +from insteonplm.states.sensor import ( + IoLincSensor, + LeakSensorDryWet, + OnOffSensor, + SmokeCO2Sensor, + VariableSensor, +) +from insteonplm.states.x10 import ( + X10AllLightsOffSensor, + X10AllLightsOnSensor, + X10AllUnitsOffSensor, + X10DimmableSwitch, + X10OnOffSensor, + X10OnOffSwitch, +) import voluptuous as vol from homeassistant.const import ( @@ -16,8 +48,8 @@ from homeassistant.const import ( ) from homeassistant.core import callback 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 _LOGGER = logging.getLogger(__name__) @@ -240,8 +272,6 @@ STATE_NAME_LABEL_MAP = { async def async_setup(hass, config): """Set up the connection to the modem.""" - import insteonplm - ipdb = IPDB() insteon_modem = None @@ -496,41 +526,6 @@ class IPDB: def __init__(self): """Create the INSTEON Product Database (IPDB).""" - from insteonplm.states.cover import Cover - - from insteonplm.states.onOff import ( - OnOffSwitch, - OnOffSwitch_OutletTop, - OnOffSwitch_OutletBottom, - OpenClosedRelay, - OnOffKeypadA, - OnOffKeypad, - ) - - from insteonplm.states.dimmable import ( - DimmableSwitch, - DimmableSwitch_Fan, - DimmableRemote, - DimmableKeypadA, - ) - - from insteonplm.states.sensor import ( - VariableSensor, - OnOffSensor, - SmokeCO2Sensor, - IoLincSensor, - LeakSensorDryWet, - ) - - from insteonplm.states.x10 import ( - X10DimmableSwitch, - X10OnOffSwitch, - X10OnOffSensor, - X10AllUnitsOffSensor, - X10AllLightsOnSensor, - X10AllLightsOffSensor, - ) - self.states = [ State(Cover, "cover"), State(OnOffSwitch_OutletTop, "switch"), @@ -685,8 +680,6 @@ class InsteonEntity(Entity): def print_aldb_to_log(aldb): """Print the All-Link Database to the log file.""" - from insteonplm.devices import ALDBStatus - _LOGGER.info("ALDB load status is %s", aldb.status.name) if aldb.status not in [ALDBStatus.LOADED, ALDBStatus.PARTIAL]: _LOGGER.warning("Device All-Link database not loaded") diff --git a/homeassistant/components/iperf3/__init__.py b/homeassistant/components/iperf3/__init__.py index eda601b09de..753ea60efa4 100644 --- a/homeassistant/components/iperf3/__init__.py +++ b/homeassistant/components/iperf3/__init__.py @@ -1,19 +1,20 @@ """Support for Iperf3 network measurement tool.""" -import logging from datetime import timedelta +import logging +import iperf3 import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import ( + CONF_HOST, + CONF_HOSTS, CONF_MONITORED_CONDITIONS, CONF_PORT, - CONF_HOST, CONF_PROTOCOL, - CONF_HOSTS, 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 @@ -80,8 +81,6 @@ SERVICE_SCHEMA = vol.Schema({vol.Optional(ATTR_HOST, default=None): cv.string}) async def async_setup(hass, config): """Set up the iperf3 component.""" - import iperf3 - hass.data[DOMAIN] = {} conf = config[DOMAIN] diff --git a/homeassistant/components/ipma/.translations/ru.json b/homeassistant/components/ipma/.translations/ru.json index a302572ed12..0db504c629c 100644 --- a/homeassistant/components/ipma/.translations/ru.json +++ b/homeassistant/components/ipma/.translations/ru.json @@ -10,7 +10,7 @@ "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" }, - "description": "\u041f\u043e\u0440\u0442\u0443\u0433\u0430\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0441\u0442\u0438\u0442\u0443\u0442 \u043c\u043e\u0440\u044f \u0438 \u0430\u0442\u043c\u043e\u0441\u0444\u0435\u0440\u044b", + "description": "\u041f\u043e\u0440\u0442\u0443\u0433\u0430\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0441\u0442\u0438\u0442\u0443\u0442 \u043c\u043e\u0440\u044f \u0438 \u0430\u0442\u043c\u043e\u0441\u0444\u0435\u0440\u044b.", "title": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435" } }, diff --git a/homeassistant/components/iqvia/.translations/ru.json b/homeassistant/components/iqvia/.translations/ru.json index 0c3afc88c94..336877fda13 100644 --- a/homeassistant/components/iqvia/.translations/ru.json +++ b/homeassistant/components/iqvia/.translations/ru.json @@ -2,7 +2,7 @@ "config": { "error": { "identifier_exists": "\u041f\u043e\u0447\u0442\u043e\u0432\u044b\u0439 \u0438\u043d\u0434\u0435\u043a\u0441 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d.", - "invalid_zip_code": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u043e\u0447\u0442\u043e\u0432\u044b\u0439 \u0438\u043d\u0434\u0435\u043a\u0441" + "invalid_zip_code": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u043e\u0447\u0442\u043e\u0432\u044b\u0439 \u0438\u043d\u0434\u0435\u043a\u0441." }, "step": { "user": { diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json index caf422938b2..04723a6a1f6 100644 --- a/homeassistant/components/iqvia/manifest.json +++ b/homeassistant/components/iqvia/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/iqvia", "requirements": [ - "numpy==1.17.1", + "numpy==1.17.3", "pyiqvia==0.2.1" ], "dependencies": [], diff --git a/homeassistant/components/iss/binary_sensor.py b/homeassistant/components/iss/binary_sensor.py index 5f38c3d166e..002b2e958f7 100644 --- a/homeassistant/components/iss/binary_sensor.py +++ b/homeassistant/components/iss/binary_sensor.py @@ -1,18 +1,19 @@ """Support for International Space Station data sensor.""" -import logging from datetime import timedelta +import logging +import pyiss import requests import voluptuous as vol -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.const import ( - CONF_NAME, - ATTR_LONGITUDE, ATTR_LATITUDE, + ATTR_LONGITUDE, + CONF_NAME, CONF_SHOW_ON_MAP, ) +import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -113,8 +114,6 @@ class IssData: @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data from the ISS API.""" - import pyiss - try: iss = pyiss.ISS() self.is_above = iss.is_ISS_above(self.latitude, self.longitude) diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index 324dcb019b3..96796e37a6a 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -3,6 +3,8 @@ from collections import namedtuple import logging from urllib.parse import urlparse +import PyISY +from PyISY.Nodes import Group import voluptuous as vol from homeassistant.const import ( @@ -312,8 +314,6 @@ def _categorize_nodes( # Don't import this node as a device at all continue - from PyISY.Nodes import Group - if isinstance(node, Group): hass.data[ISY994_NODES][SCENE_DOMAIN].append(node) continue @@ -419,8 +419,6 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: _LOGGER.error("isy994 host value in configuration is invalid") return False - import PyISY - # Connect to ISY controller. isy = PyISY.ISY( host.hostname, diff --git a/homeassistant/components/itach/remote.py b/homeassistant/components/itach/remote.py index 9895b54a50d..5390111890c 100644 --- a/homeassistant/components/itach/remote.py +++ b/homeassistant/components/itach/remote.py @@ -1,19 +1,20 @@ """Support for iTach IR devices.""" import logging +import pyitachip2ir import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components import remote -from homeassistant.const import ( - DEVICE_DEFAULT_NAME, - CONF_NAME, - CONF_MAC, - CONF_HOST, - CONF_PORT, - CONF_DEVICES, -) from homeassistant.components.remote import PLATFORM_SCHEMA +from homeassistant.const import ( + CONF_DEVICES, + CONF_HOST, + CONF_MAC, + CONF_NAME, + CONF_PORT, + DEVICE_DEFAULT_NAME, +) +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -55,8 +56,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the ITach connection and devices.""" - import pyitachip2ir - itachip2ir = pyitachip2ir.ITachIP2IR( config.get(CONF_MAC), config.get(CONF_HOST), int(config.get(CONF_PORT)) ) diff --git a/homeassistant/components/izone/.translations/de.json b/homeassistant/components/izone/.translations/de.json new file mode 100644 index 00000000000..3c7ebfa937f --- /dev/null +++ b/homeassistant/components/izone/.translations/de.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Es wurden keine iZone-Ger\u00e4te im Netzwerk gefunden.", + "single_instance_allowed": "Es ist nur eine einzige Konfiguration von iZone erforderlich." + }, + "step": { + "confirm": { + "description": "M\u00f6chten Sie iZone einrichten?", + "title": "iZone" + } + }, + "title": "iZone" + } +} \ No newline at end of file diff --git a/homeassistant/components/izone/.translations/nl.json b/homeassistant/components/izone/.translations/nl.json new file mode 100644 index 00000000000..979441f7288 --- /dev/null +++ b/homeassistant/components/izone/.translations/nl.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Geen iZone-apparaten gevonden op het netwerk.", + "single_instance_allowed": "Er is slechts \u00e9\u00e9n configuratie van iZone nodig." + }, + "step": { + "confirm": { + "description": "Wilt u iZone instellen?", + "title": "iZone" + } + }, + "title": "iZone" + } +} \ No newline at end of file diff --git a/homeassistant/components/jewish_calendar/__init__.py b/homeassistant/components/jewish_calendar/__init__.py index c7bbbdb2d90..bbe0c1d24fd 100644 --- a/homeassistant/components/jewish_calendar/__init__.py +++ b/homeassistant/components/jewish_calendar/__init__.py @@ -20,8 +20,7 @@ SENSOR_TYPES = { "data": { "date": ["Date", "mdi:judaism"], "weekly_portion": ["Parshat Hashavua", "mdi:book-open-variant"], - "holiday_name": ["Holiday name", "mdi:calendar-star"], - "holiday_type": ["Holiday type", "mdi:counter"], + "holiday": ["Holiday", "mdi:calendar-star"], "omer_count": ["Day of the Omer", "mdi:counter"], }, "time": { diff --git a/homeassistant/components/jewish_calendar/manifest.json b/homeassistant/components/jewish_calendar/manifest.json index 7b6653ba832..08182daedd0 100644 --- a/homeassistant/components/jewish_calendar/manifest.json +++ b/homeassistant/components/jewish_calendar/manifest.json @@ -3,7 +3,7 @@ "name": "Jewish calendar", "documentation": "https://www.home-assistant.io/integrations/jewish_calendar", "requirements": [ - "hdate==0.9.0" + "hdate==0.9.1" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/jewish_calendar/sensor.py b/homeassistant/components/jewish_calendar/sensor.py index 405838b1fb1..54a3d1497aa 100644 --- a/homeassistant/components/jewish_calendar/sensor.py +++ b/homeassistant/components/jewish_calendar/sensor.py @@ -23,7 +23,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= for sensor, sensor_info in SENSOR_TYPES["data"].items() ] sensors.extend( - JewishCalendarSensor(hass.data[DOMAIN], sensor, sensor_info) + JewishCalendarTimeSensor(hass.data[DOMAIN], sensor, sensor_info) for sensor, sensor_info in SENSOR_TYPES["time"].items() ) @@ -44,6 +44,7 @@ class JewishCalendarSensor(Entity): self._havdalah_offset = data["havdalah_offset"] self._diaspora = data["diaspora"] self._state = None + self._holiday_attrs = {} @property def name(self): @@ -63,7 +64,7 @@ class JewishCalendarSensor(Entity): async def async_update(self): """Update the state of the sensor.""" now = dt_util.now() - _LOGGER.debug("Now: %s Timezone = %s", now, now.tzinfo) + _LOGGER.debug("Now: %s Location: %r", now, self._location) today = now.date() sunset = dt_util.as_local( @@ -72,16 +73,6 @@ class JewishCalendarSensor(Entity): _LOGGER.debug("Now: %s Sunset: %s", now, sunset) - def make_zmanim(date): - """Create a Zmanim object.""" - return hdate.Zmanim( - date=date, - location=self._location, - candle_lighting_offset=self._candle_lighting_offset, - havdalah_offset=self._havdalah_offset, - hebrew=self._hebrew, - ) - date = hdate.HDate(today, diaspora=self._diaspora, hebrew=self._hebrew) # The Jewish day starts after darkness (called "tzais") and finishes at @@ -92,7 +83,7 @@ class JewishCalendarSensor(Entity): # tomorrow based on sunset ("shkia"), for others based on "tzais". # Hence the following variables. after_tzais_date = after_shkia_date = date - today_times = make_zmanim(today) + today_times = self.make_zmanim(today) if now > sunset: after_shkia_date = date.next_day @@ -100,37 +91,91 @@ class JewishCalendarSensor(Entity): if today_times.havdalah and now > today_times.havdalah: after_tzais_date = date.next_day + self._state = self.get_state(after_shkia_date, after_tzais_date) + _LOGGER.debug("New value for %s: %s", self._type, self._state) + + def make_zmanim(self, date): + """Create a Zmanim object.""" + return hdate.Zmanim( + date=date, + location=self._location, + candle_lighting_offset=self._candle_lighting_offset, + havdalah_offset=self._havdalah_offset, + hebrew=self._hebrew, + ) + + @property + def device_state_attributes(self): + """Return the state attributes.""" + if self._type == "holiday": + return self._holiday_attrs + + return {} + + def get_state(self, after_shkia_date, after_tzais_date): + """For a given type of sensor, return the state.""" # Terminology note: by convention in py-libhdate library, "upcoming" # refers to "current" or "upcoming" dates. if self._type == "date": - self._state = after_shkia_date.hebrew_date - elif self._type == "weekly_portion": + return after_shkia_date.hebrew_date + if self._type == "weekly_portion": # Compute the weekly portion based on the upcoming shabbat. - self._state = after_tzais_date.upcoming_shabbat.parasha - elif self._type == "holiday_name": - self._state = after_shkia_date.holiday_description - elif self._type == "holiday_type": - self._state = after_shkia_date.holiday_type - elif self._type == "upcoming_shabbat_candle_lighting": - times = make_zmanim(after_tzais_date.upcoming_shabbat.previous_day.gdate) - self._state = times.candle_lighting - elif self._type == "upcoming_candle_lighting": - times = make_zmanim( + 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 + return after_shkia_date.holiday_description + if self._type == "omer_count": + return after_shkia_date.omer_day + + return None + + +class JewishCalendarTimeSensor(JewishCalendarSensor): + """Implement attrbutes for sensors returning times.""" + + @property + def state(self): + """Return the state of the sensor.""" + return dt_util.as_utc(self._state) if self._state is not None else None + + @property + def device_class(self): + """Return the class of this sensor.""" + return "timestamp" + + @property + def device_state_attributes(self): + """Return the state attributes.""" + attrs = {} + + if self._state is None: + return attrs + + attrs["timestamp"] = self._state.timestamp() + + return attrs + + def get_state(self, after_shkia_date, after_tzais_date): + """For a given type of sensor, return the state.""" + if self._type == "upcoming_shabbat_candle_lighting": + times = self.make_zmanim( + after_tzais_date.upcoming_shabbat.previous_day.gdate + ) + return times.candle_lighting + if self._type == "upcoming_candle_lighting": + times = self.make_zmanim( after_tzais_date.upcoming_shabbat_or_yom_tov.first_day.previous_day.gdate ) - self._state = times.candle_lighting - elif self._type == "upcoming_shabbat_havdalah": - times = make_zmanim(after_tzais_date.upcoming_shabbat.gdate) - self._state = times.havdalah - elif self._type == "upcoming_havdalah": - times = make_zmanim( + return times.candle_lighting + if self._type == "upcoming_shabbat_havdalah": + times = self.make_zmanim(after_tzais_date.upcoming_shabbat.gdate) + return times.havdalah + if self._type == "upcoming_havdalah": + times = self.make_zmanim( after_tzais_date.upcoming_shabbat_or_yom_tov.last_day.gdate ) - self._state = times.havdalah - elif self._type == "omer_count": - self._state = after_shkia_date.omer_day - else: - times = make_zmanim(today).zmanim - self._state = times[self._type].time() + return times.havdalah - _LOGGER.debug("New value: %s", self._state) + times = self.make_zmanim(dt_util.now()).zmanim + return times[self._type] diff --git a/homeassistant/components/juicenet/__init__.py b/homeassistant/components/juicenet/__init__.py index a176236b224..207dac7836a 100644 --- a/homeassistant/components/juicenet/__init__.py +++ b/homeassistant/components/juicenet/__init__.py @@ -1,12 +1,13 @@ """Support for Juicenet cloud.""" import logging +import pyjuicenet import voluptuous as vol -from homeassistant.helpers import discovery from homeassistant.const import CONF_ACCESS_TOKEN -from homeassistant.helpers.entity import Entity +from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -20,8 +21,6 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): """Set up the Juicenet component.""" - import pyjuicenet - hass.data[DOMAIN] = {} access_token = config[DOMAIN].get(CONF_ACCESS_TOKEN) diff --git a/homeassistant/components/kaiterra/__init__.py b/homeassistant/components/kaiterra/__init__.py index 8c61ad54184..d043dc15eaf 100644 --- a/homeassistant/components/kaiterra/__init__.py +++ b/homeassistant/components/kaiterra/__init__.py @@ -1,35 +1,33 @@ """Support for Kaiterra devices.""" import voluptuous as vol -from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.discovery import async_load_platform -from homeassistant.helpers import config_validation as cv - from homeassistant.const import ( CONF_API_KEY, - CONF_DEVICES, CONF_DEVICE_ID, + CONF_DEVICES, + CONF_NAME, CONF_SCAN_INTERVAL, CONF_TYPE, - CONF_NAME, ) +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.helpers.event import async_track_time_interval +from .api_data import KaiterraApiData from .const import ( AVAILABLE_AQI_STANDARDS, - AVAILABLE_UNITS, AVAILABLE_DEVICE_TYPES, + AVAILABLE_UNITS, CONF_AQI_STANDARD, CONF_PREFERRED_UNITS, - DOMAIN, DEFAULT_AQI_STANDARD, DEFAULT_PREFERRED_UNIT, DEFAULT_SCAN_INTERVAL, + DOMAIN, KAITERRA_COMPONENTS, ) -from .api_data import KaiterraApiData - KAITERRA_DEVICE_SCHEMA = vol.Schema( { vol.Required(CONF_DEVICE_ID): cv.string, diff --git a/homeassistant/components/kaiterra/air_quality.py b/homeassistant/components/kaiterra/air_quality.py index 70699de394c..1de1a4bd6c5 100644 --- a/homeassistant/components/kaiterra/air_quality.py +++ b/homeassistant/components/kaiterra/air_quality.py @@ -1,16 +1,14 @@ """Support for Kaiterra Air Quality Sensors.""" from homeassistant.components.air_quality import AirQualityEntity - +from homeassistant.const import CONF_DEVICE_ID, CONF_NAME from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.const import CONF_DEVICE_ID, CONF_NAME - from .const import ( - DOMAIN, - ATTR_VOC, ATTR_AQI_LEVEL, ATTR_AQI_POLLUTANT, + ATTR_VOC, DISPATCHER_KAITERRA, + DOMAIN, ) diff --git a/homeassistant/components/kaiterra/api_data.py b/homeassistant/components/kaiterra/api_data.py index 81e28438d56..e0f4d817e03 100644 --- a/homeassistant/components/kaiterra/api_data.py +++ b/homeassistant/components/kaiterra/api_data.py @@ -1,21 +1,17 @@ """Data for all Kaiterra devices.""" +import asyncio from logging import getLogger -import asyncio - -import async_timeout - from aiohttp.client_exceptions import ClientResponseError +import async_timeout +from kaiterra_async_client import AQIStandard, KaiterraAPIClient, Units -from kaiterra_async_client import KaiterraAPIClient, AQIStandard, Units - +from homeassistant.const import CONF_API_KEY, CONF_DEVICE_ID, CONF_DEVICES, CONF_TYPE from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.const import CONF_API_KEY, CONF_DEVICES, CONF_DEVICE_ID, CONF_TYPE - from .const import ( - AQI_SCALE, AQI_LEVEL, + AQI_SCALE, CONF_AQI_STANDARD, CONF_PREFERRED_UNITS, DISPATCHER_KAITERRA, @@ -60,9 +56,10 @@ class KaiterraApiData: with async_timeout.timeout(10): data = await self._api.get_latest_sensor_readings(self._devices) except (ClientResponseError, asyncio.TimeoutError): - _LOGGER.debug("Couldn't fetch data") + _LOGGER.debug("Couldn't fetch data from Kaiterra API") self.data = {} async_dispatcher_send(self._hass, DISPATCHER_KAITERRA) + return _LOGGER.debug("New data retrieved: %s", data) @@ -102,8 +99,7 @@ class KaiterraApiData: device["aqi_pollutant"] = {"value": main_pollutant} self.data[self._devices_ids[i]] = device - - async_dispatcher_send(self._hass, DISPATCHER_KAITERRA) except IndexError as err: _LOGGER.error("Parsing error %s", err) - async_dispatcher_send(self._hass, DISPATCHER_KAITERRA) + + async_dispatcher_send(self._hass, DISPATCHER_KAITERRA) diff --git a/homeassistant/components/kaiterra/sensor.py b/homeassistant/components/kaiterra/sensor.py index 4ff6435b64d..e86d6f7d836 100644 --- a/homeassistant/components/kaiterra/sensor.py +++ b/homeassistant/components/kaiterra/sensor.py @@ -1,11 +1,9 @@ """Support for Kaiterra Temperature ahn Humidity Sensors.""" +from homeassistant.const import CONF_DEVICE_ID, CONF_NAME, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from homeassistant.helpers.dispatcher import async_dispatcher_connect - -from homeassistant.const import CONF_DEVICE_ID, CONF_NAME, TEMP_CELSIUS, TEMP_FAHRENHEIT - -from .const import DOMAIN, DISPATCHER_KAITERRA +from .const import DISPATCHER_KAITERRA, DOMAIN SENSORS = [ {"name": "Temperature", "prop": "rtemp", "device_class": "temperature"}, diff --git a/homeassistant/components/keenetic_ndms2/manifest.json b/homeassistant/components/keenetic_ndms2/manifest.json index 41e45a9e578..4613d2d9608 100644 --- a/homeassistant/components/keenetic_ndms2/manifest.json +++ b/homeassistant/components/keenetic_ndms2/manifest.json @@ -3,8 +3,10 @@ "name": "Keenetic ndms2", "documentation": "https://www.home-assistant.io/integrations/keenetic_ndms2", "requirements": [ - "ndms2_client==0.0.9" + "ndms2_client==0.0.10" ], "dependencies": [], - "codeowners": [] + "codeowners": [ + "@foxel" + ] } diff --git a/homeassistant/components/keyboard/__init__.py b/homeassistant/components/keyboard/__init__.py index 39725eec86b..0c5acf5b593 100644 --- a/homeassistant/components/keyboard/__init__.py +++ b/homeassistant/components/keyboard/__init__.py @@ -1,4 +1,5 @@ """Support to emulate keyboard presses on host machine.""" +from pykeyboard import PyKeyboard # pylint: disable=import-error import voluptuous as vol from homeassistant.const import ( @@ -17,9 +18,8 @@ TAP_KEY_SCHEMA = vol.Schema({}) def setup(hass, config): """Listen for keyboard events.""" - import pykeyboard # pylint: disable=import-error - keyboard = pykeyboard.PyKeyboard() + keyboard = PyKeyboard() keyboard.special_key_assignment() hass.services.register( diff --git a/homeassistant/components/kira/__init__.py b/homeassistant/components/kira/__init__.py index 77f91a50dfa..8948fbd0b8f 100644 --- a/homeassistant/components/kira/__init__.py +++ b/homeassistant/components/kira/__init__.py @@ -2,11 +2,13 @@ import logging import os +import pykira import voluptuous as vol from voluptuous.error import Error as VoluptuousError import yaml from homeassistant.const import ( + CONF_CODE, CONF_DEVICE, CONF_HOST, CONF_NAME, @@ -15,7 +17,6 @@ from homeassistant.const import ( CONF_TYPE, EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN, - CONF_CODE, ) from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv @@ -93,8 +94,6 @@ def load_codes(path): def setup(hass, config): """Set up the KIRA component.""" - import pykira - sensors = config.get(DOMAIN, {}).get(CONF_SENSORS, []) remotes = config.get(DOMAIN, {}).get(CONF_REMOTES, []) # If no sensors or remotes were specified, add a sensor diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index 5d0c4be3d07..00d5d18f013 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -2,6 +2,11 @@ import logging import voluptuous as vol +from xknx import XKNX +from xknx.devices import ActionCallback, DateTime, DateTimeBroadcastType, ExposeSensor +from xknx.exceptions import XKNXException +from xknx.io import DEFAULT_MCAST_PORT, ConnectionConfig, ConnectionType +from xknx.knx import AddressFilter, DPTArray, DPTBinary, GroupAddress, Telegram from homeassistant.const import ( CONF_ENTITY_ID, @@ -90,13 +95,10 @@ SERVICE_KNX_SEND_SCHEMA = vol.Schema( async def async_setup(hass, config): """Set up the KNX component.""" - from xknx.exceptions import XKNXException - try: hass.data[DATA_KNX] = KNXModule(hass, config) hass.data[DATA_KNX].async_create_exposures() await hass.data[DATA_KNX].start() - except XKNXException as ex: _LOGGER.warning("Can't connect to KNX interface: %s", ex) hass.components.persistent_notification.async_create( @@ -157,8 +159,6 @@ class KNXModule: def init_xknx(self): """Initialize of KNX object.""" - from xknx import XKNX - self.xknx = XKNX( config=self.config_file(), loop=self.hass.loop, @@ -198,8 +198,6 @@ class KNXModule: def connection_config_routing(self): """Return the connection_config if routing is configured.""" - from xknx.io import ConnectionConfig, ConnectionType - local_ip = self.config[DOMAIN][CONF_KNX_ROUTING].get(CONF_KNX_LOCAL_IP) return ConnectionConfig( connection_type=ConnectionType.ROUTING, local_ip=local_ip @@ -207,8 +205,6 @@ class KNXModule: def connection_config_tunneling(self): """Return the connection_config if tunneling is configured.""" - from xknx.io import ConnectionConfig, ConnectionType, DEFAULT_MCAST_PORT - gateway_ip = self.config[DOMAIN][CONF_KNX_TUNNELING].get(CONF_HOST) gateway_port = self.config[DOMAIN][CONF_KNX_TUNNELING].get(CONF_PORT) local_ip = self.config[DOMAIN][CONF_KNX_TUNNELING].get(CONF_KNX_LOCAL_IP) @@ -224,8 +220,6 @@ class KNXModule: def connection_config_auto(self): """Return the connection_config if auto is configured.""" # pylint: disable=no-self-use - from xknx.io import ConnectionConfig - return ConnectionConfig() def register_callbacks(self): @@ -234,8 +228,6 @@ class KNXModule: CONF_KNX_FIRE_EVENT in self.config[DOMAIN] and self.config[DOMAIN][CONF_KNX_FIRE_EVENT] ): - from xknx.knx import AddressFilter - address_filters = list( map(AddressFilter, self.config[DOMAIN][CONF_KNX_FIRE_EVENT_FILTER]) ) @@ -274,8 +266,6 @@ class KNXModule: async def service_send_to_knx_bus(self, call): """Service for sending an arbitrary KNX message to the KNX bus.""" - from xknx.knx import Telegram, GroupAddress, DPTBinary, DPTArray - attr_payload = call.data.get(SERVICE_KNX_ATTR_PAYLOAD) attr_address = call.data.get(SERVICE_KNX_ATTR_ADDRESS) @@ -304,9 +294,7 @@ class KNXAutomation: script_name = "{} turn ON script".format(device.get_name()) self.script = Script(hass, action, script_name) - import xknx - - self.action = xknx.devices.ActionCallback( + self.action = ActionCallback( hass.data[DATA_KNX].xknx, self.script.async_run, hook=hook, counter=counter ) device.actions.append(self.action) @@ -325,8 +313,6 @@ class KNXExposeTime: @callback def async_register(self): """Register listener.""" - from xknx.devices import DateTime, DateTimeBroadcastType - broadcast_type_string = self.type.upper() broadcast_type = DateTimeBroadcastType[broadcast_type_string] self.device = DateTime( @@ -350,8 +336,6 @@ class KNXExposeSensor: @callback def async_register(self): """Register listener.""" - from xknx.devices import ExposeSensor - self.device = ExposeSensor( self.xknx, name=self.entity_id, diff --git a/homeassistant/components/knx/binary_sensor.py b/homeassistant/components/knx/binary_sensor.py index fbe9c4e421e..94a171d9c2a 100644 --- a/homeassistant/components/knx/binary_sensor.py +++ b/homeassistant/components/knx/binary_sensor.py @@ -1,5 +1,6 @@ """Support for KNX/IP binary sensors.""" import voluptuous as vol +from xknx.devices import BinarySensor from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME @@ -70,9 +71,8 @@ def async_add_entities_discovery(hass, discovery_info, async_add_entities): def async_add_entities_config(hass, config, async_add_entities): """Set up binary senor for KNX platform configured within platform.""" name = config[CONF_NAME] - import xknx - binary_sensor = xknx.devices.BinarySensor( + binary_sensor = BinarySensor( hass.data[DATA_KNX].xknx, name=name, group_address_state=config[CONF_STATE_ADDRESS], diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index 07aac11b972..819fb1794c3 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -1,20 +1,22 @@ """Support for KNX/IP climate devices.""" -from typing import Optional, List +from typing import List, Optional import voluptuous as vol +from xknx.devices import Climate as XknxClimate, ClimateMode as XknxClimateMode +from xknx.knx import HVACOperationMode from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice from homeassistant.components.climate.const import ( + HVAC_MODE_AUTO, + HVAC_MODE_COOL, HVAC_MODE_DRY, HVAC_MODE_FAN_ONLY, HVAC_MODE_HEAT, HVAC_MODE_OFF, - HVAC_MODE_COOL, - HVAC_MODE_AUTO, - PRESET_ECO, - PRESET_SLEEP, PRESET_AWAY, PRESET_COMFORT, + PRESET_ECO, + PRESET_SLEEP, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, ) @@ -44,6 +46,7 @@ CONF_OPERATION_MODE_COMFORT_ADDRESS = "operation_mode_comfort_address" CONF_OPERATION_MODES = "operation_modes" CONF_ON_OFF_ADDRESS = "on_off_address" CONF_ON_OFF_STATE_ADDRESS = "on_off_state_address" +CONF_ON_OFF_INVERT = "on_off_invert" CONF_MIN_TEMP = "min_temp" CONF_MAX_TEMP = "max_temp" @@ -51,6 +54,7 @@ DEFAULT_NAME = "KNX Climate" DEFAULT_SETPOINT_SHIFT_STEP = 0.5 DEFAULT_SETPOINT_SHIFT_MAX = 6 DEFAULT_SETPOINT_SHIFT_MIN = -6 +DEFAULT_ON_OFF_INVERT = False # Map KNX operation modes to HA modes. This list might not be full. OPERATION_MODES = { # Map DPT 201.105 HVAC control modes @@ -102,6 +106,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_OPERATION_MODE_COMFORT_ADDRESS): cv.string, vol.Optional(CONF_ON_OFF_ADDRESS): cv.string, vol.Optional(CONF_ON_OFF_STATE_ADDRESS): cv.string, + vol.Optional(CONF_ON_OFF_INVERT, default=DEFAULT_ON_OFF_INVERT): cv.boolean, vol.Optional(CONF_OPERATION_MODES): vol.All( cv.ensure_list, [vol.In(OPERATION_MODES)] ), @@ -132,9 +137,7 @@ def async_add_entities_discovery(hass, discovery_info, async_add_entities): @callback def async_add_entities_config(hass, config, async_add_entities): """Set up climate for KNX platform configured within platform.""" - import xknx - - climate_mode = xknx.devices.ClimateMode( + climate_mode = XknxClimateMode( hass.data[DATA_KNX].xknx, name=config[CONF_NAME] + " Mode", group_address_operation_mode=config.get(CONF_OPERATION_MODE_ADDRESS), @@ -162,7 +165,7 @@ def async_add_entities_config(hass, config, async_add_entities): ) hass.data[DATA_KNX].xknx.devices.add(climate_mode) - climate = xknx.devices.Climate( + climate = XknxClimate( hass.data[DATA_KNX].xknx, name=config[CONF_NAME], group_address_temperature=config[CONF_TEMPERATURE_ADDRESS], @@ -182,6 +185,7 @@ def async_add_entities_config(hass, config, async_add_entities): min_temp=config.get(CONF_MIN_TEMP), max_temp=config.get(CONF_MAX_TEMP), mode=climate_mode, + on_off_invert=config[CONF_ON_OFF_INVERT], ) hass.data[DATA_KNX].xknx.devices.add(climate) @@ -298,8 +302,6 @@ class KNXClimate(ClimateDevice): elif self.device.supports_on_off and hvac_mode == HVAC_MODE_HEAT: await self.device.turn_on() elif self.device.mode.supports_operation_mode: - from xknx.knx import HVACOperationMode - knx_operation_mode = HVACOperationMode(OPERATION_MODES_INV.get(hvac_mode)) await self.device.mode.set_operation_mode(knx_operation_mode) await self.async_update_ha_state() @@ -333,8 +335,6 @@ class KNXClimate(ClimateDevice): This method must be run in the event loop and returns a coroutine. """ if self.device.mode.supports_operation_mode: - from xknx.knx import HVACOperationMode - knx_operation_mode = HVACOperationMode(PRESET_MODES_INV.get(preset_mode)) await self.device.mode.set_operation_mode(knx_operation_mode) await self.async_update_ha_state() diff --git a/homeassistant/components/knx/cover.py b/homeassistant/components/knx/cover.py index 9af7c11678a..976d1286c9f 100644 --- a/homeassistant/components/knx/cover.py +++ b/homeassistant/components/knx/cover.py @@ -1,5 +1,6 @@ """Support for KNX/IP covers.""" import voluptuous as vol +from xknx.devices import Cover as XknxCover from homeassistant.components.cover import ( ATTR_POSITION, @@ -74,9 +75,7 @@ def async_add_entities_discovery(hass, discovery_info, async_add_entities): @callback def async_add_entities_config(hass, config, async_add_entities): """Set up cover for KNX platform configured within platform.""" - import xknx - - cover = xknx.devices.Cover( + cover = XknxCover( hass.data[DATA_KNX].xknx, name=config[CONF_NAME], group_address_long=config.get(CONF_MOVE_LONG_ADDRESS), diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py index 71a82c6df2a..81bf4ad3c83 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -2,6 +2,7 @@ from enum import Enum import voluptuous as vol +from xknx.devices import Light as XknxLight from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -98,8 +99,6 @@ def async_add_entities_discovery(hass, discovery_info, async_add_entities): @callback def async_add_entities_config(hass, config, async_add_entities): """Set up light for KNX platform configured within platform.""" - import xknx - group_address_tunable_white = None group_address_tunable_white_state = None group_address_color_temp = None @@ -111,7 +110,7 @@ def async_add_entities_config(hass, config, async_add_entities): group_address_tunable_white = config.get(CONF_COLOR_TEMP_ADDRESS) group_address_tunable_white_state = config.get(CONF_COLOR_TEMP_STATE_ADDRESS) - light = xknx.devices.Light( + light = XknxLight( hass.data[DATA_KNX].xknx, name=config[CONF_NAME], group_address_switch=config[CONF_ADDRESS], diff --git a/homeassistant/components/knx/notify.py b/homeassistant/components/knx/notify.py index b83edb89eb1..64d513b8624 100644 --- a/homeassistant/components/knx/notify.py +++ b/homeassistant/components/knx/notify.py @@ -1,5 +1,6 @@ """Support for KNX/IP notification services.""" import voluptuous as vol +from xknx.devices import Notification as XknxNotification from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService from homeassistant.const import CONF_ADDRESS, CONF_NAME @@ -42,9 +43,7 @@ def async_get_service_discovery(hass, discovery_info): @callback def async_get_service_config(hass, config): """Set up notification for KNX platform configured within platform.""" - import xknx - - notification = xknx.devices.Notification( + notification = XknxNotification( hass.data[DATA_KNX].xknx, name=config[CONF_NAME], group_address=config[CONF_ADDRESS], diff --git a/homeassistant/components/knx/scene.py b/homeassistant/components/knx/scene.py index d635384092f..c8c6ac2bcfb 100644 --- a/homeassistant/components/knx/scene.py +++ b/homeassistant/components/knx/scene.py @@ -1,5 +1,6 @@ """Support for KNX scenes.""" import voluptuous as vol +from xknx.devices import Scene as XknxScene from homeassistant.components.scene import CONF_PLATFORM, Scene from homeassistant.const import CONF_ADDRESS, CONF_NAME @@ -42,9 +43,7 @@ def async_add_entities_discovery(hass, discovery_info, async_add_entities): @callback def async_add_entities_config(hass, config, async_add_entities): """Set up scene for KNX platform configured within platform.""" - import xknx - - scene = xknx.devices.Scene( + scene = XknxScene( hass.data[DATA_KNX].xknx, name=config[CONF_NAME], group_address=config[CONF_ADDRESS], diff --git a/homeassistant/components/knx/sensor.py b/homeassistant/components/knx/sensor.py index 9a19ba91b7a..a0a0f6ea18d 100644 --- a/homeassistant/components/knx/sensor.py +++ b/homeassistant/components/knx/sensor.py @@ -1,5 +1,6 @@ """Support for KNX/IP sensors.""" import voluptuous as vol +from xknx.devices import Sensor as XknxSensor from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_NAME, CONF_TYPE @@ -44,9 +45,7 @@ def async_add_entities_discovery(hass, discovery_info, async_add_entities): @callback def async_add_entities_config(hass, config, async_add_entities): """Set up sensor for KNX platform configured within platform.""" - import xknx - - sensor = xknx.devices.Sensor( + sensor = XknxSensor( hass.data[DATA_KNX].xknx, name=config[CONF_NAME], group_address_state=config[CONF_STATE_ADDRESS], diff --git a/homeassistant/components/knx/switch.py b/homeassistant/components/knx/switch.py index 72a5b5dcdd7..e9a0df5c983 100644 --- a/homeassistant/components/knx/switch.py +++ b/homeassistant/components/knx/switch.py @@ -1,5 +1,6 @@ """Support for KNX/IP switches.""" import voluptuous as vol +from xknx.devices import Switch as XknxSwitch from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import CONF_ADDRESS, CONF_NAME @@ -41,9 +42,7 @@ def async_add_entities_discovery(hass, discovery_info, async_add_entities): @callback def async_add_entities_config(hass, config, async_add_entities): """Set up switch for KNX platform configured within platform.""" - import xknx - - switch = xknx.devices.Switch( + switch = XknxSwitch( hass.data[DATA_KNX].xknx, name=config[CONF_NAME], group_address=config[CONF_ADDRESS], diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index 9f0aab6c00c..9b2ba01e90a 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -7,6 +7,10 @@ import socket import urllib import aiohttp +import jsonrpc_base +import jsonrpc_async +import jsonrpc_websocket + import voluptuous as vol from homeassistant.components.kodi import SERVICE_CALL_METHOD @@ -231,8 +235,6 @@ def cmd(func): @wraps(func) async def wrapper(obj, *args, **kwargs): """Wrap all command methods.""" - import jsonrpc_base - try: await func(obj, *args, **kwargs) except jsonrpc_base.jsonrpc.TransportError as exc: @@ -268,9 +270,6 @@ class KodiDevice(MediaPlayerDevice): unique_id=None, ): """Initialize the Kodi device.""" - import jsonrpc_async - import jsonrpc_websocket - self.hass = hass self._name = name self._unique_id = unique_id @@ -389,8 +388,6 @@ class KodiDevice(MediaPlayerDevice): async def _get_players(self): """Return the active player objects or None.""" - import jsonrpc_base - try: return await self.server.Player.GetActivePlayers() except jsonrpc_base.jsonrpc.TransportError: @@ -420,8 +417,6 @@ class KodiDevice(MediaPlayerDevice): async def async_ws_connect(self): """Connect to Kodi via websocket protocol.""" - import jsonrpc_base - try: ws_loop_future = await self._ws_server.ws_connect() except jsonrpc_base.jsonrpc.TransportError: @@ -801,8 +796,6 @@ class KodiDevice(MediaPlayerDevice): async def async_call_method(self, method, **kwargs): """Run Kodi JSONRPC API method with params.""" - import jsonrpc_base - _LOGGER.debug("Run API method %s, kwargs=%s", method, kwargs) result_ok = False try: @@ -850,8 +843,6 @@ class KodiDevice(MediaPlayerDevice): All the albums of an artist can be added with media_name="ALL" """ - import jsonrpc_base - params = {"playlistid": 0} if media_type == "SONG": if media_id is None: diff --git a/homeassistant/components/kodi/notify.py b/homeassistant/components/kodi/notify.py index 41dfc42b5de..1072cf1b732 100644 --- a/homeassistant/components/kodi/notify.py +++ b/homeassistant/components/kodi/notify.py @@ -2,6 +2,8 @@ import logging import aiohttp +import jsonrpc_async + import voluptuous as vol from homeassistant.const import ( @@ -77,8 +79,6 @@ class KodiNotificationService(BaseNotificationService): def __init__(self, hass, url, auth=None): """Initialize the service.""" - import jsonrpc_async - self._url = url kwargs = {"timeout": DEFAULT_TIMEOUT, "session": async_get_clientsession(hass)} @@ -90,8 +90,6 @@ class KodiNotificationService(BaseNotificationService): async def async_send_message(self, message="", **kwargs): """Send a message to Kodi.""" - import jsonrpc_async - try: data = kwargs.get(ATTR_DATA) or {} diff --git a/homeassistant/components/konnected/__init__.py b/homeassistant/components/konnected/__init__.py index 4cc872fb78b..624d359e154 100644 --- a/homeassistant/components/konnected/__init__.py +++ b/homeassistant/components/konnected/__init__.py @@ -4,59 +4,58 @@ import hmac import json import logging -import voluptuous as vol - from aiohttp.hdrs import AUTHORIZATION from aiohttp.web import Request, Response +import konnected +import voluptuous as vol from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA from homeassistant.components.discovery import SERVICE_KONNECTED from homeassistant.components.http import HomeAssistantView from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_STATE, + CONF_ACCESS_TOKEN, + CONF_BINARY_SENSORS, + CONF_DEVICES, + CONF_HOST, + CONF_ID, + CONF_NAME, + CONF_PIN, + CONF_PORT, + CONF_SENSORS, + CONF_SWITCHES, + CONF_TYPE, + CONF_ZONE, EVENT_HOMEASSISTANT_START, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, HTTP_UNAUTHORIZED, - CONF_DEVICES, - CONF_BINARY_SENSORS, - CONF_SENSORS, - CONF_SWITCHES, - CONF_HOST, - CONF_PORT, - CONF_ID, - CONF_NAME, - CONF_TYPE, - CONF_PIN, - CONF_ZONE, - CONF_ACCESS_TOKEN, - ATTR_ENTITY_ID, - ATTR_STATE, STATE_ON, ) +from homeassistant.helpers import config_validation as cv, discovery from homeassistant.helpers.dispatcher import dispatcher_send -from homeassistant.helpers import discovery -from homeassistant.helpers import config_validation as cv from .const import ( CONF_ACTIVATION, CONF_API_HOST, + CONF_BLINK, + CONF_DHT_SENSORS, + CONF_DISCOVERY, + CONF_DS18B20_SENSORS, + CONF_INVERSE, CONF_MOMENTARY, CONF_PAUSE, CONF_POLL_INTERVAL, CONF_REPEAT, - CONF_INVERSE, - CONF_BLINK, - CONF_DISCOVERY, - CONF_DHT_SENSORS, - CONF_DS18B20_SENSORS, DOMAIN, - STATE_LOW, - STATE_HIGH, - PIN_TO_ZONE, - ZONE_TO_PIN, ENDPOINT_ROOT, - UPDATE_ENDPOINT, + PIN_TO_ZONE, SIGNAL_SENSOR_UPDATE, + STATE_HIGH, + STATE_LOW, + UPDATE_ENDPOINT, + ZONE_TO_PIN, ) from .handlers import HANDLERS @@ -141,8 +140,6 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass, config): """Set up the Konnected platform.""" - import konnected - cfg = config.get(DOMAIN) if cfg is None: cfg = {} @@ -336,8 +333,6 @@ class DiscoveredDevice: self.host = host self.port = port - import konnected - self.client = konnected.Client(host, str(port)) self.status = self.client.get_status() diff --git a/homeassistant/components/konnected/handlers.py b/homeassistant/components/konnected/handlers.py index a355cabba56..a8914853e84 100644 --- a/homeassistant/components/konnected/handlers.py +++ b/homeassistant/components/konnected/handlers.py @@ -1,16 +1,16 @@ """Handle Konnected messages.""" import logging -from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.util import decorator from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_STATE, - DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, ) +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.util import decorator -from .const import CONF_INVERSE, SIGNAL_SENSOR_UPDATE, SIGNAL_DS18B20_NEW +from .const import CONF_INVERSE, SIGNAL_DS18B20_NEW, SIGNAL_SENSOR_UPDATE _LOGGER = logging.getLogger(__name__) HANDLERS = decorator.Registry() diff --git a/homeassistant/components/lastfm/sensor.py b/homeassistant/components/lastfm/sensor.py index 736792aefd8..68d727626cf 100644 --- a/homeassistant/components/lastfm/sensor.py +++ b/homeassistant/components/lastfm/sensor.py @@ -2,10 +2,12 @@ import logging import re +import pylast as lastfm +from pylast import WSError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_API_KEY, ATTR_ATTRIBUTION +from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -30,9 +32,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Last.fm sensor platform.""" - import pylast as lastfm - from pylast import WSError - api_key = config[CONF_API_KEY] users = config.get(CONF_USERS) @@ -53,11 +52,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class LastfmSensor(Entity): """A class for the Last.fm account.""" - def __init__(self, user, lastfm): + def __init__(self, user, lastfm_api): """Initialize the sensor.""" - self._user = lastfm.get_user(user) + self._user = lastfm_api.get_user(user) self._name = user - self._lastfm = lastfm + self._lastfm = lastfm_api self._state = "Not Scrobbling" self._playcount = None self._lastplayed = None diff --git a/homeassistant/components/lg_soundbar/media_player.py b/homeassistant/components/lg_soundbar/media_player.py index 5c98f86a2bc..30cfbf17074 100644 --- a/homeassistant/components/lg_soundbar/media_player.py +++ b/homeassistant/components/lg_soundbar/media_player.py @@ -1,14 +1,15 @@ """Support for LG soundbars.""" import logging +import temescal + from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( + SUPPORT_SELECT_SOUND_MODE, SUPPORT_SELECT_SOURCE, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - SUPPORT_SELECT_SOUND_MODE, ) - from homeassistant.const import STATE_ON _LOGGER = logging.getLogger(__name__) @@ -32,8 +33,6 @@ class LGDevice(MediaPlayerDevice): def __init__(self, discovery_info): """Initialize the LG speakers.""" - import temescal - host = discovery_info.get("host") port = discovery_info.get("port") @@ -140,7 +139,6 @@ class LGDevice(MediaPlayerDevice): @property def sound_mode(self): """Return the current sound mode.""" - import temescal if self._equaliser == -1: return "" @@ -149,8 +147,6 @@ class LGDevice(MediaPlayerDevice): @property def sound_mode_list(self): """Return the available sound modes.""" - import temescal - modes = [] for equaliser in self._equalisers: modes.append(temescal.equalisers[equaliser]) @@ -159,8 +155,6 @@ class LGDevice(MediaPlayerDevice): @property def source(self): """Return the current input source.""" - import temescal - if self._function == -1: return "" return temescal.functions[self._function] @@ -168,8 +162,6 @@ class LGDevice(MediaPlayerDevice): @property def source_list(self): """List of available input sources.""" - import temescal - sources = [] for function in self._functions: sources.append(temescal.functions[function]) @@ -191,12 +183,8 @@ class LGDevice(MediaPlayerDevice): def select_source(self, source): """Select input source.""" - import temescal - self._device.set_func(temescal.functions.index(source)) def select_sound_mode(self, sound_mode): """Set Sound Mode for Receiver..""" - import temescal - self._device.set_eq(temescal.equalisers.index(sound_mode)) diff --git a/homeassistant/components/life360/.translations/ru.json b/homeassistant/components/life360/.translations/ru.json index d033da4bae7..eba3a47ead8 100644 --- a/homeassistant/components/life360/.translations/ru.json +++ b/homeassistant/components/life360/.translations/ru.json @@ -1,16 +1,16 @@ { "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", + "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." }, "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_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", + "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\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." }, "step": { diff --git a/homeassistant/components/lifx/__init__.py b/homeassistant/components/lifx/__init__.py index f4ae2c4030a..6e921a59afe 100644 --- a/homeassistant/components/lifx/__init__.py +++ b/homeassistant/components/lifx/__init__.py @@ -1,12 +1,12 @@ """Support for LIFX.""" import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant import config_entries -from homeassistant.const import CONF_PORT from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN -from .const import DOMAIN +from homeassistant.const import CONF_PORT +import homeassistant.helpers.config_validation as cv +from .const import DOMAIN CONF_SERVER = "server" CONF_BROADCAST = "broadcast" diff --git a/homeassistant/components/lifx/config_flow.py b/homeassistant/components/lifx/config_flow.py index b324dc0cad8..71fe7247c12 100644 --- a/homeassistant/components/lifx/config_flow.py +++ b/homeassistant/components/lifx/config_flow.py @@ -1,13 +1,14 @@ """Config flow flow LIFX.""" -from homeassistant.helpers import config_entry_flow +import aiolifx + from homeassistant import config_entries +from homeassistant.helpers import config_entry_flow + from .const import DOMAIN async def _async_has_devices(hass): """Return if there are devices that can be discovered.""" - import aiolifx - lifx_ip_addresses = await aiolifx.LifxScan(hass.loop).scan() return len(lifx_ip_addresses) > 0 diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index d183dcb0fa2..50e36e8db0a 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -6,6 +6,8 @@ import logging import math import sys +import aiolifx as aiolifx_module +import aiolifx_effects as aiolifx_effects_module import voluptuous as vol from homeassistant import util @@ -151,15 +153,11 @@ LIFX_EFFECT_STOP_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_id def aiolifx(): """Return the aiolifx module.""" - import aiolifx as aiolifx_module - return aiolifx_module def aiolifx_effects(): """Return the aiolifx_effects module.""" - import aiolifx_effects as aiolifx_effects_module - return aiolifx_effects_module diff --git a/homeassistant/components/lifx_legacy/light.py b/homeassistant/components/lifx_legacy/light.py index 78a333018f9..8f767a2f559 100644 --- a/homeassistant/components/lifx_legacy/light.py +++ b/homeassistant/components/lifx_legacy/light.py @@ -9,6 +9,7 @@ https://home-assistant.io/components/light.lifx/ """ import logging +import liffylights import voluptuous as vol from homeassistant.components.light import ( @@ -16,19 +17,19 @@ from homeassistant.components.light import ( ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION, + PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, - SUPPORT_COLOR_TEMP, SUPPORT_COLOR, + SUPPORT_COLOR_TEMP, SUPPORT_TRANSITION, Light, - PLATFORM_SCHEMA, -) -from homeassistant.helpers.event import track_time_change -from homeassistant.util.color import ( - color_temperature_mired_to_kelvin, - color_temperature_kelvin_to_mired, ) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import track_time_change +from homeassistant.util.color import ( + color_temperature_kelvin_to_mired, + color_temperature_mired_to_kelvin, +) _LOGGER = logging.getLogger(__name__) @@ -71,8 +72,6 @@ class LIFX: def __init__(self, add_entities_callback, server_addr=None, broadcast_addr=None): """Initialize the light.""" - import liffylights - self._devices = [] self._add_entities_callback = add_entities_callback diff --git a/homeassistant/components/light/.translations/fr.json b/homeassistant/components/light/.translations/fr.json index fd30e931718..4a1dc82bbd6 100644 --- a/homeassistant/components/light/.translations/fr.json +++ b/homeassistant/components/light/.translations/fr.json @@ -10,7 +10,7 @@ "is_on": "{entity_name} est allum\u00e9" }, "trigger_type": { - "turned_off": "{entity_name} d\u00e9sactiv\u00e9", + "turned_off": "{entity_name} est d\u00e9sactiv\u00e9", "turned_on": "{entity_name} activ\u00e9" } } diff --git a/homeassistant/components/light/.translations/lv.json b/homeassistant/components/light/.translations/lv.json new file mode 100644 index 00000000000..7668dfa5ac8 --- /dev/null +++ b/homeassistant/components/light/.translations/lv.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "trigger_type": { + "turned_off": "{entity_name} tika izsl\u0113gta", + "turned_on": "{entity_name} tika iesl\u0113gta" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/light/.translations/pl.json b/homeassistant/components/light/.translations/pl.json index 33a38fc930e..05589210dba 100644 --- a/homeassistant/components/light/.translations/pl.json +++ b/homeassistant/components/light/.translations/pl.json @@ -10,8 +10,8 @@ "is_on": "\u015bwiat\u0142o {entity_name} jest w\u0142\u0105czone" }, "trigger_type": { - "turned_off": "wy\u0142\u0105czenie {entity_name}", - "turned_on": "w\u0142\u0105czenie {entity_name}" + "turned_off": "nast\u0105pi wy\u0142\u0105czenie {entity_name}", + "turned_on": "nast\u0105pi w\u0142\u0105czenie {entity_name}" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/ru.json b/homeassistant/components/light/.translations/ru.json index a6a7994b7c3..8ca964606ae 100644 --- a/homeassistant/components/light/.translations/ru.json +++ b/homeassistant/components/light/.translations/ru.json @@ -6,12 +6,12 @@ "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c {entity_name}" }, "condition_type": { - "is_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", - "is_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + "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" }, "trigger_type": { - "turned_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435", - "turned_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435" + "turned_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f", + "turned_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f" } } } \ No newline at end of file diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index ed61d961d88..fbd908a9e45 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -292,7 +292,8 @@ async def async_setup(hass, config): preprocess_turn_on_alternatives(params) turn_lights_off, off_params = preprocess_turn_off(params) - update_tasks = [] + poll_lights = [] + change_tasks = [] for light in target_lights: light.async_set_context(service.context) @@ -305,17 +306,22 @@ async def async_setup(hass, config): preprocess_turn_on_alternatives(pars) turn_light_off, off_pars = preprocess_turn_off(pars) if turn_light_off: - await light.async_turn_off(**off_pars) + task = light.async_request_call(light.async_turn_off(**off_pars)) else: - await light.async_turn_on(**pars) + task = light.async_request_call(light.async_turn_on(**pars)) - if not light.should_poll: - continue + change_tasks.append(task) - update_tasks.append(light.async_update_ha_state(True)) + if light.should_poll: + poll_lights.append(light) - if update_tasks: - await asyncio.wait(update_tasks) + if change_tasks: + await asyncio.wait(change_tasks) + + if poll_lights: + await asyncio.wait( + [light.async_update_ha_state(True) for light in poll_lights] + ) # Listen for light on and light off service calls. hass.services.async_register( diff --git a/homeassistant/components/light/device_condition.py b/homeassistant/components/light/device_condition.py index 4abf34e6661..e87ae3bf945 100644 --- a/homeassistant/components/light/device_condition.py +++ b/homeassistant/components/light/device_condition.py @@ -1,5 +1,5 @@ """Provides device conditions for lights.""" -from typing import List +from typing import Dict, List import voluptuous as vol from homeassistant.core import HomeAssistant @@ -21,9 +21,16 @@ def async_condition_from_config( """Evaluate state based on configuration.""" if config_validation: config = CONDITION_SCHEMA(config) - return toggle_entity.async_condition_from_config(config, config_validation) + return toggle_entity.async_condition_from_config(config) -async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[dict]: +async def async_get_conditions( + hass: HomeAssistant, device_id: str +) -> List[Dict[str, str]]: """List device conditions.""" return await toggle_entity.async_get_conditions(hass, device_id, DOMAIN) + + +async def async_get_condition_capabilities(hass: HomeAssistant, config: dict) -> dict: + """List condition capabilities.""" + return await toggle_entity.async_get_condition_capabilities(hass, config) diff --git a/homeassistant/components/light/device_trigger.py b/homeassistant/components/light/device_trigger.py index 5bd5d83e1c0..432d24d3c14 100644 --- a/homeassistant/components/light/device_trigger.py +++ b/homeassistant/components/light/device_trigger.py @@ -32,6 +32,6 @@ async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: return await toggle_entity.async_get_triggers(hass, device_id, DOMAIN) -async def async_get_trigger_capabilities(hass: HomeAssistant, trigger: dict) -> dict: +async def async_get_trigger_capabilities(hass: HomeAssistant, config: dict) -> dict: """List trigger capabilities.""" - return await toggle_entity.async_get_trigger_capabilities(hass, trigger) + return await toggle_entity.async_get_trigger_capabilities(hass, config) diff --git a/homeassistant/components/light/reproduce_state.py b/homeassistant/components/light/reproduce_state.py new file mode 100644 index 00000000000..ae618f7a8ef --- /dev/null +++ b/homeassistant/components/light/reproduce_state.py @@ -0,0 +1,94 @@ +"""Reproduce an Light state.""" +import asyncio +import logging +from types import MappingProxyType +from typing import Iterable, Optional + +from homeassistant.const import ( + ATTR_ENTITY_ID, + STATE_ON, + STATE_OFF, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.core import Context, State +from homeassistant.helpers.typing import HomeAssistantType + +from . import ( + DOMAIN, + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, + ATTR_EFFECT, + ATTR_HS_COLOR, + ATTR_RGB_COLOR, + ATTR_WHITE_VALUE, + ATTR_XY_COLOR, +) + +_LOGGER = logging.getLogger(__name__) + +VALID_STATES = {STATE_ON, STATE_OFF} +ATTR_GROUP = [ATTR_BRIGHTNESS, ATTR_EFFECT, ATTR_WHITE_VALUE] +COLOR_GROUP = [ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_RGB_COLOR, ATTR_XY_COLOR] + + +async def _async_reproduce_state( + hass: HomeAssistantType, state: State, context: Optional[Context] = None +) -> None: + """Reproduce a single state.""" + cur_state = hass.states.get(state.entity_id) + + if cur_state is None: + _LOGGER.warning("Unable to find entity %s", state.entity_id) + return + + if state.state not in VALID_STATES: + _LOGGER.warning( + "Invalid state specified for %s: %s", state.entity_id, state.state + ) + return + + # Return if we are already at the right state. + if cur_state.state == state.state and all( + check_attr_equal(cur_state.attributes, state.attributes, attr) + for attr in ATTR_GROUP + COLOR_GROUP + ): + return + + service_data = {ATTR_ENTITY_ID: state.entity_id} + + if state.state == STATE_ON: + service = SERVICE_TURN_ON + for attr in ATTR_GROUP: + # All attributes that are not colors + if attr in state.attributes: + service_data[attr] = state.attributes[attr] + + for color_attr in COLOR_GROUP: + # Choose the first color that is specified + if color_attr in state.attributes: + service_data[color_attr] = state.attributes[color_attr] + break + + elif state.state == STATE_OFF: + service = SERVICE_TURN_OFF + + await hass.services.async_call( + DOMAIN, service, service_data, context=context, blocking=True + ) + + +async def async_reproduce_states( + hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None +) -> None: + """Reproduce Light states.""" + await asyncio.gather( + *(_async_reproduce_state(hass, state, context) for state in states) + ) + + +def check_attr_equal( + attr1: MappingProxyType, attr2: MappingProxyType, attr_str: str +) -> bool: + """Return true if the given attributes are equal.""" + return attr1.get(attr_str) == attr2.get(attr_str) diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index ef944d75efc..9173f79f964 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -5,22 +5,22 @@ turn_on: fields: entity_id: description: Name(s) of entities to turn on - example: 'light.kitchen' + example: "light.kitchen" transition: description: Duration in seconds it takes to get to next state example: 60 rgb_color: description: Color for the light in RGB-format. - example: '[255, 100, 100]' + example: "[255, 100, 100]" color_name: description: A human readable color name. - example: 'red' + example: "red" hs_color: description: Color for the light in hue/sat format. Hue is 0-360 and Sat is 0-100. - example: '[300, 70]' + example: "[300, 70]" xy_color: description: Color for the light in XY-format. - example: '[0.52, 0.43]' + example: "[0.52, 0.43]" color_temp: description: Color temperature for the light in mireds. example: 250 @@ -29,7 +29,7 @@ turn_on: example: 4000 white_value: description: Number between 0..255 indicating level of white. - example: '250' + example: "250" brightness: description: Number between 0..255 indicating brightness, where 0 turns the light off, 1 is the minimum brightness and 255 is the maximum brightness supported by the light. example: 120 @@ -40,12 +40,14 @@ turn_on: description: Name of a light profile to use. example: relax flash: - description: If the light should flash. + description: If the light should flash. Valid values are short and long. + example: short values: - short - long effect: description: Light effect. + example: random values: - colorloop - random @@ -55,12 +57,13 @@ turn_off: fields: entity_id: description: Name(s) of entities to turn off. - example: 'light.kitchen' + example: "light.kitchen" transition: description: Duration in seconds it takes to get to next state. example: 60 flash: - description: If the light should flash. + description: If the light should flash. Valid values are short and long. + example: short values: - short - long @@ -68,23 +71,69 @@ turn_off: toggle: description: Toggles a light. fields: - '...': - description: All turn_on parameters can be used. + entity_id: + description: Name(s) of entities to turn on + example: "light.kitchen" + transition: + description: Duration in seconds it takes to get to next state + example: 60 + rgb_color: + description: Color for the light in RGB-format. + example: "[255, 100, 100]" + color_name: + description: A human readable color name. + example: "red" + hs_color: + description: Color for the light in hue/sat format. Hue is 0-360 and Sat is 0-100. + example: "[300, 70]" + xy_color: + description: Color for the light in XY-format. + example: "[0.52, 0.43]" + color_temp: + description: Color temperature for the light in mireds. + example: 250 + kelvin: + description: Color temperature for the light in Kelvin. + example: 4000 + white_value: + description: Number between 0..255 indicating level of white. + example: "250" + brightness: + description: Number between 0..255 indicating brightness, where 0 turns the light off, 1 is the minimum brightness and 255 is the maximum brightness supported by the light. + example: 120 + brightness_pct: + description: Number between 0..100 indicating percentage of full brightness, where 0 turns the light off, 1 is the minimum brightness and 100 is the maximum brightness supported by the light. + example: 47 + profile: + description: Name of a light profile to use. + example: relax + flash: + description: If the light should flash. Valid values are short and long. + example: short + values: + - short + - long + effect: + description: Light effect. + example: random + 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' - '...': + 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]' + example: "[0,5]" transition: description: Duration in seconds it takes to get to the final state. example: 10 @@ -97,19 +146,19 @@ lifx_effect_pulse: fields: entity_id: description: Name(s) of entities to run the effect on. - example: 'light.kitchen' + example: "light.kitchen" mode: - description: 'Decides how colors are changed. Possible values: blink, breathe, ping, strobe, solid.' + 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' + example: "red" rgb_color: description: The temporary color in RGB-format. - example: '[255, 100, 100]' + example: "[255, 100, 100]" period: description: Duration of the effect in seconds (default 1.0). example: 3 @@ -125,7 +174,7 @@ lifx_effect_colorloop: fields: entity_id: description: Name(s) of entities to run the effect on. - example: 'light.disco1, light.disco2, light.disco3' + 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 @@ -147,14 +196,14 @@ lifx_effect_stop: fields: entity_id: description: Name(s) of entities to stop effects on. Leave out to stop effects everywhere. - example: 'light.bedroom' + 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' + example: "light.xiaomi_miio" scene: description: Number of the fixed scene, between 1 and 4. example: 1 @@ -164,7 +213,7 @@ xiaomi_miio_set_delayed_turn_off: fields: entity_id: description: Name of the light entity. - example: 'light.xiaomi_miio' + 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/linky/.translations/pl.json b/homeassistant/components/linky/.translations/pl.json index a4f68fa8687..d4fa7ee4d11 100644 --- a/homeassistant/components/linky/.translations/pl.json +++ b/homeassistant/components/linky/.translations/pl.json @@ -14,7 +14,7 @@ "user": { "data": { "password": "Has\u0142o", - "username": "E-mail" + "username": "Adres e-mail" }, "description": "Wprowad\u017a dane uwierzytelniaj\u0105ce", "title": "Linky" diff --git a/homeassistant/components/linky/.translations/ru.json b/homeassistant/components/linky/.translations/ru.json index b569cce9239..463343490a7 100644 --- a/homeassistant/components/linky/.translations/ru.json +++ b/homeassistant/components/linky/.translations/ru.json @@ -4,11 +4,11 @@ "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." }, "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)", + "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.", - "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" + "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": { "user": { @@ -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\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", "title": "Linky" } }, diff --git a/homeassistant/components/linky/__init__.py b/homeassistant/components/linky/__init__.py index a7f3d7bb03e..1d382b43525 100644 --- a/homeassistant/components/linky/__init__.py +++ b/homeassistant/components/linky/__init__.py @@ -47,9 +47,12 @@ async def async_setup(hass, config): async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): """Set up Linky sensors.""" - hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, "sensor") ) - return True + + +async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): + """Unload Linky sensors.""" + return await hass.config_entries.async_forward_entry_unload(entry, "sensor") diff --git a/homeassistant/components/linode/__init__.py b/homeassistant/components/linode/__init__.py index 6f590c33e08..a18b63d7226 100644 --- a/homeassistant/components/linode/__init__.py +++ b/homeassistant/components/linode/__init__.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +import linode import voluptuous as vol from homeassistant.const import CONF_ACCESS_TOKEN @@ -35,8 +36,6 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): """Set up the Linode component.""" - import linode - conf = config[DOMAIN] access_token = conf.get(CONF_ACCESS_TOKEN) @@ -58,16 +57,12 @@ class Linode: def __init__(self, access_token): """Initialize the Linode connection.""" - import linode - self._access_token = access_token self.data = None self.manager = linode.LinodeClient(token=self._access_token) def get_node_id(self, node_name): """Get the status of a Linode Instance.""" - import linode - node_id = None try: @@ -83,8 +78,6 @@ class Linode: @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Use the data from Linode API.""" - import linode - try: self.data = self.manager.linode.get_instances() except linode.errors.ApiError as _ex: diff --git a/homeassistant/components/linux_battery/sensor.py b/homeassistant/components/linux_battery/sensor.py index 9256c3ad18d..bc02affdaed 100644 --- a/homeassistant/components/linux_battery/sensor.py +++ b/homeassistant/components/linux_battery/sensor.py @@ -2,6 +2,7 @@ import logging import os +from batinfo import Batteries import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -72,15 +73,12 @@ class LinuxBatterySensor(Entity): def __init__(self, name, battery_id, system): """Initialize the battery sensor.""" - import batinfo - - self._battery = batinfo.Batteries() + self._battery = Batteries() self._name = name self._battery_stat = None self._battery_id = battery_id - 1 self._system = system - self._unit_of_measurement = "%" @property def name(self): @@ -100,7 +98,7 @@ class LinuxBatterySensor(Entity): @property def unit_of_measurement(self): """Return the unit the value is expressed in.""" - return self._unit_of_measurement + return "%" @property def device_state_attributes(self): diff --git a/homeassistant/components/lirc/__init__.py b/homeassistant/components/lirc/__init__.py index 47814d00e9a..bfc8e455624 100644 --- a/homeassistant/components/lirc/__init__.py +++ b/homeassistant/components/lirc/__init__.py @@ -1,12 +1,13 @@ """Support for LIRC devices.""" # pylint: disable=no-member, import-error +import logging import threading import time -import logging +import lirc import voluptuous as vol -from homeassistant.const import EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START +from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP _LOGGER = logging.getLogger(__name__) @@ -23,8 +24,6 @@ CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) def setup(hass, config): """Set up the LIRC capability.""" - import lirc - # blocking=True gives unexpected behavior (multiple responses for 1 press) # also by not blocking, we allow hass to shut down the thread gracefully # on exit. @@ -61,8 +60,6 @@ class LircInterface(threading.Thread): def run(self): """Run the loop of the LIRC interface thread.""" - import lirc - _LOGGER.debug("LIRC interface thread started") while not self.stopped.isSet(): try: diff --git a/homeassistant/components/liveboxplaytv/media_player.py b/homeassistant/components/liveboxplaytv/media_player.py index c466d71c4c5..996b4f33b50 100644 --- a/homeassistant/components/liveboxplaytv/media_player.py +++ b/homeassistant/components/liveboxplaytv/media_player.py @@ -2,6 +2,8 @@ from datetime import timedelta import logging +from liveboxplaytv import LiveboxPlayTv +import pyteleloisirs import requests import voluptuous as vol @@ -85,7 +87,6 @@ class LiveboxPlayTvDevice(MediaPlayerDevice): def __init__(self, host, port, name): """Initialize the Livebox Play TV device.""" - from liveboxplaytv import LiveboxPlayTv self._client = LiveboxPlayTv(host, port) # Assume that the appliance is not muted @@ -103,7 +104,6 @@ class LiveboxPlayTvDevice(MediaPlayerDevice): async def async_update(self): """Retrieve the latest data.""" - import pyteleloisirs try: self._state = self.refresh_state() diff --git a/homeassistant/components/locative/.translations/hu.json b/homeassistant/components/locative/.translations/hu.json index e90910c29a2..3528f1c1e45 100644 --- a/homeassistant/components/locative/.translations/hu.json +++ b/homeassistant/components/locative/.translations/hu.json @@ -6,7 +6,7 @@ }, "step": { "user": { - "description": "Biztosan be szeretn\u00e9d be\u00e1ll\u00edtani a Locative Webhookot?", + "description": "Biztosan be szeretn\u00e9d \u00e1ll\u00edtani a Locative Webhook-ot?", "title": "Locative Webhook be\u00e1ll\u00edt\u00e1sa" } }, diff --git a/homeassistant/components/locative/__init__.py b/homeassistant/components/locative/__init__.py index 61e0b1f7474..ed8bcb6e7e5 100644 --- a/homeassistant/components/locative/__init__.py +++ b/homeassistant/components/locative/__init__.py @@ -127,8 +127,7 @@ async def async_unload_entry(hass, entry): """Unload a config entry.""" hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID]) hass.data[DOMAIN]["unsub_device_tracker"].pop(entry.entry_id)() - await hass.config_entries.async_forward_entry_unload(entry, DEVICE_TRACKER) - return True + return await hass.config_entries.async_forward_entry_unload(entry, DEVICE_TRACKER) # pylint: disable=invalid-name diff --git a/homeassistant/components/lock/.translations/ca.json b/homeassistant/components/lock/.translations/ca.json new file mode 100644 index 00000000000..53198a21573 --- /dev/null +++ b/homeassistant/components/lock/.translations/ca.json @@ -0,0 +1,13 @@ +{ + "device_automation": { + "action_type": { + "lock": "Bloqueja {entity_name}", + "open": "Obre {entity_name}", + "unlock": "Desbloqueja {entity_name}" + }, + "condition_type": { + "is_locked": "{entity_name} est\u00e0 bloquejat/ada", + "is_unlocked": "{entity_name} est\u00e0 desbloquejat/ada" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/da.json b/homeassistant/components/lock/.translations/da.json new file mode 100644 index 00000000000..de4f603ac43 --- /dev/null +++ b/homeassistant/components/lock/.translations/da.json @@ -0,0 +1,12 @@ +{ + "device_automation": { + "action_type": { + "lock": "L\u00e5s {entity_name}", + "open": "\u00c5ben {entity_name}" + }, + "condition_type": { + "is_locked": "{entity_name} er l\u00e5st", + "is_unlocked": "{entity_name} er l\u00e5st op" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/de.json b/homeassistant/components/lock/.translations/de.json new file mode 100644 index 00000000000..02c387ff487 --- /dev/null +++ b/homeassistant/components/lock/.translations/de.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condition_type": { + "is_locked": "{entity_name} ist gesperrt", + "is_unlocked": "{entity_name} ist entsperrt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/en.json b/homeassistant/components/lock/.translations/en.json new file mode 100644 index 00000000000..a9800eecadd --- /dev/null +++ b/homeassistant/components/lock/.translations/en.json @@ -0,0 +1,13 @@ +{ + "device_automation": { + "action_type": { + "lock": "Lock {entity_name}", + "open": "Open {entity_name}", + "unlock": "Unlock {entity_name}" + }, + "condition_type": { + "is_locked": "{entity_name} is locked", + "is_unlocked": "{entity_name} is unlocked" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/es.json b/homeassistant/components/lock/.translations/es.json new file mode 100644 index 00000000000..c6ef789e9cb --- /dev/null +++ b/homeassistant/components/lock/.translations/es.json @@ -0,0 +1,13 @@ +{ + "device_automation": { + "action_type": { + "lock": "Bloquear {entity_name}", + "open": "Abrir {entity_name}", + "unlock": "Desbloquear {entity_name}" + }, + "condition_type": { + "is_locked": "{entity_name} est\u00e1 bloqueado", + "is_unlocked": "{entity_name} est\u00e1 desbloqueado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/fr.json b/homeassistant/components/lock/.translations/fr.json new file mode 100644 index 00000000000..748a1e9290c --- /dev/null +++ b/homeassistant/components/lock/.translations/fr.json @@ -0,0 +1,13 @@ +{ + "device_automation": { + "action_type": { + "lock": "V\u00e9rouiller {entity_name}", + "open": "Ouvre {entity_name}", + "unlock": "D\u00e9verrouiller {entity_name}" + }, + "condition_type": { + "is_locked": "{entity_name} est verrouill\u00e9", + "is_unlocked": "{entity_name} est d\u00e9verrouill\u00e9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/it.json b/homeassistant/components/lock/.translations/it.json new file mode 100644 index 00000000000..f56ef71060b --- /dev/null +++ b/homeassistant/components/lock/.translations/it.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condition_type": { + "is_locked": "{entity_name} \u00e8 bloccato", + "is_unlocked": "{entity_name} \u00e8 sbloccato" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/ko.json b/homeassistant/components/lock/.translations/ko.json new file mode 100644 index 00000000000..6abd9cd60e6 --- /dev/null +++ b/homeassistant/components/lock/.translations/ko.json @@ -0,0 +1,13 @@ +{ + "device_automation": { + "action_type": { + "lock": "{entity_name} \uc7a0\uae08", + "open": "{entity_name} \uc5f4\uae30", + "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" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/lb.json b/homeassistant/components/lock/.translations/lb.json new file mode 100644 index 00000000000..90dd7e6087a --- /dev/null +++ b/homeassistant/components/lock/.translations/lb.json @@ -0,0 +1,13 @@ +{ + "device_automation": { + "action_type": { + "lock": "{entity_name} sp\u00e4ren", + "open": "{entity_name} opmaachen", + "unlock": "{entity_name} entsp\u00e4ren" + }, + "condition_type": { + "is_locked": "{entity_name} ass gespaart", + "is_unlocked": "{entity_name} ass entspaart" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/nl.json b/homeassistant/components/lock/.translations/nl.json new file mode 100644 index 00000000000..099b7308334 --- /dev/null +++ b/homeassistant/components/lock/.translations/nl.json @@ -0,0 +1,13 @@ +{ + "device_automation": { + "action_type": { + "lock": "Vergrendel {entity_name}", + "open": "Open {entity_name}", + "unlock": "Ontgrendel {entity_name}" + }, + "condition_type": { + "is_locked": "{entity_name} is vergrendeld", + "is_unlocked": "{entity_name} is ontgrendeld" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/no.json b/homeassistant/components/lock/.translations/no.json new file mode 100644 index 00000000000..28c809a82d1 --- /dev/null +++ b/homeassistant/components/lock/.translations/no.json @@ -0,0 +1,13 @@ +{ + "device_automation": { + "action_type": { + "lock": "L\u00e5s {entity_name}", + "open": "\u00c5pne {entity_name}", + "unlock": "L\u00e5s opp {entity_name}" + }, + "condition_type": { + "is_locked": "{entity_name} er l\u00e5st", + "is_unlocked": "{entity_name} er l\u00e5st opp" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/pl.json b/homeassistant/components/lock/.translations/pl.json new file mode 100644 index 00000000000..a3fe7358398 --- /dev/null +++ b/homeassistant/components/lock/.translations/pl.json @@ -0,0 +1,13 @@ +{ + "device_automation": { + "action_type": { + "lock": "zablokuj {entity_name}", + "open": "otw\u00f3rz {entity_name}", + "unlock": "odblokuj {entity_name}" + }, + "condition_type": { + "is_locked": "zamek {entity_name} jest zamkni\u0119ty", + "is_unlocked": "zamek {entity_name} jest otwarty" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/ru.json b/homeassistant/components/lock/.translations/ru.json new file mode 100644 index 00000000000..1610668721f --- /dev/null +++ b/homeassistant/components/lock/.translations/ru.json @@ -0,0 +1,13 @@ +{ + "device_automation": { + "action_type": { + "lock": "\u0417\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u0442\u044c {entity_name}", + "open": "\u041e\u0442\u043a\u0440\u044b\u0442\u044c {entity_name}", + "unlock": "\u0420\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u0442\u044c {entity_name}" + }, + "condition_type": { + "is_locked": "{entity_name} \u0432 \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_unlocked": "{entity_name} \u0432 \u0440\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/sl.json b/homeassistant/components/lock/.translations/sl.json new file mode 100644 index 00000000000..d2e32499d2e --- /dev/null +++ b/homeassistant/components/lock/.translations/sl.json @@ -0,0 +1,13 @@ +{ + "device_automation": { + "action_type": { + "lock": "Zakleni {entity_name}", + "open": "Odpri {entity_name}", + "unlock": "Odkleni {entity_name}" + }, + "condition_type": { + "is_locked": "{entity_name} je/so zaklenjen/a", + "is_unlocked": "{entity_name} je/so odklenjen/a" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/zh-Hant.json b/homeassistant/components/lock/.translations/zh-Hant.json new file mode 100644 index 00000000000..7c8abb76e16 --- /dev/null +++ b/homeassistant/components/lock/.translations/zh-Hant.json @@ -0,0 +1,13 @@ +{ + "device_automation": { + "action_type": { + "lock": "\u4e0a\u9396 {entity_name}", + "open": "\u958b\u555f {entity_name}", + "unlock": "\u89e3\u9396 {entity_name}" + }, + "condition_type": { + "is_locked": "{entity_name} \u5df2\u4e0a\u9396", + "is_unlocked": "{entity_name} \u5df2\u89e3\u9396" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lock/device_action.py b/homeassistant/components/lock/device_action.py new file mode 100644 index 00000000000..c678bfc17cf --- /dev/null +++ b/homeassistant/components/lock/device_action.py @@ -0,0 +1,92 @@ +"""Provides device automations for Lock.""" +from typing import Optional, List +import voluptuous as vol + +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + CONF_DOMAIN, + CONF_TYPE, + CONF_DEVICE_ID, + CONF_ENTITY_ID, + SERVICE_LOCK, + SERVICE_OPEN, + SERVICE_UNLOCK, +) +from homeassistant.core import HomeAssistant, Context +from homeassistant.helpers import entity_registry +import homeassistant.helpers.config_validation as cv +from . import DOMAIN, SUPPORT_OPEN + +ACTION_TYPES = {"lock", "unlock", "open"} + +ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( + { + vol.Required(CONF_TYPE): vol.In(ACTION_TYPES), + vol.Required(CONF_ENTITY_ID): cv.entity_domain(DOMAIN), + } +) + + +async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device actions for Lock devices.""" + registry = await entity_registry.async_get_registry(hass) + actions = [] + + # 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 + + # 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: "lock", + } + ) + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "unlock", + } + ) + + state = hass.states.get(entry.entity_id) + if state: + features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + if features & (SUPPORT_OPEN): + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "open", + } + ) + + return actions + + +async def async_call_action_from_config( + hass: HomeAssistant, config: dict, variables: dict, context: Optional[Context] +) -> None: + """Execute a device action.""" + config = ACTION_SCHEMA(config) + + service_data = {ATTR_ENTITY_ID: config[CONF_ENTITY_ID]} + + if config[CONF_TYPE] == "lock": + service = SERVICE_LOCK + elif config[CONF_TYPE] == "unlock": + service = SERVICE_UNLOCK + elif config[CONF_TYPE] == "open": + service = SERVICE_OPEN + + await hass.services.async_call( + DOMAIN, service, service_data, blocking=True, context=context + ) diff --git a/homeassistant/components/lock/device_condition.py b/homeassistant/components/lock/device_condition.py new file mode 100644 index 00000000000..328da6ad450 --- /dev/null +++ b/homeassistant/components/lock/device_condition.py @@ -0,0 +1,79 @@ +"""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_ENTITY_ID, + 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 . import DOMAIN + +CONDITION_TYPES = {"is_locked", "is_unlocked"} + +CONDITION_SCHEMA = DEVICE_CONDITION_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In(CONDITION_TYPES), + } +) + + +async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device conditions for Lock devices.""" + registry = await entity_registry.async_get_registry(hass) + conditions = [] + + # 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 + + # Add conditions for each entity that belongs to this integration + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_locked", + } + ) + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_unlocked", + } + ) + + return conditions + + +def async_condition_from_config( + config: ConfigType, config_validation: bool +) -> condition.ConditionCheckerType: + """Create a function to test a device condition.""" + if config_validation: + config = CONDITION_SCHEMA(config) + if config[CONF_TYPE] == "is_locked": + state = STATE_LOCKED + else: + state = STATE_UNLOCKED + + def test_is_state(hass: HomeAssistant, variables: TemplateVarsType) -> bool: + """Test if an entity is a certain state.""" + return condition.state(hass, config[ATTR_ENTITY_ID], state) + + return test_is_state diff --git a/homeassistant/components/lock/reproduce_state.py b/homeassistant/components/lock/reproduce_state.py new file mode 100644 index 00000000000..dc1bee85839 --- /dev/null +++ b/homeassistant/components/lock/reproduce_state.py @@ -0,0 +1,61 @@ +"""Reproduce an Lock state.""" +import asyncio +import logging +from typing import Iterable, Optional + +from homeassistant.const import ( + ATTR_ENTITY_ID, + STATE_LOCKED, + STATE_UNLOCKED, + SERVICE_LOCK, + SERVICE_UNLOCK, +) +from homeassistant.core import Context, State +from homeassistant.helpers.typing import HomeAssistantType + +from . import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +VALID_STATES = {STATE_LOCKED, STATE_UNLOCKED} + + +async def _async_reproduce_state( + hass: HomeAssistantType, state: State, context: Optional[Context] = None +) -> None: + """Reproduce a single state.""" + cur_state = hass.states.get(state.entity_id) + + if cur_state is None: + _LOGGER.warning("Unable to find entity %s", state.entity_id) + return + + if state.state not in VALID_STATES: + _LOGGER.warning( + "Invalid state specified for %s: %s", state.entity_id, state.state + ) + return + + # Return if we are already at the right state. + if cur_state.state == state.state: + return + + service_data = {ATTR_ENTITY_ID: state.entity_id} + + if state.state == STATE_LOCKED: + service = SERVICE_LOCK + elif state.state == STATE_UNLOCKED: + service = SERVICE_UNLOCK + + await hass.services.async_call( + DOMAIN, service, service_data, context=context, blocking=True + ) + + +async def async_reproduce_states( + hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None +) -> None: + """Reproduce Lock states.""" + await asyncio.gather( + *(_async_reproduce_state(hass, state, context) for state in states) + ) diff --git a/homeassistant/components/lock/services.yaml b/homeassistant/components/lock/services.yaml index 0b4688c02a2..d17e00addd1 100644 --- a/homeassistant/components/lock/services.yaml +++ b/homeassistant/components/lock/services.yaml @@ -47,6 +47,16 @@ lock: description: An optional code to lock the lock with. example: 1234 +open: + description: Open all or specified locks. + fields: + entity_id: + description: Name of lock to open. + example: 'lock.front_door' + code: + description: An optional code to open the lock with. + example: 1234 + set_usercode: description: Set a usercode to lock. fields: diff --git a/homeassistant/components/lock/strings.json b/homeassistant/components/lock/strings.json new file mode 100644 index 00000000000..9c858916476 --- /dev/null +++ b/homeassistant/components/lock/strings.json @@ -0,0 +1,13 @@ +{ + "device_automation": { + "action_type": { + "lock": "Lock {entity_name}", + "open": "Open {entity_name}", + "unlock": "Unlock {entity_name}" + }, + "condition_type": { + "is_locked": "{entity_name} is locked", + "is_unlocked": "{entity_name} is unlocked" + } + } +} diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index 3c5e828765c..8675f778a26 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -2,12 +2,26 @@ from datetime import timedelta from itertools import groupby import logging +import time +from sqlalchemy.exc import SQLAlchemyError import voluptuous as vol -from homeassistant.loader import bind_hass from homeassistant.components import sun +from homeassistant.components.alexa.smart_home import EVENT_ALEXA_SMART_HOME +from homeassistant.components.homekit.const import ( + ATTR_DISPLAY_NAME, + ATTR_VALUE, + DOMAIN as DOMAIN_HOMEKIT, + EVENT_HOMEKIT_CHANGED, +) from homeassistant.components.http import HomeAssistantView +from homeassistant.components.recorder.models import Events, States +from homeassistant.components.recorder.util import ( + QUERY_RETRY_WAIT, + RETRIES, + session_scope, +) from homeassistant.const import ( ATTR_DOMAIN, ATTR_ENTITY_ID, @@ -16,26 +30,21 @@ from homeassistant.const import ( ATTR_SERVICE, CONF_EXCLUDE, CONF_INCLUDE, + EVENT_AUTOMATION_TRIGGERED, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, EVENT_LOGBOOK_ENTRY, - EVENT_STATE_CHANGED, - EVENT_AUTOMATION_TRIGGERED, EVENT_SCRIPT_STARTED, + EVENT_STATE_CHANGED, HTTP_BAD_REQUEST, STATE_NOT_HOME, STATE_OFF, STATE_ON, ) from homeassistant.core import DOMAIN as HA_DOMAIN, State, callback, split_entity_id -from homeassistant.components.alexa.smart_home import EVENT_ALEXA_SMART_HOME -from homeassistant.components.homekit.const import ( - ATTR_DISPLAY_NAME, - ATTR_VALUE, - DOMAIN as DOMAIN_HOMEKIT, - EVENT_HOMEKIT_CHANGED, -) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entityfilter import generate_filter +from homeassistant.loader import bind_hass import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -371,11 +380,6 @@ def humanify(hass, events): def _get_related_entity_ids(session, entity_filter): - from homeassistant.components.recorder.models import States - from homeassistant.components.recorder.util import RETRIES, QUERY_RETRY_WAIT - from sqlalchemy.exc import SQLAlchemyError - import time - timer_start = time.perf_counter() query = session.query(States).with_entities(States.entity_id).distinct() @@ -402,8 +406,6 @@ def _get_related_entity_ids(session, entity_filter): def _generate_filter_from_config(config): - from homeassistant.helpers.entityfilter import generate_filter - excluded_entities = [] excluded_domains = [] included_entities = [] @@ -425,9 +427,6 @@ def _generate_filter_from_config(config): def _get_events(hass, config, start_day, end_day, entity_id=None): """Get events for a period of time.""" - from homeassistant.components.recorder.models import Events, States - from homeassistant.components.recorder.util import session_scope - entities_filter = _generate_filter_from_config(config) def yield_events(query): diff --git a/homeassistant/components/logbook/services.yaml b/homeassistant/components/logbook/services.yaml index e69de29bb2d..08c463feed2 100644 --- a/homeassistant/components/logbook/services.yaml +++ b/homeassistant/components/logbook/services.yaml @@ -0,0 +1,15 @@ +log: + description: Create a custom entry in your logbook. + fields: + name: + description: Custom name for an entity, can be referenced with entity_id + example: "Kitchen" + message: + description: Message of the custom logbook entry + example: "is being used" + entity_id: + description: Entity to reference in custom logbook entry [Optional] + example: "light.kitchen" + domain: + description: Icon of domain to display in custom logbook entry [Optional] + example: "light" \ No newline at end of file diff --git a/homeassistant/components/logger/services.yaml b/homeassistant/components/logger/services.yaml index 4d1ba649d36..514aac4c71c 100644 --- a/homeassistant/components/logger/services.yaml +++ b/homeassistant/components/logger/services.yaml @@ -1,6 +1,22 @@ set_default_level: description: Set the default log level for components. fields: - level: {description: 'Default severity level. Possible values are notset, debug, - info, warn, warning, error, fatal, critical', example: debug} -set_level: {description: Set log level for components.} + level: + description: "Default severity level. Possible values are debug, info, warn, warning, error, fatal, critical" + example: debug + +set_level: + description: Set log level for components. + fields: + homeassistant.core: + description: "Example on how to change the logging level for a Home Assistant core components. Possible values are debug, info, warn, warning, error, fatal, critical" + example: debug + homeassistant.components.mqtt: + description: "Example on how to change the logging level for an Integration. Possible values are debug, info, warn, warning, error, fatal, critical" + example: warning + custom_components.my_integration: + description: "Example on how to change the logging level for a Custom Integration. Possible values are debug, info, warn, warning, error, fatal, critical" + example: debug + aiohttp: + description: "Example on how to change the logging level for a Python module. Possible values are debug, info, warn, warning, error, fatal, critical" + example: error diff --git a/homeassistant/components/logi_circle/camera.py b/homeassistant/components/logi_circle/camera.py index 27b81d8331e..ec8f1595168 100644 --- a/homeassistant/components/logi_circle/camera.py +++ b/homeassistant/components/logi_circle/camera.py @@ -148,11 +148,11 @@ class LogiCam(Camera): async def async_turn_off(self): """Disable streaming mode for this camera.""" - await self._camera.set_streaming_mode(False) + await self._camera.set_config("streaming", False) async def async_turn_on(self): """Enable streaming mode for this camera.""" - await self._camera.set_streaming_mode(True) + await self._camera.set_config("streaming", True) @property def should_poll(self): diff --git a/homeassistant/components/loopenergy/sensor.py b/homeassistant/components/loopenergy/sensor.py index 994c3e2fd89..537907d9d0a 100644 --- a/homeassistant/components/loopenergy/sensor.py +++ b/homeassistant/components/loopenergy/sensor.py @@ -1,6 +1,7 @@ """Support for Loop Energy sensors.""" import logging +import pyloopenergy import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -54,8 +55,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Loop Energy sensors.""" - import pyloopenergy - elec_config = config.get(CONF_ELEC) gas_config = config.get(CONF_GAS, {}) diff --git a/homeassistant/components/luci/device_tracker.py b/homeassistant/components/luci/device_tracker.py index 87a32767cc2..59c3251a437 100644 --- a/homeassistant/components/luci/device_tracker.py +++ b/homeassistant/components/luci/device_tracker.py @@ -8,12 +8,19 @@ from homeassistant.components.device_tracker import ( PLATFORM_SCHEMA, DeviceScanner, ) -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_SSL, CONF_USERNAME +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_SSL, + CONF_USERNAME, + CONF_VERIFY_SSL, +) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) DEFAULT_SSL = False +DEFAULT_VERIFY_SSL = True PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -21,6 +28,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, + vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, } ) @@ -44,6 +52,7 @@ class LuciDeviceScanner(DeviceScanner): config[CONF_USERNAME], config[CONF_PASSWORD], config[CONF_SSL], + config[CONF_VERIFY_SSL], ) self.last_results = {} diff --git a/homeassistant/components/luci/manifest.json b/homeassistant/components/luci/manifest.json index 646fc1a3cbf..d7cf72ebaf5 100644 --- a/homeassistant/components/luci/manifest.json +++ b/homeassistant/components/luci/manifest.json @@ -3,8 +3,11 @@ "name": "Luci", "documentation": "https://www.home-assistant.io/integrations/luci", "requirements": [ - "openwrt-luci-rpc==1.1.1" + "openwrt-luci-rpc==1.1.2" ], "dependencies": [], - "codeowners": ["@fbradyirl"] + "codeowners": [ + "@fbradyirl", + "@mzdrale" + ] } diff --git a/homeassistant/components/luftdaten/.translations/nn.json b/homeassistant/components/luftdaten/.translations/nn.json new file mode 100644 index 00000000000..52b1ec33166 --- /dev/null +++ b/homeassistant/components/luftdaten/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Luftdaten" + } +} \ No newline at end of file diff --git a/homeassistant/components/luftdaten/.translations/ru.json b/homeassistant/components/luftdaten/.translations/ru.json index 7ae83b550e3..1a05137f82d 100644 --- a/homeassistant/components/luftdaten/.translations/ru.json +++ b/homeassistant/components/luftdaten/.translations/ru.json @@ -1,8 +1,8 @@ { "config": { "error": { - "communication_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a API Luftdaten", - "invalid_sensor": "\u0414\u0430\u0442\u0447\u0438\u043a \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0438\u043b\u0438 \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d", + "communication_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a API Luftdaten.", + "invalid_sensor": "\u0414\u0430\u0442\u0447\u0438\u043a \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0438\u043b\u0438 \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d.", "sensor_exists": "\u0414\u0430\u0442\u0447\u0438\u043a \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d." }, "step": { diff --git a/homeassistant/components/luftdaten/__init__.py b/homeassistant/components/luftdaten/__init__.py index 86129eafc02..3dca82404c0 100644 --- a/homeassistant/components/luftdaten/__init__.py +++ b/homeassistant/components/luftdaten/__init__.py @@ -1,6 +1,8 @@ """Support for Luftdaten stations.""" import logging +from luftdaten import Luftdaten +from luftdaten.exceptions import LuftdatenError import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT @@ -114,8 +116,6 @@ async def async_setup(hass, config): async def async_setup_entry(hass, config_entry): """Set up Luftdaten as config entry.""" - from luftdaten import Luftdaten - from luftdaten.exceptions import LuftdatenError if not isinstance(config_entry.data[CONF_SENSOR_ID], int): _async_fixup_sensor_id(hass, config_entry, config_entry.data[CONF_SENSOR_ID]) @@ -172,12 +172,9 @@ async def async_unload_entry(hass, config_entry): ) remove_listener() - for component in ("sensor",): - await hass.config_entries.async_forward_entry_unload(config_entry, component) - hass.data[DOMAIN][DATA_LUFTDATEN_CLIENT].pop(config_entry.entry_id) - return True + return await hass.config_entries.async_forward_entry_unload(config_entry, "sensor") class LuftDatenData: @@ -191,8 +188,6 @@ class LuftDatenData: async def async_update(self): """Update sensor/binary sensor data.""" - from luftdaten.exceptions import LuftdatenError - try: await self.client.get_data() diff --git a/homeassistant/components/luftdaten/config_flow.py b/homeassistant/components/luftdaten/config_flow.py index 7a8ef0df8ba..1f382b86c0f 100644 --- a/homeassistant/components/luftdaten/config_flow.py +++ b/homeassistant/components/luftdaten/config_flow.py @@ -1,6 +1,8 @@ """Config flow to configure the Luftdaten component.""" from collections import OrderedDict +from luftdaten import Luftdaten +from luftdaten.exceptions import LuftdatenConnectionError import voluptuous as vol from homeassistant import config_entries @@ -60,7 +62,6 @@ class LuftDatenFlowHandler(config_entries.ConfigFlow): async def async_step_user(self, user_input=None): """Handle the start of the config flow.""" - from luftdaten import Luftdaten, exceptions if not user_input: return self._show_form() @@ -75,7 +76,7 @@ class LuftDatenFlowHandler(config_entries.ConfigFlow): try: await luftdaten.get_data() valid = await luftdaten.validate_sensor() - except exceptions.LuftdatenConnectionError: + except LuftdatenConnectionError: return self._show_form({CONF_SENSOR_ID: "communication_error"}) if not valid: diff --git a/homeassistant/components/lupusec/__init__.py b/homeassistant/components/lupusec/__init__.py index c64789ec4dd..60f3a192b07 100644 --- a/homeassistant/components/lupusec/__init__.py +++ b/homeassistant/components/lupusec/__init__.py @@ -1,11 +1,12 @@ """Support for Lupusec Home Security system.""" import logging +import lupupy +from lupupy.exceptions import LupusecException import voluptuous as vol -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers import discovery -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_NAME, CONF_IP_ADDRESS +from homeassistant.const import CONF_IP_ADDRESS, CONF_NAME, CONF_PASSWORD, CONF_USERNAME +from homeassistant.helpers import config_validation as cv, discovery from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -34,8 +35,6 @@ LUPUSEC_PLATFORMS = ["alarm_control_panel", "binary_sensor", "switch"] def setup(hass, config): """Set up Lupusec component.""" - from lupupy.exceptions import LupusecException - conf = config[DOMAIN] username = conf[CONF_USERNAME] password = conf[CONF_PASSWORD] @@ -67,8 +66,6 @@ class LupusecSystem: def __init__(self, username, password, ip_address, name): """Initialize the system.""" - import lupupy - self.lupusec = lupupy.Lupusec(username, password, ip_address) self.name = name diff --git a/homeassistant/components/lupusec/binary_sensor.py b/homeassistant/components/lupusec/binary_sensor.py index ccd45e9f874..b2a332a03e7 100644 --- a/homeassistant/components/lupusec/binary_sensor.py +++ b/homeassistant/components/lupusec/binary_sensor.py @@ -2,6 +2,8 @@ from datetime import timedelta import logging +import lupupy.constants as CONST + from homeassistant.components.binary_sensor import DEVICE_CLASSES, BinarySensorDevice from . import DOMAIN as LUPUSEC_DOMAIN, LupusecDevice @@ -16,8 +18,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if discovery_info is None: return - import lupupy.constants as CONST - data = hass.data[LUPUSEC_DOMAIN] device_types = [CONST.TYPE_OPENING] diff --git a/homeassistant/components/lupusec/switch.py b/homeassistant/components/lupusec/switch.py index b6391959397..a6864f39ef7 100644 --- a/homeassistant/components/lupusec/switch.py +++ b/homeassistant/components/lupusec/switch.py @@ -2,6 +2,8 @@ from datetime import timedelta import logging +import lupupy.constants as CONST + from homeassistant.components.switch import SwitchDevice from . import DOMAIN as LUPUSEC_DOMAIN, LupusecDevice @@ -16,8 +18,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if discovery_info is None: return - import lupupy.constants as CONST - data = hass.data[LUPUSEC_DOMAIN] devices = [] diff --git a/homeassistant/components/lutron/__init__.py b/homeassistant/components/lutron/__init__.py index de3ca40fd1d..09ab0fc747b 100644 --- a/homeassistant/components/lutron/__init__.py +++ b/homeassistant/components/lutron/__init__.py @@ -68,18 +68,19 @@ def setup(hass, base_config): hass.data[LUTRON_DEVICES]["switch"].append((area.name, output)) for keypad in area.keypads: for button in keypad.buttons: - # This is the best way to determine if a button does anything - # useful until pylutron is updated to provide information on - # which buttons actually control scenes. - for led in keypad.leds: - if ( - led.number == button.number - and button.name != "Unknown Button" - and button.button_type in ("SingleAction", "Toggle") - ): - hass.data[LUTRON_DEVICES]["scene"].append( - (area.name, keypad.name, button, led) - ) + # If the button has a function assigned to it, add it as a scene + if button.name != "Unknown Button" and button.button_type in ( + "SingleAction", + "Toggle", + ): + # Associate an LED with a button if there is one + led = next( + (led for led in keypad.leds if led.number == button.number), + None, + ) + hass.data[LUTRON_DEVICES]["scene"].append( + (area.name, keypad.name, button, led) + ) hass.data[LUTRON_BUTTONS].append(LutronButton(hass, keypad, button)) if area.occupancy_group is not None: diff --git a/homeassistant/components/lw12wifi/light.py b/homeassistant/components/lw12wifi/light.py index 3b9ccae1681..abf75a1e318 100644 --- a/homeassistant/components/lw12wifi/light.py +++ b/homeassistant/components/lw12wifi/light.py @@ -2,6 +2,7 @@ import logging +import lw12 import voluptuous as vol from homeassistant.components.light import ( @@ -9,18 +10,17 @@ from homeassistant.components.light import ( ATTR_EFFECT, ATTR_HS_COLOR, ATTR_TRANSITION, - Light, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, - SUPPORT_EFFECT, SUPPORT_COLOR, + SUPPORT_EFFECT, SUPPORT_TRANSITION, + Light, ) from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util - _LOGGER = logging.getLogger(__name__) @@ -38,8 +38,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up LW-12 WiFi LED Controller platform.""" - import lw12 - # Assign configuration variables. name = config.get(CONF_NAME) host = config.get(CONF_HOST) @@ -107,8 +105,6 @@ class LW12WiFi(Light): Use the Enum element name for display. """ - import lw12 - return [effect.name.replace("_", " ").title() for effect in lw12.LW12_EFFECT] @property @@ -123,8 +119,6 @@ class LW12WiFi(Light): def turn_on(self, **kwargs): """Instruct the light to turn on.""" - import lw12 - self._light.light_on() if ATTR_HS_COLOR in kwargs: self._rgb_color = color_util.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR]) diff --git a/homeassistant/components/magicseaweed/sensor.py b/homeassistant/components/magicseaweed/sensor.py index 66ab87a6569..174ecf1882e 100644 --- a/homeassistant/components/magicseaweed/sensor.py +++ b/homeassistant/components/magicseaweed/sensor.py @@ -1,19 +1,21 @@ """Support for magicseaweed data from magicseaweed.com.""" from datetime import timedelta import logging + +import magicseaweed import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_API_KEY, - CONF_NAME, - CONF_MONITORED_CONDITIONS, ATTR_ATTRIBUTION, + CONF_API_KEY, + CONF_MONITORED_CONDITIONS, + CONF_NAME, ) import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt_util from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -175,8 +177,6 @@ class MagicSeaweedData: def __init__(self, api_key, spot_id, units): """Initialize the data object.""" - import magicseaweed - self._msw = magicseaweed.MSW_Forecast(api_key, spot_id, None, units) self.currently = None self.hourly = {} diff --git a/homeassistant/components/mailgun/.translations/ca.json b/homeassistant/components/mailgun/.translations/ca.json index f43467de7d9..6bcb737588a 100644 --- a/homeassistant/components/mailgun/.translations/ca.json +++ b/homeassistant/components/mailgun/.translations/ca.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia." }, "create_entry": { - "default": "Per enviar esdeveniments a Home Assistant, haur\u00e0s de configurar [Webhooks amb Mailgun]({mailgun_url}). \n\nCompleta la seg\u00fcent informaci\u00f3: \n\n- URL: `{webhook_url}` \n- M\u00e8tode: POST \n- Tipus de contingut: application/x-www-form-urlencoded\n\nConsulta la [documentaci\u00f3]({docs_url}) sobre com configurar els automatismes per gestionar dades entrants." + "default": "Per enviar esdeveniments a Home Assistant, haur\u00e0s de configurar [Webhooks amb Mailgun]({mailgun_url}). \n\nCompleta la seg\u00fcent informaci\u00f3: \n\n- URL: `{webhook_url}` \n- M\u00e8tode: POST \n- Tipus de contingut: application/x-www-form-urlencoded\n\nConsulta la [documentaci\u00f3]({docs_url}) sobre com configurar les automatitzacions per gestionar dades entrants." }, "step": { "user": { diff --git a/homeassistant/components/mailgun/.translations/ru.json b/homeassistant/components/mailgun/.translations/ru.json index 39503154b6c..094940e6f90 100644 --- a/homeassistant/components/mailgun/.translations/ru.json +++ b/homeassistant/components/mailgun/.translations/ru.json @@ -10,7 +10,7 @@ "step": { "user": { "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 Mailgun?", - "title": "Mailgun Webhook" + "title": "Mailgun" } }, "title": "Mailgun" diff --git a/homeassistant/components/mastodon/manifest.json b/homeassistant/components/mastodon/manifest.json index e041ba2f669..cacdf9dd502 100644 --- a/homeassistant/components/mastodon/manifest.json +++ b/homeassistant/components/mastodon/manifest.json @@ -3,7 +3,7 @@ "name": "Mastodon", "documentation": "https://www.home-assistant.io/integrations/mastodon", "requirements": [ - "Mastodon.py==1.4.6" + "Mastodon.py==1.5.0" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/mastodon/notify.py b/homeassistant/components/mastodon/notify.py index e7f7de5917f..88716de5773 100644 --- a/homeassistant/components/mastodon/notify.py +++ b/homeassistant/components/mastodon/notify.py @@ -1,13 +1,14 @@ """Mastodon platform for notify component.""" import logging +from mastodon import Mastodon +from mastodon.Mastodon import MastodonAPIError, MastodonUnauthorizedError import voluptuous as vol +from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService from homeassistant.const import CONF_ACCESS_TOKEN import homeassistant.helpers.config_validation as cv -from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService - _LOGGER = logging.getLogger(__name__) CONF_BASE_URL = "base_url" @@ -28,9 +29,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def get_service(hass, config, discovery_info=None): """Get the Mastodon notification service.""" - from mastodon import Mastodon - from mastodon.Mastodon import MastodonUnauthorizedError - client_id = config.get(CONF_CLIENT_ID) client_secret = config.get(CONF_CLIENT_SECRET) access_token = config.get(CONF_ACCESS_TOKEN) @@ -60,8 +58,6 @@ class MastodonNotificationService(BaseNotificationService): def send_message(self, message="", **kwargs): """Send a message to a user.""" - from mastodon.Mastodon import MastodonAPIError - try: self._api.toot(message) except MastodonAPIError: diff --git a/homeassistant/components/mcp23017/binary_sensor.py b/homeassistant/components/mcp23017/binary_sensor.py index c059ad6a1b6..088052c469e 100644 --- a/homeassistant/components/mcp23017/binary_sensor.py +++ b/homeassistant/components/mcp23017/binary_sensor.py @@ -2,6 +2,10 @@ import logging import voluptuous as vol +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 from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA from homeassistant.const import DEVICE_DEFAULT_NAME @@ -37,10 +41,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the MCP23017 binary sensors.""" - import board - import busio - import adafruit_mcp230xx - pull_mode = config[CONF_PULL_MODE] invert_logic = config[CONF_INVERT_LOGIC] i2c_address = config[CONF_I2C_ADDRESS] @@ -65,8 +65,6 @@ class MCP23017BinarySensor(BinarySensorDevice): def __init__(self, name, pin, pull_mode, invert_logic): """Initialize the MCP23017 binary sensor.""" - import digitalio - self._name = name or DEVICE_DEFAULT_NAME self._pin = pin self._pull_mode = pull_mode diff --git a/homeassistant/components/mcp23017/manifest.json b/homeassistant/components/mcp23017/manifest.json index 2dbffd829f8..13c36424dd6 100644 --- a/homeassistant/components/mcp23017/manifest.json +++ b/homeassistant/components/mcp23017/manifest.json @@ -3,7 +3,7 @@ "name": "MCP23017 I/O Expander", "documentation": "https://www.home-assistant.io/integrations/mcp23017", "requirements": [ - "RPi.GPIO==0.6.5", + "RPi.GPIO==0.7.0", "adafruit-blinka==1.2.1", "adafruit-circuitpython-mcp230xx==1.1.2" ], diff --git a/homeassistant/components/mcp23017/switch.py b/homeassistant/components/mcp23017/switch.py index 46978319198..399ed17c44b 100644 --- a/homeassistant/components/mcp23017/switch.py +++ b/homeassistant/components/mcp23017/switch.py @@ -2,6 +2,10 @@ import logging import voluptuous as vol +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 from homeassistant.components.switch import PLATFORM_SCHEMA from homeassistant.const import DEVICE_DEFAULT_NAME @@ -31,10 +35,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the MCP23017 devices.""" - import board - import busio - import adafruit_mcp230xx - invert_logic = config.get(CONF_INVERT_LOGIC) i2c_address = config.get(CONF_I2C_ADDRESS) @@ -54,8 +54,6 @@ class MCP23017Switch(ToggleEntity): def __init__(self, name, pin, invert_logic): """Initialize the pin.""" - import digitalio - self._name = name or DEVICE_DEFAULT_NAME self._pin = pin self._invert_logic = invert_logic diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index 886535555d5..4fd5470ebdf 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.09.28" + "youtube_dl==2019.10.22" ], "dependencies": [ "media_player" diff --git a/homeassistant/components/media_extractor/services.yaml b/homeassistant/components/media_extractor/services.yaml index e69de29bb2d..c5588c34134 100644 --- a/homeassistant/components/media_extractor/services.yaml +++ b/homeassistant/components/media_extractor/services.yaml @@ -0,0 +1,13 @@ +play_media: + description: Downloads file from given url. + fields: + entity_id: + description: Name(s) of entities to play media on. + example: 'media_player.living_room_chromecast' + media_content_id: + description: The ID of the content to play. Platform dependent. + example: 'https://soundcloud.com/bruttoband/brutto-11' + media_content_type: + description: The type of the content to play. Must be one of MUSIC, TVSHOW, VIDEO, EPISODE, CHANNEL or PLAYLIST MUSIC. + example: 'music' + diff --git a/homeassistant/components/melissa/__init__.py b/homeassistant/components/melissa/__init__.py index 830036b072a..c03939e3e9c 100644 --- a/homeassistant/components/melissa/__init__.py +++ b/homeassistant/components/melissa/__init__.py @@ -1,9 +1,10 @@ """Support for Melissa climate.""" import logging +import melissa import voluptuous as vol -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers import config_validation as cv from homeassistant.helpers.discovery import async_load_platform @@ -28,8 +29,6 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass, config): """Set up the Melissa Climate component.""" - import melissa - conf = config[DOMAIN] username = conf.get(CONF_USERNAME) password = conf.get(CONF_PASSWORD) diff --git a/homeassistant/components/melissa/climate.py b/homeassistant/components/melissa/climate.py index 10ea6200c6f..38f4977c96a 100644 --- a/homeassistant/components/melissa/climate.py +++ b/homeassistant/components/melissa/climate.py @@ -156,7 +156,9 @@ class MelissaClimate(ClimateDevice): return mode = self.hass_mode_to_melissa(hvac_mode) - await self.async_send({self._api.MODE: mode}) + await self.async_send( + {self._api.MODE: mode, self._api.STATE: self._api.STATE_ON} + ) async def async_send(self, value): """Send action to service.""" diff --git a/homeassistant/components/message_bird/notify.py b/homeassistant/components/message_bird/notify.py index 5df02ef69c4..ce1d275a832 100644 --- a/homeassistant/components/message_bird/notify.py +++ b/homeassistant/components/message_bird/notify.py @@ -1,16 +1,17 @@ """MessageBird platform for notify component.""" import logging +import messagebird +from messagebird.client import ErrorException import voluptuous as vol -from homeassistant.const import CONF_API_KEY, CONF_SENDER -import homeassistant.helpers.config_validation as cv - from homeassistant.components.notify import ( ATTR_TARGET, PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.const import CONF_API_KEY, CONF_SENDER +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -26,8 +27,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def get_service(hass, config, discovery_info=None): """Get the MessageBird notification service.""" - import messagebird - client = messagebird.Client(config[CONF_API_KEY]) try: # validates the api key @@ -49,8 +48,6 @@ class MessageBirdNotificationService(BaseNotificationService): def send_message(self, message=None, **kwargs): """Send a message to a specified target.""" - from messagebird.client import ErrorException - targets = kwargs.get(ATTR_TARGET) if not targets: _LOGGER.error("No target specified") diff --git a/homeassistant/components/met/.translations/nl.json b/homeassistant/components/met/.translations/nl.json index 87f13084f7e..c8b120b855a 100644 --- a/homeassistant/components/met/.translations/nl.json +++ b/homeassistant/components/met/.translations/nl.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "Naam bestaat al" + "name_exists": "Locatie bestaat al." }, "step": { "user": { diff --git a/homeassistant/components/met/.translations/nn.json b/homeassistant/components/met/.translations/nn.json index 0e024a0e1eb..6daa5b2657a 100644 --- a/homeassistant/components/met/.translations/nn.json +++ b/homeassistant/components/met/.translations/nn.json @@ -6,6 +6,7 @@ "name": "Namn" } } - } + }, + "title": "Met.no" } } \ No newline at end of file diff --git a/homeassistant/components/met/.translations/pt.json b/homeassistant/components/met/.translations/pt.json new file mode 100644 index 00000000000..c7081cd694a --- /dev/null +++ b/homeassistant/components/met/.translations/pt.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "latitude": "Latitude", + "longitude": "Longitude" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/met/.translations/ru.json b/homeassistant/components/met/.translations/ru.json index 559382cf209..768152084aa 100644 --- a/homeassistant/components/met/.translations/ru.json +++ b/homeassistant/components/met/.translations/ru.json @@ -11,10 +11,10 @@ "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" }, - "description": "\u041c\u0435\u0442\u0435\u043e\u0440\u043e\u043b\u043e\u0433\u0438\u0447\u0435\u0441\u043a\u0438\u0439 \u0438\u043d\u0441\u0442\u0438\u0442\u0443\u0442", + "description": "\u041d\u043e\u0440\u0432\u0435\u0436\u0441\u043a\u0438\u0439 \u043c\u0435\u0442\u0435\u043e\u0440\u043e\u043b\u043e\u0433\u0438\u0447\u0435\u0441\u043a\u0438\u0439 \u0438\u043d\u0441\u0442\u0438\u0442\u0443\u0442.", "title": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435" } }, - "title": "Met.no" + "title": "\u041c\u0435\u0442\u0435\u043e\u0440\u043e\u043b\u043e\u0433\u0438\u0447\u0435\u0441\u043a\u0430\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u041d\u043e\u0440\u0432\u0435\u0433\u0438\u0438 (Met.no)" } } \ No newline at end of file diff --git a/homeassistant/components/metoffice/sensor.py b/homeassistant/components/metoffice/sensor.py index 3ca55533ce3..98d94ebe6ca 100644 --- a/homeassistant/components/metoffice/sensor.py +++ b/homeassistant/components/metoffice/sensor.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +import datapoint as dp import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -92,8 +93,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Met Office sensor platform.""" - import datapoint as dp - api_key = config.get(CONF_API_KEY) latitude = config.get(CONF_LATITUDE, hass.config.latitude) longitude = config.get(CONF_LONGITUDE, hass.config.longitude) @@ -193,8 +192,6 @@ class MetOfficeCurrentData: @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data from Datapoint.""" - import datapoint as dp - try: forecast = self._datapoint.get_forecast_for_site(self._site.id, "3hourly") self.data = forecast.now() diff --git a/homeassistant/components/metoffice/weather.py b/homeassistant/components/metoffice/weather.py index bb7a64005ce..09350588d46 100644 --- a/homeassistant/components/metoffice/weather.py +++ b/homeassistant/components/metoffice/weather.py @@ -1,6 +1,7 @@ """Support for UK Met Office weather service.""" import logging +import datapoint as dp import voluptuous as vol from homeassistant.components.weather import PLATFORM_SCHEMA, WeatherEntity @@ -35,8 +36,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Met Office weather platform.""" - import datapoint as dp - name = config.get(CONF_NAME) datapoint = dp.connection(api_key=config.get(CONF_API_KEY)) diff --git a/homeassistant/components/microsoft/manifest.json b/homeassistant/components/microsoft/manifest.json index 16ae94c212e..5834897ee90 100644 --- a/homeassistant/components/microsoft/manifest.json +++ b/homeassistant/components/microsoft/manifest.json @@ -3,7 +3,7 @@ "name": "Microsoft", "documentation": "https://www.home-assistant.io/integrations/microsoft", "requirements": [ - "pycsspeechtts==1.0.2" + "pycsspeechtts==1.0.3" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/microsoft/tts.py b/homeassistant/components/microsoft/tts.py index 3536c788bb9..d214f6648dd 100644 --- a/homeassistant/components/microsoft/tts.py +++ b/homeassistant/components/microsoft/tts.py @@ -14,6 +14,7 @@ CONF_RATE = "rate" CONF_VOLUME = "volume" CONF_PITCH = "pitch" CONF_CONTOUR = "contour" +CONF_REGION = "region" _LOGGER = logging.getLogger(__name__) @@ -72,6 +73,7 @@ DEFAULT_RATE = 0 DEFAULT_VOLUME = 0 DEFAULT_PITCH = "default" DEFAULT_CONTOUR = "" +DEFAULT_REGION = "eastus" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -87,6 +89,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ), vol.Optional(CONF_PITCH, default=DEFAULT_PITCH): cv.string, vol.Optional(CONF_CONTOUR, default=DEFAULT_CONTOUR): cv.string, + vol.Optional(CONF_REGION, default=DEFAULT_REGION): cv.string, } ) @@ -102,13 +105,16 @@ def get_engine(hass, config): config[CONF_VOLUME], config[CONF_PITCH], config[CONF_CONTOUR], + config[CONF_REGION], ) class MicrosoftProvider(Provider): """The Microsoft speech API provider.""" - def __init__(self, apikey, lang, gender, ttype, rate, volume, pitch, contour): + def __init__( + self, apikey, lang, gender, ttype, rate, volume, pitch, contour, region + ): """Init Microsoft TTS service.""" self._apikey = apikey self._lang = lang @@ -119,6 +125,7 @@ class MicrosoftProvider(Provider): self._volume = f"{volume}%" self._pitch = pitch self._contour = contour + self._region = region self.name = "Microsoft" @property @@ -138,7 +145,7 @@ class MicrosoftProvider(Provider): from pycsspeechtts import pycsspeechtts try: - trans = pycsspeechtts.TTSTranslator(self._apikey) + trans = pycsspeechtts.TTSTranslator(self._apikey, self._region) data = trans.speak( language=language, gender=self._gender, diff --git a/homeassistant/components/miflora/sensor.py b/homeassistant/components/miflora/sensor.py index 28020a80175..a08c4ce5eac 100644 --- a/homeassistant/components/miflora/sensor.py +++ b/homeassistant/components/miflora/sensor.py @@ -1,20 +1,31 @@ """Support for Xiaomi Mi Flora BLE plant sensor.""" from datetime import timedelta import logging + +import btlewrap +from btlewrap import BluetoothBackendException +from miflora import miflora_poller import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.helpers.entity import Entity -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( CONF_FORCE_UPDATE, + CONF_MAC, CONF_MONITORED_CONDITIONS, CONF_NAME, - CONF_MAC, CONF_SCAN_INTERVAL, EVENT_HOMEASSISTANT_START, ) from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity + +try: + import bluepy.btle # noqa: F401 pylint: disable=unused-import + + BACKEND = btlewrap.BluepyBackend +except ImportError: + BACKEND = btlewrap.GatttoolBackend _LOGGER = logging.getLogger(__name__) @@ -53,17 +64,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the MiFlora sensor.""" - from miflora import miflora_poller - - try: - import bluepy.btle # noqa: F401 pylint: disable=unused-import - from btlewrap import BluepyBackend - - backend = BluepyBackend - except ImportError: - from btlewrap import GatttoolBackend - - backend = GatttoolBackend + backend = BACKEND _LOGGER.debug("Miflora is using %s backend.", backend.__name__) cache = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL).total_seconds() @@ -152,8 +153,6 @@ class MiFloraSensor(Entity): This uses a rolling median over 3 values to filter out outliers. """ - from btlewrap import BluetoothBackendException - try: _LOGGER.debug("Polling data for %s", self.name) data = self.poller.parameter_value(self.parameter) diff --git a/homeassistant/components/mitemp_bt/sensor.py b/homeassistant/components/mitemp_bt/sensor.py index adeba48dbc8..b536149680d 100644 --- a/homeassistant/components/mitemp_bt/sensor.py +++ b/homeassistant/components/mitemp_bt/sensor.py @@ -1,21 +1,30 @@ """Support for Xiaomi Mi Temp BLE environmental sensor.""" import logging +import btlewrap +from btlewrap.base import BluetoothBackendException +from mitemp_bt import mitemp_bt_poller import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.helpers.entity import Entity -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( CONF_FORCE_UPDATE, + CONF_MAC, CONF_MONITORED_CONDITIONS, CONF_NAME, - CONF_MAC, + DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_BATTERY, ) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +try: + import bluepy.btle # noqa: F401 pylint: disable=unused-import + + BACKEND = btlewrap.BluepyBackend +except ImportError: + BACKEND = btlewrap.GatttoolBackend _LOGGER = logging.getLogger(__name__) @@ -60,17 +69,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the MiTempBt sensor.""" - from mitemp_bt import mitemp_bt_poller - - try: - import bluepy.btle # noqa: F401 pylint: disable=unused-import - from btlewrap import BluepyBackend - - backend = BluepyBackend - except ImportError: - from btlewrap import GatttoolBackend - - backend = GatttoolBackend + backend = BACKEND _LOGGER.debug("MiTempBt is using %s backend.", backend.__name__) cache = config.get(CONF_CACHE) @@ -152,8 +151,6 @@ class MiTempBtSensor(Entity): This uses a rolling median over 3 values to filter out outliers. """ - from btlewrap.base import BluetoothBackendException - try: _LOGGER.debug("Polling data for %s", self.name) data = self.poller.parameter_value(self.parameter) diff --git a/homeassistant/components/mobile_app/.translations/ko.json b/homeassistant/components/mobile_app/.translations/ko.json index faf30e5f985..899845fcc2e 100644 --- a/homeassistant/components/mobile_app/.translations/ko.json +++ b/homeassistant/components/mobile_app/.translations/ko.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "install_app": "\ubaa8\ubc14\uc77c \uc571\uc744 \uc5f4\uc5b4 Home Assistant \uc640 \ud1b5\ud569\uc744 \uc124\uc815\ud574\uc8fc\uc138\uc694. \ud638\ud658\ub418\ub294 \uc571 \ubaa9\ub85d\uc740 [\uc548\ub0b4]({apps_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." + "install_app": "\ubaa8\ubc14\uc77c \uc571\uc744 \uc5f4\uc5b4 Home Assistant \uc640 \uc5f0\ub3d9\uc744 \uc124\uc815\ud574\uc8fc\uc138\uc694. \ud638\ud658\ub418\ub294 \uc571 \ubaa9\ub85d\uc740 [\uc548\ub0b4]({apps_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." }, "step": { "confirm": { diff --git a/homeassistant/components/mobile_app/__init__.py b/homeassistant/components/mobile_app/__init__.py index 01877099201..ca2a58d1f96 100644 --- a/homeassistant/components/mobile_app/__init__.py +++ b/homeassistant/components/mobile_app/__init__.py @@ -1,6 +1,6 @@ """Integrates Native Apps to Home Assistant.""" -from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.components.webhook import async_register as webhook_register +from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.helpers import device_registry as dr, discovery from homeassistant.helpers.typing import ConfigType, HomeAssistantType @@ -20,7 +20,6 @@ from .const import ( STORAGE_KEY, STORAGE_VERSION, ) - from .http_api import RegistrationsView from .webhook import handle_webhook from .websocket_api import register_websocket_handlers diff --git a/homeassistant/components/mobile_app/binary_sensor.py b/homeassistant/components/mobile_app/binary_sensor.py index 975c4c16c32..73bf925553e 100644 --- a/homeassistant/components/mobile_app/binary_sensor.py +++ b/homeassistant/components/mobile_app/binary_sensor.py @@ -1,9 +1,9 @@ """Binary sensor platform for mobile_app.""" from functools import partial +from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.core import callback -from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import ( @@ -13,7 +13,6 @@ from .const import ( DATA_DEVICES, DOMAIN, ) - from .entity import MobileAppEntity, sensor_id diff --git a/homeassistant/components/mobile_app/config_flow.py b/homeassistant/components/mobile_app/config_flow.py index 96b0a35aae2..bc9c6167da8 100644 --- a/homeassistant/components/mobile_app/config_flow.py +++ b/homeassistant/components/mobile_app/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Mobile App.""" from homeassistant import config_entries -from .const import DOMAIN, ATTR_DEVICE_NAME + +from .const import ATTR_DEVICE_NAME, DOMAIN @config_entries.HANDLERS.register(DOMAIN) diff --git a/homeassistant/components/mobile_app/const.py b/homeassistant/components/mobile_app/const.py index d01990b74b9..0b6a93a39ea 100644 --- a/homeassistant/components/mobile_app/const.py +++ b/homeassistant/components/mobile_app/const.py @@ -4,13 +4,13 @@ import voluptuous as vol from homeassistant.components.binary_sensor import ( DEVICE_CLASSES as BINARY_SENSOR_CLASSES, ) -from homeassistant.components.sensor import DEVICE_CLASSES as 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 diff --git a/homeassistant/components/mobile_app/device_tracker.py b/homeassistant/components/mobile_app/device_tracker.py index 0e05c424609..f58f80aa5fc 100644 --- a/homeassistant/components/mobile_app/device_tracker.py +++ b/homeassistant/components/mobile_app/device_tracker.py @@ -1,19 +1,20 @@ """Device tracker platform that adds support for OwnTracks over MQTT.""" import logging -from homeassistant.core import callback -from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, ATTR_BATTERY_LEVEL -from homeassistant.components.device_tracker.const import SOURCE_TYPE_GPS from homeassistant.components.device_tracker.config_entry import TrackerEntity +from homeassistant.components.device_tracker.const import SOURCE_TYPE_GPS +from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_LATITUDE, ATTR_LONGITUDE +from homeassistant.core import callback from homeassistant.helpers.restore_state import RestoreEntity + from .const import ( ATTR_ALTITUDE, ATTR_BATTERY, ATTR_COURSE, ATTR_DEVICE_ID, ATTR_DEVICE_NAME, - ATTR_GPS_ACCURACY, ATTR_GPS, + ATTR_GPS_ACCURACY, ATTR_LOCATION_NAME, ATTR_SPEED, ATTR_VERTICAL_ACCURACY, diff --git a/homeassistant/components/mobile_app/helpers.py b/homeassistant/components/mobile_app/helpers.py index 3be082951c5..2fb949720d6 100644 --- a/homeassistant/components/mobile_app/helpers.py +++ b/homeassistant/components/mobile_app/helpers.py @@ -1,9 +1,11 @@ """Helpers for mobile_app.""" -import logging import json +import logging from typing import Callable, Dict, Tuple -from aiohttp.web import json_response, Response +from aiohttp.web import Response, json_response +from nacl.encoding import Base64Encoder +from nacl.secret import SecretBox from homeassistant.core import Context from homeassistant.helpers.json import JSONEncoder @@ -13,8 +15,8 @@ from .const import ( ATTR_APP_DATA, ATTR_APP_ID, ATTR_APP_NAME, - ATTR_DEVICE_ID, ATTR_APP_VERSION, + ATTR_DEVICE_ID, ATTR_DEVICE_NAME, ATTR_MANUFACTURER, ATTR_MODEL, @@ -36,8 +38,6 @@ def setup_decrypt() -> Tuple[int, Callable]: Async friendly. """ - from nacl.secret import SecretBox - from nacl.encoding import Base64Encoder def decrypt(ciphertext, key): """Decrypt ciphertext using key.""" @@ -51,8 +51,6 @@ def setup_encrypt() -> Tuple[int, Callable]: Async friendly. """ - from nacl.secret import SecretBox - from nacl.encoding import Base64Encoder def encrypt(ciphertext, key): """Encrypt ciphertext using key.""" diff --git a/homeassistant/components/mobile_app/http_api.py b/homeassistant/components/mobile_app/http_api.py index 67914ea7076..ee69f15fb11 100644 --- a/homeassistant/components/mobile_app/http_api.py +++ b/homeassistant/components/mobile_app/http_api.py @@ -1,18 +1,19 @@ """Provides an HTTP API for mobile_app.""" -import uuid from typing import Dict +import uuid -from aiohttp.web import Response, Request +from aiohttp.web import Request, Response +from nacl.secret import SecretBox from homeassistant.auth.util import generate_secret from homeassistant.components.cloud import ( + CloudNotAvailable, async_create_cloudhook, async_remote_ui_url, - CloudNotAvailable, ) from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.data_validator import RequestDataValidator -from homeassistant.const import HTTP_CREATED, CONF_WEBHOOK_ID +from homeassistant.const import CONF_WEBHOOK_ID, HTTP_CREATED from .const import ( ATTR_DEVICE_ID, @@ -24,7 +25,6 @@ from .const import ( DOMAIN, REGISTRATION_SCHEMA, ) - from .helpers import supports_encryption @@ -49,8 +49,6 @@ class RegistrationsView(HomeAssistantView): data[CONF_WEBHOOK_ID] = webhook_id if data[ATTR_SUPPORTS_ENCRYPTION] and supports_encryption(): - from nacl.secret import SecretBox - data[CONF_SECRET] = generate_secret(SecretBox.KEY_SIZE) data[CONF_USER_ID] = request["hass_user"].id diff --git a/homeassistant/components/mobile_app/notify.py b/homeassistant/components/mobile_app/notify.py index 1e6a0517026..8ac34c9af1d 100644 --- a/homeassistant/components/mobile_app/notify.py +++ b/homeassistant/components/mobile_app/notify.py @@ -12,7 +12,6 @@ from homeassistant.components.notify import ( ATTR_TITLE_DEFAULT, BaseNotificationService, ) - from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.util.dt as dt_util diff --git a/homeassistant/components/mobile_app/sensor.py b/homeassistant/components/mobile_app/sensor.py index b96a6f1e2f0..199ba968dd2 100644 --- a/homeassistant/components/mobile_app/sensor.py +++ b/homeassistant/components/mobile_app/sensor.py @@ -13,7 +13,6 @@ from .const import ( DATA_DEVICES, DOMAIN, ) - from .entity import MobileAppEntity, sensor_id diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index f95d5b993f0..66188500fd6 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -1,13 +1,12 @@ """Webhook handlers for mobile_app.""" import logging -from aiohttp.web import HTTPBadRequest, Response, Request +from aiohttp.web import HTTPBadRequest, Request, Response import voluptuous as vol -from homeassistant.components.cloud import async_remote_ui_url, CloudNotAvailable +from homeassistant.components.cloud import CloudNotAvailable, async_remote_ui_url from homeassistant.components.frontend import MANIFEST_JSON from homeassistant.components.zone.const import DOMAIN as ZONE_DOMAIN - from homeassistant.const import ( ATTR_DOMAIN, ATTR_SERVICE, @@ -50,10 +49,10 @@ from .const import ( ERR_ENCRYPTION_REQUIRED, ERR_SENSOR_DUPLICATE_UNIQUE_ID, ERR_SENSOR_NOT_REGISTERED, + SIGNAL_LOCATION_UPDATE, SIGNAL_SENSOR_UPDATE, WEBHOOK_PAYLOAD_SCHEMA, WEBHOOK_SCHEMAS, - WEBHOOK_TYPES, WEBHOOK_TYPE_CALL_SERVICE, WEBHOOK_TYPE_FIRE_EVENT, WEBHOOK_TYPE_GET_CONFIG, @@ -63,10 +62,8 @@ from .const import ( WEBHOOK_TYPE_UPDATE_LOCATION, WEBHOOK_TYPE_UPDATE_REGISTRATION, WEBHOOK_TYPE_UPDATE_SENSOR_STATES, - SIGNAL_LOCATION_UPDATE, + WEBHOOK_TYPES, ) - - from .helpers import ( _decrypt_payload, empty_okay_response, @@ -77,7 +74,6 @@ from .helpers import ( webhook_response, ) - _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/mobile_app/websocket_api.py b/homeassistant/components/mobile_app/websocket_api.py index d0d13415b4d..813d0a9cf89 100644 --- a/homeassistant/components/mobile_app/websocket_api.py +++ b/homeassistant/components/mobile_app/websocket_api.py @@ -29,7 +29,6 @@ from .const import ( DATA_STORE, DOMAIN, ) - from .helpers import safe_registration, savable_state diff --git a/homeassistant/components/mopar/__init__.py b/homeassistant/components/mopar/__init__.py index 857dbab2a3b..21a3c3d16ea 100644 --- a/homeassistant/components/mopar/__init__.py +++ b/homeassistant/components/mopar/__init__.py @@ -1,17 +1,18 @@ """Support for Mopar vehicles.""" -import logging from datetime import timedelta +import logging +import motorparts import voluptuous as vol from homeassistant.components.lock import DOMAIN as LOCK from homeassistant.components.sensor import DOMAIN as SENSOR from homeassistant.components.switch import DOMAIN as SWITCH from homeassistant.const import ( - CONF_USERNAME, CONF_PASSWORD, CONF_PIN, CONF_SCAN_INTERVAL, + CONF_USERNAME, ) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.discovery import load_platform @@ -53,8 +54,6 @@ SERVICE_HORN_SCHEMA = vol.Schema({vol.Required(ATTR_VEHICLE_INDEX): cv.positive_ def setup(hass, config): """Set up the Mopar component.""" - import motorparts - conf = config[DOMAIN] cookie = hass.config.path(COOKIE_FILE) try: @@ -101,8 +100,6 @@ class MoparData: def update(self, now, **kwargs): """Update data.""" - import motorparts - _LOGGER.debug("Updating vehicle data") try: self.vehicles = motorparts.get_summary(self._session)["vehicles"] @@ -123,8 +120,6 @@ class MoparData: @property def attribution(self): """Get the attribution string from Mopar.""" - import motorparts - return motorparts.ATTRIBUTION def get_vehicle_name(self, index): @@ -136,8 +131,6 @@ class MoparData: def actuate(self, command, index): """Run a command on the specified Mopar vehicle.""" - import motorparts - try: response = getattr(motorparts, command)(self._session, index) except motorparts.MoparError as error: diff --git a/homeassistant/components/mopar/sensor.py b/homeassistant/components/mopar/sensor.py index a29e9c5c739..2243fcdaa22 100644 --- a/homeassistant/components/mopar/sensor.py +++ b/homeassistant/components/mopar/sensor.py @@ -1,8 +1,8 @@ """Support for the Mopar vehicle sensor platform.""" from homeassistant.components.mopar import ( - DOMAIN as MOPAR_DOMAIN, - DATA_UPDATED, ATTR_VEHICLE_INDEX, + DATA_UPDATED, + DOMAIN as MOPAR_DOMAIN, ) from homeassistant.const import ATTR_ATTRIBUTION, LENGTH_KILOMETERS from homeassistant.core import callback diff --git a/homeassistant/components/mopar/switch.py b/homeassistant/components/mopar/switch.py index bbada4ecee7..2dad56637ce 100644 --- a/homeassistant/components/mopar/switch.py +++ b/homeassistant/components/mopar/switch.py @@ -3,7 +3,7 @@ import logging from homeassistant.components.mopar import DOMAIN as MOPAR_DOMAIN from homeassistant.components.switch import SwitchDevice -from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.const import STATE_OFF, STATE_ON _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py index c19f8f49226..2628815727c 100644 --- a/homeassistant/components/mpd/media_player.py +++ b/homeassistant/components/mpd/media_player.py @@ -3,9 +3,10 @@ from datetime import timedelta import logging import os +import mpd 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, @@ -85,8 +86,6 @@ class MpdDevice(MediaPlayerDevice): # pylint: disable=no-member def __init__(self, server, port, password, name): """Initialize the MPD device.""" - import mpd - self.server = server self.port = port self._name = name @@ -107,8 +106,6 @@ class MpdDevice(MediaPlayerDevice): def _connect(self): """Connect to MPD.""" - import mpd - try: self._client.connect(self.server, self.port) @@ -121,8 +118,6 @@ class MpdDevice(MediaPlayerDevice): def _disconnect(self): """Disconnect from MPD.""" - import mpd - try: self._client.disconnect() except mpd.ConnectionError: @@ -144,8 +139,6 @@ class MpdDevice(MediaPlayerDevice): def update(self): """Get the latest data and update the state.""" - import mpd - try: if not self._is_connected: self._connect() @@ -261,8 +254,6 @@ class MpdDevice(MediaPlayerDevice): @Throttle(PLAYLIST_UPDATE_INTERVAL) def _update_playlists(self, **kwargs): """Update available MPD playlists.""" - import mpd - try: self._playlists = [] for playlist_data in self._client.listplaylists(): diff --git a/homeassistant/components/mqtt/.translations/ru.json b/homeassistant/components/mqtt/.translations/ru.json index ac27652cbdd..925b8cf5ab4 100644 --- a/homeassistant/components/mqtt/.translations/ru.json +++ b/homeassistant/components/mqtt/.translations/ru.json @@ -4,7 +4,7 @@ "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": { - "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0431\u0440\u043e\u043a\u0435\u0440\u0443" + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0431\u0440\u043e\u043a\u0435\u0440\u0443." }, "step": { "broker": { diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 9b25a6ef6e4..2fab599ac3f 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -1,5 +1,6 @@ """Support for MQTT message handling.""" import asyncio +import sys from functools import partial, wraps import inspect from itertools import groupby @@ -15,6 +16,8 @@ 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 @@ -36,6 +39,7 @@ from homeassistant.exceptions import ( ConfigEntryNotReady, ) from homeassistant.helpers import config_validation as cv, template +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType, HomeAssistantType, ServiceDataType from homeassistant.loader import bind_hass @@ -50,7 +54,12 @@ from .const import ( DEFAULT_DISCOVERY, CONF_STATE_TOPIC, ATTR_DISCOVERY_HASH, + PROTOCOL_311, + DEFAULT_QOS, ) +from .discovery import MQTT_DISCOVERY_UPDATED, clear_discovery_hash +from .models import PublishPayloadType, Message, MessageCallbackType +from .subscription import async_subscribe_topics, async_unsubscribe_topics _LOGGER = logging.getLogger(__name__) @@ -95,11 +104,9 @@ CONF_VIA_DEVICE = "via_device" CONF_DEPRECATED_VIA_HUB = "via_hub" PROTOCOL_31 = "3.1" -PROTOCOL_311 = "3.1.1" DEFAULT_PORT = 1883 DEFAULT_KEEPALIVE = 60 -DEFAULT_QOS = 0 DEFAULT_RETAIN = False DEFAULT_PROTOCOL = PROTOCOL_311 DEFAULT_DISCOVERY_PREFIX = "homeassistant" @@ -329,23 +336,9 @@ MQTT_PUBLISH_SCHEMA = vol.Schema( # pylint: disable=invalid-name -PublishPayloadType = Union[str, bytes, int, float, None] SubscribePayloadType = Union[str, bytes] # Only bytes if encoding is None -@attr.s(slots=True, frozen=True) -class Message: - """MQTT Message.""" - - topic = attr.ib(type=str) - payload = attr.ib(type=PublishPayloadType) - qos = attr.ib(type=int) - retain = attr.ib(type=bool) - - -MessageCallbackType = Callable[[Message], None] - - def _build_publish_data(topic: Any, qos: int, retain: bool) -> ServiceDataType: """Build the arguments for the publish service without the payload.""" data = {ATTR_TOPIC: topic} @@ -629,8 +622,6 @@ async def async_setup_entry(hass, entry): elif conf_tls_version == "1.0": tls_version = ssl.PROTOCOL_TLSv1 else: - import sys - # Python3.6 supports automatic negotiation of highest TLS version if sys.hexversion >= 0x03060000: tls_version = ssl.PROTOCOL_TLS # pylint: disable=no-member @@ -735,8 +726,6 @@ class MQTT: tls_version: Optional[int], ) -> None: """Initialize Home Assistant MQTT client.""" - import paho.mqtt.client as mqtt - self.hass = hass self.broker = broker self.port = port @@ -776,7 +765,9 @@ class MQTT: self._mqttc.on_message = self._mqtt_on_message if will_message is not None: - self._mqttc.will_set(*attr.astuple(will_message)) + self._mqttc.will_set( # pylint: disable=no-value-for-parameter + *attr.astuple(will_message) + ) async def async_publish( self, topic: str, payload: PublishPayloadType, qos: int, retain: bool @@ -806,8 +797,6 @@ class MQTT: return CONNECTION_FAILED_RECOVERABLE if result != 0: - import paho.mqtt.client as mqtt - _LOGGER.error("Failed to connect: %s", mqtt.error_string(result)) return CONNECTION_FAILED @@ -889,8 +878,6 @@ class MQTT: Resubscribe to all topics we were subscribed to and publish birth message. """ - import paho.mqtt.client as mqtt - if result_code != mqtt.CONNACK_ACCEPTED: _LOGGER.error( "Unable to connect to the MQTT broker: %s", @@ -909,7 +896,11 @@ class MQTT: self.hass.add_job(self._async_perform_subscription, topic, max_qos) if self.birth_message: - self.hass.add_job(self.async_publish(*attr.astuple(self.birth_message))) + self.hass.add_job( + self.async_publish( # pylint: disable=no-value-for-parameter + *attr.astuple(self.birth_message) + ) + ) def _mqtt_on_message(self, _mqttc, _userdata, msg) -> None: """Message received callback.""" @@ -934,10 +925,11 @@ class MQTT: payload = msg.payload.decode(subscription.encoding) except (AttributeError, UnicodeDecodeError): _LOGGER.warning( - "Can't decode payload %s on %s with encoding %s", + "Can't decode payload %s on %s with encoding %s (for %s)", msg.payload, msg.topic, subscription.encoding, + subscription.callback, ) continue @@ -978,8 +970,6 @@ class MQTT: def _raise_on_error(result_code: int) -> None: """Raise error if error result.""" if result_code != 0: - import paho.mqtt.client as mqtt - raise HomeAssistantError( "Error talking to MQTT: {}".format(mqtt.error_string(result_code)) ) @@ -987,8 +977,6 @@ def _raise_on_error(result_code: int) -> None: def _match_topic(subscription: str, topic: str) -> bool: """Test if topic matches subscription.""" - from paho.mqtt.matcher import MQTTMatcher - matcher = MQTTMatcher() matcher[subscription] = True try: @@ -1022,8 +1010,6 @@ class MqttAttributes(Entity): async def _attributes_subscribe_topics(self): """(Re)Subscribe to topics.""" - from .subscription import async_subscribe_topics - attr_tpl = self._attributes_config.get(CONF_JSON_ATTRS_TEMPLATE) if attr_tpl is not None: attr_tpl.hass = self.hass @@ -1059,8 +1045,6 @@ class MqttAttributes(Entity): async def async_will_remove_from_hass(self): """Unsubscribe when removed.""" - from .subscription import async_unsubscribe_topics - self._attributes_sub_state = await async_unsubscribe_topics( self.hass, self._attributes_sub_state ) @@ -1096,7 +1080,6 @@ class MqttAvailability(Entity): async def _availability_subscribe_topics(self): """(Re)Subscribe to topics.""" - from .subscription import async_subscribe_topics @callback def availability_message_received(msg: Message) -> None: @@ -1122,8 +1105,6 @@ class MqttAvailability(Entity): async def async_will_remove_from_hass(self): """Unsubscribe when removed.""" - from .subscription import async_unsubscribe_topics - self._availability_sub_state = await async_unsubscribe_topics( self.hass, self._availability_sub_state ) @@ -1148,9 +1129,6 @@ class MqttDiscoveryUpdate(Entity): """Subscribe to discovery updates.""" await super().async_added_to_hass() - from homeassistant.helpers.dispatcher import async_dispatcher_connect - from .discovery import MQTT_DISCOVERY_UPDATED, clear_discovery_hash - @callback def discovery_callback(payload): """Handle discovery update.""" diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index 2350dfc6634..5e995494a64 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -88,11 +88,13 @@ ABBREVIATIONS = { "pl_cls": "payload_close", "pl_disarm": "payload_disarm", "pl_hi_spd": "payload_high_speed", + "pl_home": "payload_home", "pl_lock": "payload_lock", "pl_loc": "payload_locate", "pl_lo_spd": "payload_low_speed", "pl_med_spd": "payload_medium_speed", "pl_not_avail": "payload_not_available", + "pl_not_home": "payload_not_home", "pl_off": "payload_off", "pl_off_spd": "payload_off_speed", "pl_on": "payload_on", @@ -128,6 +130,7 @@ ABBREVIATIONS = { "spd_stat_t": "speed_state_topic", "spd_val_tpl": "speed_value_template", "spds": "speeds", + "src_type": "source_type", "stat_clsd": "state_closed", "stat_off": "state_off", "stat_on": "state_on", diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index d3c6ee819b5..a8a378e723c 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -3,6 +3,7 @@ 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 ( @@ -125,8 +126,6 @@ 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/const.py b/homeassistant/components/mqtt/const.py index b365ee9d33e..3234bebbfc1 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -5,3 +5,5 @@ DEFAULT_DISCOVERY = False ATTR_DISCOVERY_HASH = "discovery_hash" CONF_STATE_TOPIC = "state_topic" +PROTOCOL_311 = "3.1.1" +DEFAULT_QOS = 0 diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 66e14ca9a5a..e6cfab90c26 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -90,7 +90,7 @@ DEFAULT_TILT_MIN = 0 DEFAULT_TILT_OPEN_POSITION = 100 DEFAULT_TILT_OPTIMISTIC = False -OPEN_CLOSE_FEATURES = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP +OPEN_CLOSE_FEATURES = SUPPORT_OPEN | SUPPORT_CLOSE TILT_FEATURES = ( SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT @@ -122,7 +122,9 @@ PLATFORM_SCHEMA = vol.All( vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_PAYLOAD_CLOSE, default=DEFAULT_PAYLOAD_CLOSE): cv.string, vol.Optional(CONF_PAYLOAD_OPEN, default=DEFAULT_PAYLOAD_OPEN): cv.string, - vol.Optional(CONF_PAYLOAD_STOP, default=DEFAULT_PAYLOAD_STOP): cv.string, + vol.Optional(CONF_PAYLOAD_STOP, default=DEFAULT_PAYLOAD_STOP): vol.Any( + cv.string, None + ), vol.Optional(CONF_POSITION_CLOSED, default=DEFAULT_POSITION_CLOSED): int, vol.Optional(CONF_POSITION_OPEN, default=DEFAULT_POSITION_OPEN): int, vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, @@ -396,6 +398,9 @@ class MqttCover( if self._config.get(CONF_COMMAND_TOPIC) is not None: supported_features = OPEN_CLOSE_FEATURES + if self._config.get(CONF_PAYLOAD_STOP) is not None: + supported_features |= SUPPORT_STOP + if self._config.get(CONF_SET_POSITION_TOPIC) is not None: supported_features |= SUPPORT_SET_POSITION diff --git a/homeassistant/components/mqtt/device_tracker.py b/homeassistant/components/mqtt/device_tracker.py index e9613e09a95..d25d7ce21d3 100644 --- a/homeassistant/components/mqtt/device_tracker.py +++ b/homeassistant/components/mqtt/device_tracker.py @@ -4,17 +4,26 @@ import logging import voluptuous as vol from homeassistant.components import mqtt -from homeassistant.components.device_tracker import PLATFORM_SCHEMA -from homeassistant.const import CONF_DEVICES +from homeassistant.components.device_tracker import PLATFORM_SCHEMA, SOURCE_TYPES 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 _LOGGER = logging.getLogger(__name__) +CONF_PAYLOAD_HOME = "payload_home" +CONF_PAYLOAD_NOT_HOME = "payload_not_home" +CONF_SOURCE_TYPE = "source_type" + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(mqtt.SCHEMA_BASE).extend( - {vol.Required(CONF_DEVICES): {cv.string: mqtt.valid_subscribe_topic}} + { + vol.Required(CONF_DEVICES): {cv.string: mqtt.valid_subscribe_topic}, + vol.Optional(CONF_PAYLOAD_HOME, default=STATE_HOME): cv.string, + vol.Optional(CONF_PAYLOAD_NOT_HOME, default=STATE_NOT_HOME): cv.string, + vol.Optional(CONF_SOURCE_TYPE): vol.In(SOURCE_TYPES), + } ) @@ -22,13 +31,27 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None): """Set up the MQTT tracker.""" devices = config[CONF_DEVICES] qos = config[CONF_QOS] + payload_home = config[CONF_PAYLOAD_HOME] + payload_not_home = config[CONF_PAYLOAD_NOT_HOME] + source_type = config.get(CONF_SOURCE_TYPE) for dev_id, topic in devices.items(): @callback def async_message_received(msg, dev_id=dev_id): """Handle received MQTT message.""" - hass.async_create_task(async_see(dev_id=dev_id, location_name=msg.payload)) + if msg.payload == payload_home: + location_name = STATE_HOME + elif msg.payload == payload_not_home: + location_name = STATE_NOT_HOME + else: + location_name = msg.payload + + see_args = {"dev_id": dev_id, "location_name": location_name} + if source_type: + see_args["source_type"] = source_type + + hass.async_create_task(async_see(**see_args)) await mqtt.async_subscribe(hass, topic, async_message_received, qos) diff --git a/homeassistant/components/mqtt/light/__init__.py b/homeassistant/components/mqtt/light/__init__.py index 688cef03467..95a850fb9e8 100644 --- a/homeassistant/components/mqtt/light/__init__.py +++ b/homeassistant/components/mqtt/light/__init__.py @@ -16,34 +16,24 @@ from homeassistant.components.mqtt.discovery import ( ) from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import HomeAssistantType, ConfigType +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 +from .schema_template import PLATFORM_SCHEMA_TEMPLATE, async_setup_entity_template _LOGGER = logging.getLogger(__name__) -CONF_SCHEMA = "schema" - def validate_mqtt_light(value): """Validate MQTT light schema.""" - from . import schema_basic - from . import schema_json - from . import schema_template - schemas = { - "basic": schema_basic.PLATFORM_SCHEMA_BASIC, - "json": schema_json.PLATFORM_SCHEMA_JSON, - "template": schema_template.PLATFORM_SCHEMA_TEMPLATE, + "basic": PLATFORM_SCHEMA_BASIC, + "json": PLATFORM_SCHEMA_JSON, + "template": PLATFORM_SCHEMA_TEMPLATE, } return schemas[value[CONF_SCHEMA]](value) -MQTT_LIGHT_SCHEMA_SCHEMA = vol.Schema( - { - vol.Optional(CONF_SCHEMA, default="basic"): vol.All( - vol.Lower, vol.Any("basic", "json", "template") - ) - } -) - PLATFORM_SCHEMA = vol.All( MQTT_LIGHT_SCHEMA_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA), validate_mqtt_light ) @@ -81,14 +71,10 @@ async def _async_setup_entity( config, async_add_entities, config_entry=None, discovery_hash=None ): """Set up a MQTT Light.""" - from . import schema_basic - from . import schema_json - from . import schema_template - setup_entity = { - "basic": schema_basic.async_setup_entity_basic, - "json": schema_json.async_setup_entity_json, - "template": schema_template.async_setup_entity_template, + "basic": async_setup_entity_basic, + "json": async_setup_entity_json, + "template": async_setup_entity_template, } await setup_entity[config[CONF_SCHEMA]]( config, async_add_entities, config_entry, discovery_hash diff --git a/homeassistant/components/mqtt/light/schema.py b/homeassistant/components/mqtt/light/schema.py new file mode 100644 index 00000000000..a7ab5e986a7 --- /dev/null +++ b/homeassistant/components/mqtt/light/schema.py @@ -0,0 +1,12 @@ +"""Shared schema code.""" +import voluptuous as vol + +CONF_SCHEMA = "schema" + +MQTT_LIGHT_SCHEMA_SCHEMA = vol.Schema( + { + vol.Optional(CONF_SCHEMA, default="basic"): vol.All( + vol.Lower, vol.Any("basic", "json", "template") + ) + } +) diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index 216762f9b2b..829809dd9c3 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -56,7 +56,7 @@ from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util -from . import MQTT_LIGHT_SCHEMA_SCHEMA +from .schema import MQTT_LIGHT_SCHEMA_SCHEMA _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 1a46cd5e535..c4de1edbc3c 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -59,7 +59,7 @@ from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType import homeassistant.util.color as color_util -from . import MQTT_LIGHT_SCHEMA_SCHEMA +from .schema import MQTT_LIGHT_SCHEMA_SCHEMA from .schema_basic import CONF_BRIGHTNESS_SCALE _LOGGER = logging.getLogger(__name__) @@ -463,7 +463,7 @@ class MqttLightJson( message["flash"] = self._flash_times[CONF_FLASH_TIME_SHORT] if ATTR_TRANSITION in kwargs: - message["transition"] = int(kwargs[ATTR_TRANSITION]) + message["transition"] = kwargs[ATTR_TRANSITION] if ATTR_BRIGHTNESS in kwargs and self._brightness is not None: message["brightness"] = int( @@ -521,7 +521,7 @@ class MqttLightJson( message = {"state": "OFF"} if ATTR_TRANSITION in kwargs: - message["transition"] = int(kwargs[ATTR_TRANSITION]) + message["transition"] = kwargs[ATTR_TRANSITION] mqtt.async_publish( self.hass, diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index 410eff6143f..c80ab2f95a7 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -49,7 +49,7 @@ import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util from homeassistant.helpers.restore_state import RestoreEntity -from . import MQTT_LIGHT_SCHEMA_SCHEMA +from .schema import MQTT_LIGHT_SCHEMA_SCHEMA _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py new file mode 100644 index 00000000000..5f014aadd08 --- /dev/null +++ b/homeassistant/components/mqtt/models.py @@ -0,0 +1,20 @@ +"""Modesl used by multiple MQTT modules.""" +from typing import Union, Callable + +import attr + +# pylint: disable=invalid-name +PublishPayloadType = Union[str, bytes, int, float, None] + + +@attr.s(slots=True, frozen=True) +class Message: + """MQTT Message.""" + + topic = attr.ib(type=str) + payload = attr.ib(type=PublishPayloadType) + qos = attr.ib(type=int) + retain = attr.ib(type=bool) + + +MessageCallbackType = Callable[[Message], None] diff --git a/homeassistant/components/mqtt/server.py b/homeassistant/components/mqtt/server.py index 2c70d18d772..f5d369a75c7 100644 --- a/homeassistant/components/mqtt/server.py +++ b/homeassistant/components/mqtt/server.py @@ -4,10 +4,14 @@ 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 +from .const import PROTOCOL_311 + _LOGGER = logging.getLogger(__name__) # None allows custom config to be created through generate_config @@ -33,8 +37,6 @@ def async_start(hass, password, server_config): This method is a coroutine. """ - from hbmqtt.broker import Broker, BrokerException - passwd = tempfile.NamedTemporaryFile() gen_server_config, client_config = generate_config(hass, passwd, password) @@ -63,8 +65,6 @@ def async_start(hass, password, server_config): def generate_config(hass, passwd, password): """Generate a configuration based on current Home Assistant instance.""" - from . import PROTOCOL_311 - config = { "listeners": { "default": { @@ -83,8 +83,6 @@ def generate_config(hass, passwd, password): username = "homeassistant" # Encrypt with what hbmqtt uses to verify - from passlib.apps import custom_app_context - passwd.write( "homeassistant:{}\n".format(custom_app_context.encrypt(password)).encode( "utf-8" diff --git a/homeassistant/components/mqtt/subscription.py b/homeassistant/components/mqtt/subscription.py index d85399b5dcb..be48a769a23 100644 --- a/homeassistant/components/mqtt/subscription.py +++ b/homeassistant/components/mqtt/subscription.py @@ -8,7 +8,8 @@ from homeassistant.components import mqtt from homeassistant.helpers.typing import HomeAssistantType from homeassistant.loader import bind_hass -from . import DEFAULT_QOS, MessageCallbackType +from .const import DEFAULT_QOS +from .models import MessageCallbackType _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/mqtt/vacuum/__init__.py b/homeassistant/components/mqtt/vacuum/__init__.py index 5fdaa744ca9..12fd4c51693 100644 --- a/homeassistant/components/mqtt/vacuum/__init__.py +++ b/homeassistant/components/mqtt/vacuum/__init__.py @@ -15,51 +15,19 @@ from homeassistant.components.mqtt.discovery import ( clear_discovery_hash, ) from homeassistant.helpers.dispatcher import async_dispatcher_connect +from .schema import CONF_SCHEMA, LEGACY, STATE, MQTT_VACUUM_SCHEMA +from .schema_legacy import PLATFORM_SCHEMA_LEGACY, async_setup_entity_legacy +from .schema_state import PLATFORM_SCHEMA_STATE, async_setup_entity_state _LOGGER = logging.getLogger(__name__) -CONF_SCHEMA = "schema" -LEGACY = "legacy" -STATE = "state" - def validate_mqtt_vacuum(value): """Validate MQTT vacuum schema.""" - from . import schema_legacy - from . import schema_state - - schemas = { - LEGACY: schema_legacy.PLATFORM_SCHEMA_LEGACY, - STATE: schema_state.PLATFORM_SCHEMA_STATE, - } + schemas = {LEGACY: PLATFORM_SCHEMA_LEGACY, STATE: PLATFORM_SCHEMA_STATE} return schemas[value[CONF_SCHEMA]](value) -def services_to_strings(services, service_to_string): - """Convert SUPPORT_* service bitmask to list of service strings.""" - strings = [] - for service in service_to_string: - if service & services: - strings.append(service_to_string[service]) - return strings - - -def strings_to_services(strings, string_to_service): - """Convert service strings to SUPPORT_* service bitmask.""" - services = 0 - for string in strings: - services |= string_to_service[string] - return services - - -MQTT_VACUUM_SCHEMA = vol.Schema( - { - vol.Optional(CONF_SCHEMA, default=LEGACY): vol.All( - vol.Lower, vol.Any(LEGACY, STATE) - ) - } -) - PLATFORM_SCHEMA = vol.All( MQTT_VACUUM_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA), validate_mqtt_vacuum ) @@ -95,13 +63,7 @@ async def _async_setup_entity( config, async_add_entities, config_entry, discovery_hash=None ): """Set up the MQTT vacuum.""" - from . import schema_legacy - from . import schema_state - - setup_entity = { - LEGACY: schema_legacy.async_setup_entity_legacy, - STATE: schema_state.async_setup_entity_state, - } + setup_entity = {LEGACY: async_setup_entity_legacy, STATE: async_setup_entity_state} await setup_entity[config[CONF_SCHEMA]]( config, async_add_entities, config_entry, discovery_hash ) diff --git a/homeassistant/components/mqtt/vacuum/schema.py b/homeassistant/components/mqtt/vacuum/schema.py new file mode 100644 index 00000000000..949b5cede9c --- /dev/null +++ b/homeassistant/components/mqtt/vacuum/schema.py @@ -0,0 +1,31 @@ +"""Shared schema code.""" +import voluptuous as vol + +CONF_SCHEMA = "schema" +LEGACY = "legacy" +STATE = "state" + +MQTT_VACUUM_SCHEMA = vol.Schema( + { + vol.Optional(CONF_SCHEMA, default=LEGACY): vol.All( + vol.Lower, vol.Any(LEGACY, STATE) + ) + } +) + + +def services_to_strings(services, service_to_string): + """Convert SUPPORT_* service bitmask to list of service strings.""" + strings = [] + for service in service_to_string: + if service & services: + strings.append(service_to_string[service]) + return strings + + +def strings_to_services(strings, string_to_service): + """Convert service strings to SUPPORT_* service bitmask.""" + services = 0 + for string in strings: + services |= string_to_service[string] + return services diff --git a/homeassistant/components/mqtt/vacuum/schema_legacy.py b/homeassistant/components/mqtt/vacuum/schema_legacy.py index f2fa8f8da66..d770cfbb7f8 100644 --- a/homeassistant/components/mqtt/vacuum/schema_legacy.py +++ b/homeassistant/components/mqtt/vacuum/schema_legacy.py @@ -33,7 +33,7 @@ from homeassistant.components.mqtt import ( subscription, ) -from . import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services +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 1ab415aef7b..40b3eeb752c 100644 --- a/homeassistant/components/mqtt/vacuum/schema_state.py +++ b/homeassistant/components/mqtt/vacuum/schema_state.py @@ -46,7 +46,7 @@ from homeassistant.components.mqtt import ( CONF_QOS, ) -from . import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services +from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/msteams/__init__.py b/homeassistant/components/msteams/__init__.py new file mode 100644 index 00000000000..42423887fa6 --- /dev/null +++ b/homeassistant/components/msteams/__init__.py @@ -0,0 +1 @@ +"""The Microsoft Teams component.""" diff --git a/homeassistant/components/msteams/manifest.json b/homeassistant/components/msteams/manifest.json new file mode 100644 index 00000000000..f907cf570bb --- /dev/null +++ b/homeassistant/components/msteams/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "msteams", + "name": "Microsoft Teams", + "documentation": "https://www.home-assistant.io/integrations/msteams", + "requirements": ["pymsteams==0.1.12"], + "dependencies": [], + "codeowners": ["@peroyvind"] +} diff --git a/homeassistant/components/msteams/notify.py b/homeassistant/components/msteams/notify.py new file mode 100644 index 00000000000..c986f1d2363 --- /dev/null +++ b/homeassistant/components/msteams/notify.py @@ -0,0 +1,67 @@ +"""Microsoft Teams platform for notify component.""" +import logging + +import pymsteams +import voluptuous as vol + +from homeassistant.components.notify import ( + ATTR_DATA, + ATTR_TITLE, + ATTR_TITLE_DEFAULT, + PLATFORM_SCHEMA, + BaseNotificationService, +) +from homeassistant.const import CONF_URL +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +ATTR_FILE_URL = "image_url" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_URL): cv.url}) + + +def get_service(hass, config, discovery_info=None): + """Get the Microsoft Teams notification service.""" + webhook_url = config.get(CONF_URL) + + try: + return MSTeamsNotificationService(webhook_url) + + except RuntimeError as err: + _LOGGER.exception("Error in creating a new Microsoft Teams message: %s", err) + return None + + +class MSTeamsNotificationService(BaseNotificationService): + """Implement the notification service for Microsoft Teams.""" + + def __init__(self, webhook_url): + """Initialize the service.""" + self._webhook_url = webhook_url + self.teams_message = pymsteams.connectorcard(self._webhook_url) + + def send_message(self, message=None, **kwargs): + """Send a message to the webhook.""" + title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) + data = kwargs.get(ATTR_DATA) + + self.teams_message.title(title) + + self.teams_message.text(message) + + if data is not None: + file_url = data.get(ATTR_FILE_URL) + + if file_url is not None: + if not file_url.startswith("http"): + _LOGGER.error("URL should start with http or https") + return + + message_section = pymsteams.cardsection() + message_section.addImage(file_url) + self.teams_message.addSection(message_section) + try: + self.teams_message.send() + except RuntimeError as err: + _LOGGER.error("Could not send notification. Error: %s", err) diff --git a/homeassistant/components/mvglive/sensor.py b/homeassistant/components/mvglive/sensor.py index 3c753d832e0..da1db0e02aa 100644 --- a/homeassistant/components/mvglive/sensor.py +++ b/homeassistant/components/mvglive/sensor.py @@ -1,14 +1,15 @@ """Support for departure information for public transport in Munich.""" -import logging -from datetime import timedelta - from copy import deepcopy +from datetime import timedelta +import logging + +import MVGLive import voluptuous as vol +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ATTR_ATTRIBUTION, 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 CONF_NAME, ATTR_ATTRIBUTION _LOGGER = logging.getLogger(__name__) @@ -150,8 +151,6 @@ class MVGLiveData: self, station, destinations, directions, lines, products, timeoffset, number ): """Initialize the sensor.""" - import MVGLive - self._station = station self._destinations = destinations self._directions = directions diff --git a/homeassistant/components/mychevy/__init__.py b/homeassistant/components/mychevy/__init__.py index 8ec83ed374b..0ec4d05a623 100644 --- a/homeassistant/components/mychevy/__init__.py +++ b/homeassistant/components/mychevy/__init__.py @@ -4,11 +4,11 @@ import logging import threading import time +import mychevy.mychevy as mc import voluptuous as vol from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers import discovery +from homeassistant.helpers import config_validation as cv, discovery from homeassistant.util import Throttle DOMAIN = "mychevy" @@ -71,8 +71,6 @@ class EVBinarySensorConfig: def setup(hass, base_config): """Set up the mychevy component.""" - import mychevy.mychevy as mc - config = base_config.get(DOMAIN) email = config.get(CONF_USERNAME) diff --git a/homeassistant/components/mythicbeastsdns/__init__.py b/homeassistant/components/mythicbeastsdns/__init__.py index 993b62ac48d..d961c2e6e3d 100644 --- a/homeassistant/components/mythicbeastsdns/__init__.py +++ b/homeassistant/components/mythicbeastsdns/__init__.py @@ -1,10 +1,10 @@ """Support for Mythic Beasts Dynamic DNS service.""" -import logging from datetime import timedelta +import logging +import mbddns import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( CONF_DOMAIN, CONF_HOST, @@ -12,6 +12,7 @@ from homeassistant.const import ( CONF_SCAN_INTERVAL, ) from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_time_interval _LOGGER = logging.getLogger(__name__) @@ -39,8 +40,6 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass, config): """Initialize the Mythic Beasts component.""" - import mbddns - domain = config[DOMAIN][CONF_DOMAIN] password = config[DOMAIN][CONF_PASSWORD] host = config[DOMAIN][CONF_HOST] diff --git a/homeassistant/components/namecheapdns/__init__.py b/homeassistant/components/namecheapdns/__init__.py index 56c50ff52f8..fbc78f622a1 100644 --- a/homeassistant/components/namecheapdns/__init__.py +++ b/homeassistant/components/namecheapdns/__init__.py @@ -1,13 +1,14 @@ """Support for namecheap DNS services.""" -import logging from datetime import timedelta +import logging +import defusedxml.ElementTree as ET import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_DOMAIN -from homeassistant.helpers.event import async_track_time_interval +from homeassistant.const import CONF_DOMAIN, CONF_HOST, CONF_PASSWORD from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import async_track_time_interval _LOGGER = logging.getLogger(__name__) @@ -55,8 +56,6 @@ async def async_setup(hass, config): async def _update_namecheapdns(session, host, domain, password): """Update namecheap DNS entry.""" - import defusedxml.ElementTree as ET - params = {"host": host, "domain": domain, "password": password} resp = await session.get(UPDATE_URL, params=params) diff --git a/homeassistant/components/neato/.translations/da.json b/homeassistant/components/neato/.translations/da.json index 7f0d122f38b..ca180efa005 100644 --- a/homeassistant/components/neato/.translations/da.json +++ b/homeassistant/components/neato/.translations/da.json @@ -4,6 +4,9 @@ "already_configured": "Allerede konfigureret", "invalid_credentials": "Ugyldige legitimationsoplysninger" }, + "create_entry": { + "default": "Se [Neato-dokumentation] ({docs_url})." + }, "error": { "invalid_credentials": "Ugyldige legitimationsoplysninger", "unexpected_error": "Uventet fejl" diff --git a/homeassistant/components/neato/.translations/ko.json b/homeassistant/components/neato/.translations/ko.json new file mode 100644 index 00000000000..aeb591f7b20 --- /dev/null +++ b/homeassistant/components/neato/.translations/ko.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "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." + }, + "error": { + "invalid_credentials": "\uc0ac\uc6a9\uc790 \uc774\ub984 \ud639\uc740 \ube44\ubc00\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unexpected_error": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984", + "vendor": "\uacf5\uae09 \uc5c5\uccb4" + }, + "description": "[Neato \uc124\uba85\uc11c] ({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694.", + "title": "Neato \uacc4\uc815 \uc815\ubcf4" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/nl.json b/homeassistant/components/neato/.translations/nl.json new file mode 100644 index 00000000000..4846f0180f1 --- /dev/null +++ b/homeassistant/components/neato/.translations/nl.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Al geconfigureerd", + "invalid_credentials": "Ongeldige gebruikersgegevens" + }, + "create_entry": { + "default": "Zie [Neato-documentatie] ({docs_url})." + }, + "error": { + "invalid_credentials": "Ongeldige inloggegevens", + "unexpected_error": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "password": "Wachtwoord", + "username": "Gebruikersnaam", + "vendor": "Leverancier" + }, + "description": "Zie [Neato-documentatie] ({docs_url}).", + "title": "Neato-account info" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/pt-BR.json b/homeassistant/components/neato/.translations/pt-BR.json new file mode 100644 index 00000000000..8c4c45f9c89 --- /dev/null +++ b/homeassistant/components/neato/.translations/pt-BR.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/ru.json b/homeassistant/components/neato/.translations/ru.json index 1a206258e24..999e45880cf 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\u0435\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\u0435\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 e17c562171a..ddf9789f678 100644 --- a/homeassistant/components/neato/__init__.py +++ b/homeassistant/components/neato/__init__.py @@ -1,230 +1,170 @@ """Support for Neato botvac connected vacuum cleaners.""" +import asyncio import logging from datetime import timedelta -from urllib.error import HTTPError import voluptuous as vol +from pybotvac import Account, Neato, Vorwerk +from pybotvac.exceptions import NeatoException, NeatoLoginException, NeatoRobotException -import homeassistant.helpers.config_validation as cv +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.helpers import discovery +from homeassistant.helpers import config_validation as cv from homeassistant.util import Throttle +from .config_flow import NeatoConfigFlow +from .const import ( + CONF_VENDOR, + NEATO_CONFIG, + NEATO_DOMAIN, + NEATO_LOGIN, + NEATO_MAP_DATA, + NEATO_PERSISTENT_MAPS, + NEATO_ROBOTS, + SCAN_INTERVAL_MINUTES, + VALID_VENDORS, +) + _LOGGER = logging.getLogger(__name__) -CONF_VENDOR = "vendor" -DOMAIN = "neato" -NEATO_ROBOTS = "neato_robots" -NEATO_LOGIN = "neato_login" -NEATO_MAP_DATA = "neato_map_data" -NEATO_PERSISTENT_MAPS = "neato_persistent_maps" CONFIG_SCHEMA = vol.Schema( { - DOMAIN: vol.Schema( + NEATO_DOMAIN: vol.Schema( { vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_VENDOR, default="neato"): vol.In( - ["neato", "vorwerk"] - ), + vol.Optional(CONF_VENDOR, default="neato"): vol.In(VALID_VENDORS), } ) }, extra=vol.ALLOW_EXTRA, ) -MODE = {1: "Eco", 2: "Turbo"} -ACTION = { - 0: "Invalid", - 1: "House Cleaning", - 2: "Spot Cleaning", - 3: "Manual Cleaning", - 4: "Docking", - 5: "User Menu Active", - 6: "Suspended Cleaning", - 7: "Updating", - 8: "Copying logs", - 9: "Recovering Location", - 10: "IEC test", - 11: "Map cleaning", - 12: "Exploring map (creating a persistent map)", - 13: "Acquiring Persistent Map IDs", - 14: "Creating & Uploading Map", - 15: "Suspended Exploration", -} - -ERRORS = { - "ui_error_battery_battundervoltlithiumsafety": "Replace battery", - "ui_error_battery_critical": "Replace battery", - "ui_error_battery_invalidsensor": "Replace battery", - "ui_error_battery_lithiumadapterfailure": "Replace battery", - "ui_error_battery_mismatch": "Replace battery", - "ui_error_battery_nothermistor": "Replace battery", - "ui_error_battery_overtemp": "Replace battery", - "ui_error_battery_overvolt": "Replace battery", - "ui_error_battery_undercurrent": "Replace battery", - "ui_error_battery_undertemp": "Replace battery", - "ui_error_battery_undervolt": "Replace battery", - "ui_error_battery_unplugged": "Replace battery", - "ui_error_brush_stuck": "Brush stuck", - "ui_error_brush_overloaded": "Brush overloaded", - "ui_error_bumper_stuck": "Bumper stuck", - "ui_error_check_battery_switch": "Check battery", - "ui_error_corrupt_scb": "Call customer service corrupt board", - "ui_error_deck_debris": "Deck debris", - "ui_error_dflt_app": "Check Neato app", - "ui_error_disconnect_chrg_cable": "Disconnected charge cable", - "ui_error_disconnect_usb_cable": "Disconnected USB cable", - "ui_error_dust_bin_missing": "Dust bin missing", - "ui_error_dust_bin_full": "Dust bin full", - "ui_error_dust_bin_emptied": "Dust bin emptied", - "ui_error_hardware_failure": "Hardware failure", - "ui_error_ldrop_stuck": "Clear my path", - "ui_error_lds_jammed": "Clear my path", - "ui_error_lds_bad_packets": "Check Neato app", - "ui_error_lds_disconnected": "Check Neato app", - "ui_error_lds_missed_packets": "Check Neato app", - "ui_error_lwheel_stuck": "Clear my path", - "ui_error_navigation_backdrop_frontbump": "Clear my path", - "ui_error_navigation_backdrop_leftbump": "Clear my path", - "ui_error_navigation_backdrop_wheelextended": "Clear my path", - "ui_error_navigation_noprogress": "Clear my path", - "ui_error_navigation_origin_unclean": "Clear my path", - "ui_error_navigation_pathproblems": "Cannot return to base", - "ui_error_navigation_pinkycommsfail": "Clear my path", - "ui_error_navigation_falling": "Clear my path", - "ui_error_navigation_noexitstogo": "Clear my path", - "ui_error_navigation_nomotioncommands": "Clear my path", - "ui_error_navigation_rightdrop_leftbump": "Clear my path", - "ui_error_navigation_undockingfailed": "Clear my path", - "ui_error_picked_up": "Picked up", - "ui_error_qa_fail": "Check Neato app", - "ui_error_rdrop_stuck": "Clear my path", - "ui_error_reconnect_failed": "Reconnect failed", - "ui_error_rwheel_stuck": "Clear my path", - "ui_error_stuck": "Stuck!", - "ui_error_unable_to_return_to_base": "Unable to return to base", - "ui_error_unable_to_see": "Clean vacuum sensors", - "ui_error_vacuum_slip": "Clear my path", - "ui_error_vacuum_stuck": "Clear my path", - "ui_error_warning": "Error check app", - "batt_base_connect_fail": "Battery failed to connect to base", - "batt_base_no_power": "Battery base has no power", - "batt_low": "Battery low", - "batt_on_base": "Battery on base", - "clean_tilt_on_start": "Clean the tilt on start", - "dustbin_full": "Dust bin full", - "dustbin_missing": "Dust bin missing", - "gen_picked_up": "Picked up", - "hw_fail": "Hardware failure", - "hw_tof_sensor_sensor": "Hardware sensor disconnected", - "lds_bad_packets": "Bad packets", - "lds_deck_debris": "Debris on deck", - "lds_disconnected": "Disconnected", - "lds_jammed": "Jammed", - "lds_missed_packets": "Missed packets", - "maint_brush_stuck": "Brush stuck", - "maint_brush_overload": "Brush overloaded", - "maint_bumper_stuck": "Bumper stuck", - "maint_customer_support_qa": "Contact customer support", - "maint_vacuum_stuck": "Vacuum is stuck", - "maint_vacuum_slip": "Vacuum is stuck", - "maint_left_drop_stuck": "Vacuum is stuck", - "maint_left_wheel_stuck": "Vacuum is stuck", - "maint_right_drop_stuck": "Vacuum is stuck", - "maint_right_wheel_stuck": "Vacuum is stuck", - "not_on_charge_base": "Not on the charge base", - "nav_robot_falling": "Clear my path", - "nav_no_path": "Clear my path", - "nav_path_problem": "Clear my path", - "nav_backdrop_frontbump": "Clear my path", - "nav_backdrop_leftbump": "Clear my path", - "nav_backdrop_wheelextended": "Clear my path", - "nav_mag_sensor": "Clear my path", - "nav_no_exit": "Clear my path", - "nav_no_movement": "Clear my path", - "nav_rightdrop_leftbump": "Clear my path", - "nav_undocking_failed": "Clear my path", -} - -ALERTS = { - "ui_alert_dust_bin_full": "Please empty dust bin", - "ui_alert_recovering_location": "Returning to start", - "ui_alert_battery_chargebasecommerr": "Battery error", - "ui_alert_busy_charging": "Busy charging", - "ui_alert_charging_base": "Base charging", - "ui_alert_charging_power": "Charging power", - "ui_alert_connect_chrg_cable": "Connect charge cable", - "ui_alert_info_thank_you": "Thank you", - "ui_alert_invalid": "Invalid check app", - "ui_alert_old_error": "Old error", - "ui_alert_swupdate_fail": "Update failed", - "dustbin_full": "Please empty dust bin", - "maint_brush_change": "Change the brush", - "maint_filter_change": "Change the filter", - "clean_completed_to_start": "Cleaning completed", - "nav_floorplan_not_created": "No floorplan found", - "nav_floorplan_load_fail": "Failed to load floorplan", - "nav_floorplan_localization_fail": "Failed to load floorplan", - "clean_incomplete_to_start": "Cleaning incomplete", - "log_upload_failed": "Logs failed to upload", -} - - -def setup(hass, config): +async def async_setup(hass, config): """Set up the Neato component.""" - from pybotvac import Account, Neato, Vorwerk - if config[DOMAIN][CONF_VENDOR] == "neato": - hass.data[NEATO_LOGIN] = NeatoHub(hass, config[DOMAIN], Account, Neato) - elif config[DOMAIN][CONF_VENDOR] == "vorwerk": - hass.data[NEATO_LOGIN] = NeatoHub(hass, config[DOMAIN], Account, Vorwerk) + if NEATO_DOMAIN not in config: + # There is an entry and nothing in configuration.yaml + return True + + entries = hass.config_entries.async_entries(NEATO_DOMAIN) + hass.data[NEATO_CONFIG] = config[NEATO_DOMAIN] + + if entries: + # There is an entry and something in the configuration.yaml + entry = entries[0] + conf = config[NEATO_DOMAIN] + if ( + entry.data[CONF_USERNAME] == conf[CONF_USERNAME] + and entry.data[CONF_PASSWORD] == conf[CONF_PASSWORD] + and entry.data[CONF_VENDOR] == conf[CONF_VENDOR] + ): + # The entry is not outdated + return True + + # The entry is outdated + error = await hass.async_add_executor_job( + NeatoConfigFlow.try_login, + conf[CONF_USERNAME], + conf[CONF_PASSWORD], + conf[CONF_VENDOR], + ) + if error is not None: + _LOGGER.error(error) + return False + + # Update the entry + hass.config_entries.async_update_entry(entry, data=config[NEATO_DOMAIN]) + else: + # Create the new entry + hass.async_create_task( + hass.config_entries.flow.async_init( + NEATO_DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config[NEATO_DOMAIN], + ) + ) + + return True + + +async def async_setup_entry(hass, entry): + """Set up config entry.""" + hass.data[NEATO_LOGIN] = NeatoHub(hass, entry.data, Account) + hub = hass.data[NEATO_LOGIN] - if not hub.login(): + await hass.async_add_executor_job(hub.login) + if not hub.logged_in: _LOGGER.debug("Failed to login to Neato API") return False - hub.update_robots() - for component in ("camera", "vacuum", "switch"): - discovery.load_platform(hass, component, DOMAIN, {}, config) + try: + await hass.async_add_executor_job(hub.update_robots) + except NeatoRobotException: + _LOGGER.debug("Failed to connect to Neato API") + return False + + for component in ("camera", "vacuum", "switch", "sensor"): + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +async def async_unload_entry(hass, entry): + """Unload config entry.""" + hass.data.pop(NEATO_LOGIN) + await asyncio.gather( + hass.config_entries.async_forward_entry_unload(entry, "camera"), + hass.config_entries.async_forward_entry_unload(entry, "vacuum"), + hass.config_entries.async_forward_entry_unload(entry, "switch"), + hass.config_entries.async_forward_entry_unload(entry, "sensor"), + ) return True class NeatoHub: """A My Neato hub wrapper class.""" - def __init__(self, hass, domain_config, neato, vendor): + def __init__(self, hass, domain_config, neato): """Initialize the Neato hub.""" self.config = domain_config self._neato = neato self._hass = hass - self._vendor = vendor - self.my_neato = neato( - domain_config[CONF_USERNAME], domain_config[CONF_PASSWORD], vendor - ) - self._hass.data[NEATO_ROBOTS] = self.my_neato.robots - self._hass.data[NEATO_PERSISTENT_MAPS] = self.my_neato.persistent_maps - self._hass.data[NEATO_MAP_DATA] = self.my_neato.maps + if self.config[CONF_VENDOR] == "vorwerk": + self._vendor = Vorwerk() + else: # Neato + self._vendor = Neato() + + self.my_neato = None + self.logged_in = False def login(self): """Login to My Neato.""" + _LOGGER.debug("Trying to connect to Neato API") try: - _LOGGER.debug("Trying to connect to Neato API") self.my_neato = self._neato( self.config[CONF_USERNAME], self.config[CONF_PASSWORD], self._vendor ) - return True - except HTTPError: - _LOGGER.error("Unable to connect to Neato API") - return False + except NeatoException as ex: + if isinstance(ex, NeatoLoginException): + _LOGGER.error("Invalid credentials") + else: + _LOGGER.error("Unable to connect to Neato API") + self.logged_in = False + return - @Throttle(timedelta(seconds=300)) + self.logged_in = True + _LOGGER.debug("Successfully connected to Neato API") + + @Throttle(timedelta(minutes=SCAN_INTERVAL_MINUTES)) def update_robots(self): """Update the robot states.""" - _LOGGER.debug("Running HUB.update_robots %s", self._hass.data[NEATO_ROBOTS]) + _LOGGER.debug("Running HUB.update_robots %s", self._hass.data.get(NEATO_ROBOTS)) self._hass.data[NEATO_ROBOTS] = self.my_neato.robots self._hass.data[NEATO_PERSISTENT_MAPS] = self.my_neato.persistent_maps self._hass.data[NEATO_MAP_DATA] = self.my_neato.maps diff --git a/homeassistant/components/neato/camera.py b/homeassistant/components/neato/camera.py index 5d4e0057960..f60835b1146 100644 --- a/homeassistant/components/neato/camera.py +++ b/homeassistant/components/neato/camera.py @@ -2,35 +2,58 @@ from datetime import timedelta import logging +from pybotvac.exceptions import NeatoRobotException + from homeassistant.components.camera import Camera -from . import NEATO_LOGIN, NEATO_MAP_DATA, NEATO_ROBOTS +from .const import ( + NEATO_DOMAIN, + NEATO_LOGIN, + NEATO_MAP_DATA, + NEATO_ROBOTS, + SCAN_INTERVAL_MINUTES, +) _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(minutes=10) +SCAN_INTERVAL = timedelta(minutes=SCAN_INTERVAL_MINUTES) +ATTR_GENERATED_AT = "generated_at" -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 Neato Camera.""" + pass + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up Neato camera with config entry.""" dev = [] + neato = hass.data.get(NEATO_LOGIN) + mapdata = hass.data.get(NEATO_MAP_DATA) for robot in hass.data[NEATO_ROBOTS]: if "maps" in robot.traits: - dev.append(NeatoCleaningMap(hass, robot)) + dev.append(NeatoCleaningMap(neato, robot, mapdata)) + + if not dev: + return + _LOGGER.debug("Adding robots for cleaning maps %s", dev) - add_entities(dev, True) + async_add_entities(dev, True) class NeatoCleaningMap(Camera): """Neato cleaning map for last clean.""" - def __init__(self, hass, robot): + def __init__(self, neato, robot, mapdata): """Initialize Neato cleaning map.""" super().__init__() self.robot = robot - self._robot_name = "{} {}".format(self.robot.name, "Cleaning Map") + self.neato = neato + self._mapdata = mapdata + self._available = self.neato.logged_in if self.neato is not None else False + self._robot_name = f"{self.robot.name} Cleaning Map" self._robot_serial = self.robot.serial - self.neato = hass.data[NEATO_LOGIN] + self._generated_at = None self._image_url = None self._image = None @@ -41,16 +64,45 @@ class NeatoCleaningMap(Camera): def update(self): """Check the contents of the map list.""" - self.neato.update_robots() + if self.neato is None: + _LOGGER.error("Error while updating camera") + self._image = None + self._image_url = None + self._available = False + return + + _LOGGER.debug("Running camera update") + try: + self.neato.update_robots() + except NeatoRobotException as ex: + if self._available: # Print only once when available + _LOGGER.error("Neato camera connection error: %s", ex) + self._image = None + self._image_url = None + self._available = False + return + image_url = None - map_data = self.hass.data[NEATO_MAP_DATA] - image_url = map_data[self._robot_serial]["maps"][0]["url"] + map_data = self._mapdata[self._robot_serial]["maps"][0] + image_url = map_data["url"] if image_url == self._image_url: _LOGGER.debug("The map image_url is the same as old") return - image = self.neato.download_map(image_url) + + try: + image = self.neato.download_map(image_url) + except NeatoRobotException as ex: + if self._available: # Print only once when available + _LOGGER.error("Neato camera connection error: %s", ex) + self._image = None + self._image_url = None + self._available = False + return + self._image = image.read() self._image_url = image_url + self._generated_at = (map_data["generated_at"].strip("Z")).replace("T", " ") + self._available = True @property def name(self): @@ -61,3 +113,23 @@ class NeatoCleaningMap(Camera): def unique_id(self): """Return unique ID.""" return self._robot_serial + + @property + def available(self): + """Return if the robot is available.""" + return self._available + + @property + def device_info(self): + """Device info for neato robot.""" + return {"identifiers": {(NEATO_DOMAIN, self._robot_serial)}} + + @property + def device_state_attributes(self): + """Return the state attributes of the vacuum cleaner.""" + data = {} + + if self._generated_at is not None: + data[ATTR_GENERATED_AT] = self._generated_at + + return data diff --git a/homeassistant/components/neato/config_flow.py b/homeassistant/components/neato/config_flow.py new file mode 100644 index 00000000000..56fba9047e7 --- /dev/null +++ b/homeassistant/components/neato/config_flow.py @@ -0,0 +1,112 @@ +"""Config flow to configure Neato integration.""" + +import logging + +from pybotvac import Account, Neato, Vorwerk +from pybotvac.exceptions import NeatoLoginException, NeatoRobotException +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME + +# pylint: disable=unused-import +from .const import CONF_VENDOR, NEATO_DOMAIN, VALID_VENDORS + +DOCS_URL = "https://www.home-assistant.io/components/neato" +DEFAULT_VENDOR = "neato" + +_LOGGER = logging.getLogger(__name__) + + +class NeatoConfigFlow(config_entries.ConfigFlow, domain=NEATO_DOMAIN): + """Neato integration config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + def __init__(self): + """Initialize flow.""" + self._username = vol.UNDEFINED + self._password = vol.UNDEFINED + self._vendor = vol.UNDEFINED + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + errors = {} + + if self._async_current_entries(): + return self.async_abort(reason="already_configured") + + if user_input is not None: + self._username = user_input["username"] + self._password = user_input["password"] + self._vendor = user_input["vendor"] + + error = await self.hass.async_add_executor_job( + self.try_login, self._username, self._password, self._vendor + ) + if error: + errors["base"] = error + else: + return self.async_create_entry( + title=user_input[CONF_USERNAME], + data=user_input, + description_placeholders={"docs_url": DOCS_URL}, + ) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Optional(CONF_VENDOR, default="neato"): vol.In(VALID_VENDORS), + } + ), + description_placeholders={"docs_url": DOCS_URL}, + errors=errors, + ) + + async def async_step_import(self, user_input): + """Import a config flow from configuration.""" + + if self._async_current_entries(): + return self.async_abort(reason="already_configured") + + username = user_input[CONF_USERNAME] + password = user_input[CONF_PASSWORD] + vendor = user_input[CONF_VENDOR] + + error = await self.hass.async_add_executor_job( + self.try_login, username, password, vendor + ) + if error is not None: + _LOGGER.error(error) + return self.async_abort(reason=error) + + return self.async_create_entry( + title=f"{username} (from configuration)", + data={ + CONF_USERNAME: username, + CONF_PASSWORD: password, + CONF_VENDOR: vendor, + }, + ) + + @staticmethod + def try_login(username, password, vendor): + """Try logging in to device and return any errors.""" + this_vendor = None + if vendor == "vorwerk": + this_vendor = Vorwerk() + else: # Neato + this_vendor = Neato() + + try: + Account(username, password, this_vendor) + except NeatoLoginException: + return "invalid_credentials" + except NeatoRobotException: + return "unexpected_error" + + return None diff --git a/homeassistant/components/neato/const.py b/homeassistant/components/neato/const.py new file mode 100644 index 00000000000..6dbaeb10d36 --- /dev/null +++ b/homeassistant/components/neato/const.py @@ -0,0 +1,152 @@ +"""Constants for Neato integration.""" + +NEATO_DOMAIN = "neato" + +CONF_VENDOR = "vendor" +NEATO_CONFIG = "neato_config" +NEATO_LOGIN = "neato_login" +NEATO_MAP_DATA = "neato_map_data" +NEATO_PERSISTENT_MAPS = "neato_persistent_maps" +NEATO_ROBOTS = "neato_robots" + +SCAN_INTERVAL_MINUTES = 5 + +VALID_VENDORS = ["neato", "vorwerk"] + +MODE = {1: "Eco", 2: "Turbo"} + +ACTION = { + 0: "Invalid", + 1: "House Cleaning", + 2: "Spot Cleaning", + 3: "Manual Cleaning", + 4: "Docking", + 5: "User Menu Active", + 6: "Suspended Cleaning", + 7: "Updating", + 8: "Copying logs", + 9: "Recovering Location", + 10: "IEC test", + 11: "Map cleaning", + 12: "Exploring map (creating a persistent map)", + 13: "Acquiring Persistent Map IDs", + 14: "Creating & Uploading Map", + 15: "Suspended Exploration", +} + +ERRORS = { + "ui_error_battery_battundervoltlithiumsafety": "Replace battery", + "ui_error_battery_critical": "Replace battery", + "ui_error_battery_invalidsensor": "Replace battery", + "ui_error_battery_lithiumadapterfailure": "Replace battery", + "ui_error_battery_mismatch": "Replace battery", + "ui_error_battery_nothermistor": "Replace battery", + "ui_error_battery_overtemp": "Replace battery", + "ui_error_battery_overvolt": "Replace battery", + "ui_error_battery_undercurrent": "Replace battery", + "ui_error_battery_undertemp": "Replace battery", + "ui_error_battery_undervolt": "Replace battery", + "ui_error_battery_unplugged": "Replace battery", + "ui_error_brush_stuck": "Brush stuck", + "ui_error_brush_overloaded": "Brush overloaded", + "ui_error_bumper_stuck": "Bumper stuck", + "ui_error_check_battery_switch": "Check battery", + "ui_error_corrupt_scb": "Call customer service corrupt board", + "ui_error_deck_debris": "Deck debris", + "ui_error_dflt_app": "Check Neato app", + "ui_error_disconnect_chrg_cable": "Disconnected charge cable", + "ui_error_disconnect_usb_cable": "Disconnected USB cable", + "ui_error_dust_bin_missing": "Dust bin missing", + "ui_error_dust_bin_full": "Dust bin full", + "ui_error_dust_bin_emptied": "Dust bin emptied", + "ui_error_hardware_failure": "Hardware failure", + "ui_error_ldrop_stuck": "Clear my path", + "ui_error_lds_jammed": "Clear my path", + "ui_error_lds_bad_packets": "Check Neato app", + "ui_error_lds_disconnected": "Check Neato app", + "ui_error_lds_missed_packets": "Check Neato app", + "ui_error_lwheel_stuck": "Clear my path", + "ui_error_navigation_backdrop_frontbump": "Clear my path", + "ui_error_navigation_backdrop_leftbump": "Clear my path", + "ui_error_navigation_backdrop_wheelextended": "Clear my path", + "ui_error_navigation_noprogress": "Clear my path", + "ui_error_navigation_origin_unclean": "Clear my path", + "ui_error_navigation_pathproblems": "Cannot return to base", + "ui_error_navigation_pinkycommsfail": "Clear my path", + "ui_error_navigation_falling": "Clear my path", + "ui_error_navigation_noexitstogo": "Clear my path", + "ui_error_navigation_nomotioncommands": "Clear my path", + "ui_error_navigation_rightdrop_leftbump": "Clear my path", + "ui_error_navigation_undockingfailed": "Clear my path", + "ui_error_picked_up": "Picked up", + "ui_error_qa_fail": "Check Neato app", + "ui_error_rdrop_stuck": "Clear my path", + "ui_error_reconnect_failed": "Reconnect failed", + "ui_error_rwheel_stuck": "Clear my path", + "ui_error_stuck": "Stuck!", + "ui_error_unable_to_return_to_base": "Unable to return to base", + "ui_error_unable_to_see": "Clean vacuum sensors", + "ui_error_vacuum_slip": "Clear my path", + "ui_error_vacuum_stuck": "Clear my path", + "ui_error_warning": "Error check app", + "batt_base_connect_fail": "Battery failed to connect to base", + "batt_base_no_power": "Battery base has no power", + "batt_low": "Battery low", + "batt_on_base": "Battery on base", + "clean_tilt_on_start": "Clean the tilt on start", + "dustbin_full": "Dust bin full", + "dustbin_missing": "Dust bin missing", + "gen_picked_up": "Picked up", + "hw_fail": "Hardware failure", + "hw_tof_sensor_sensor": "Hardware sensor disconnected", + "lds_bad_packets": "Bad packets", + "lds_deck_debris": "Debris on deck", + "lds_disconnected": "Disconnected", + "lds_jammed": "Jammed", + "lds_missed_packets": "Missed packets", + "maint_brush_stuck": "Brush stuck", + "maint_brush_overload": "Brush overloaded", + "maint_bumper_stuck": "Bumper stuck", + "maint_customer_support_qa": "Contact customer support", + "maint_vacuum_stuck": "Vacuum is stuck", + "maint_vacuum_slip": "Vacuum is stuck", + "maint_left_drop_stuck": "Vacuum is stuck", + "maint_left_wheel_stuck": "Vacuum is stuck", + "maint_right_drop_stuck": "Vacuum is stuck", + "maint_right_wheel_stuck": "Vacuum is stuck", + "not_on_charge_base": "Not on the charge base", + "nav_robot_falling": "Clear my path", + "nav_no_path": "Clear my path", + "nav_path_problem": "Clear my path", + "nav_backdrop_frontbump": "Clear my path", + "nav_backdrop_leftbump": "Clear my path", + "nav_backdrop_wheelextended": "Clear my path", + "nav_mag_sensor": "Clear my path", + "nav_no_exit": "Clear my path", + "nav_no_movement": "Clear my path", + "nav_rightdrop_leftbump": "Clear my path", + "nav_undocking_failed": "Clear my path", +} + +ALERTS = { + "ui_alert_dust_bin_full": "Please empty dust bin", + "ui_alert_recovering_location": "Returning to start", + "ui_alert_battery_chargebasecommerr": "Battery error", + "ui_alert_busy_charging": "Busy charging", + "ui_alert_charging_base": "Base charging", + "ui_alert_charging_power": "Charging power", + "ui_alert_connect_chrg_cable": "Connect charge cable", + "ui_alert_info_thank_you": "Thank you", + "ui_alert_invalid": "Invalid check app", + "ui_alert_old_error": "Old error", + "ui_alert_swupdate_fail": "Update failed", + "dustbin_full": "Please empty dust bin", + "maint_brush_change": "Change the brush", + "maint_filter_change": "Change the filter", + "clean_completed_to_start": "Cleaning completed", + "nav_floorplan_not_created": "No floorplan found", + "nav_floorplan_load_fail": "Failed to load floorplan", + "nav_floorplan_localization_fail": "Failed to load floorplan", + "clean_incomplete_to_start": "Cleaning incomplete", + "log_upload_failed": "Logs failed to upload", +} diff --git a/homeassistant/components/neato/manifest.json b/homeassistant/components/neato/manifest.json index 8b0c5acc723..03f8089159e 100644 --- a/homeassistant/components/neato/manifest.json +++ b/homeassistant/components/neato/manifest.json @@ -1,10 +1,14 @@ { "domain": "neato", "name": "Neato", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/neato", "requirements": [ - "pybotvac==0.0.15" + "pybotvac==0.0.17" ], "dependencies": [], - "codeowners": [] -} + "codeowners": [ + "@dshokouhi", + "@Santobert" + ] +} \ No newline at end of file diff --git a/homeassistant/components/neato/sensor.py b/homeassistant/components/neato/sensor.py new file mode 100644 index 00000000000..36175151e0e --- /dev/null +++ b/homeassistant/components/neato/sensor.py @@ -0,0 +1,104 @@ +"""Support for Neato sensors.""" +from datetime import timedelta +import logging + +from pybotvac.exceptions import NeatoRobotException + +from homeassistant.components.sensor import DEVICE_CLASS_BATTERY +from homeassistant.helpers.entity import Entity + +from .const import NEATO_DOMAIN, NEATO_LOGIN, NEATO_ROBOTS, SCAN_INTERVAL_MINUTES + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = timedelta(minutes=SCAN_INTERVAL_MINUTES) + +BATTERY = "Battery" + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the Neato sensor.""" + pass + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the Neato sensor using config entry.""" + dev = [] + neato = hass.data.get(NEATO_LOGIN) + for robot in hass.data[NEATO_ROBOTS]: + dev.append(NeatoSensor(neato, robot)) + + if not dev: + return + + _LOGGER.debug("Adding robots for sensors %s", dev) + async_add_entities(dev, True) + + +class NeatoSensor(Entity): + """Neato sensor.""" + + 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._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: + _LOGGER.error("Neato sensor connection error: %s", ex) + self._state = None + self._available = False + return + + self._available = True + _LOGGER.debug("self._state=%s", self._state) + + @property + def name(self): + """Return the name of this sensor.""" + return self._robot_name + + @property + def unique_id(self): + """Return unique ID.""" + return self._robot_serial + + @property + def device_class(self): + """Return the device class.""" + return DEVICE_CLASS_BATTERY + + @property + def available(self): + """Return availability.""" + return self._available + + @property + def state(self): + """Return the state.""" + return self._state["details"]["charge"] + + @property + def unit_of_measurement(self): + """Return unit of measurement.""" + return "%" + + @property + def device_info(self): + """Device info for neato robot.""" + return {"identifiers": {(NEATO_DOMAIN, self._robot_serial)}} diff --git a/homeassistant/components/neato/strings.json b/homeassistant/components/neato/strings.json new file mode 100644 index 00000000000..69cdb48a560 --- /dev/null +++ b/homeassistant/components/neato/strings.json @@ -0,0 +1,27 @@ +{ + "config": { + "title": "Neato", + "step": { + "user": { + "title": "Neato Account Info", + "data": { + "username": "Username", + "password": "Password", + "vendor": "Vendor" + }, + "description": "See [Neato documentation]({docs_url})." + } + }, + "error": { + "invalid_credentials": "Invalid credentials", + "unexpected_error": "Unexpected error" + }, + "create_entry": { + "default": "See [Neato documentation]({docs_url})." + }, + "abort": { + "already_configured": "Already configured", + "invalid_credentials": "Invalid credentials" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/switch.py b/homeassistant/components/neato/switch.py index 539e8cb748c..8536af63945 100644 --- a/homeassistant/components/neato/switch.py +++ b/homeassistant/components/neato/switch.py @@ -2,66 +2,77 @@ from datetime import timedelta import logging -import requests +from pybotvac.exceptions import NeatoRobotException from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.helpers.entity import ToggleEntity -from . import NEATO_LOGIN, NEATO_ROBOTS +from .const import NEATO_DOMAIN, NEATO_LOGIN, NEATO_ROBOTS, SCAN_INTERVAL_MINUTES _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(minutes=10) +SCAN_INTERVAL = timedelta(minutes=SCAN_INTERVAL_MINUTES) SWITCH_TYPE_SCHEDULE = "schedule" SWITCH_TYPES = {SWITCH_TYPE_SCHEDULE: ["Schedule"]} -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 Neato switches.""" + pass + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up Neato switch with config entry.""" dev = [] + neato = hass.data.get(NEATO_LOGIN) for robot in hass.data[NEATO_ROBOTS]: for type_name in SWITCH_TYPES: - dev.append(NeatoConnectedSwitch(hass, robot, type_name)) + dev.append(NeatoConnectedSwitch(neato, robot, type_name)) + + if not dev: + return + _LOGGER.debug("Adding switches %s", dev) - add_entities(dev) + async_add_entities(dev, True) class NeatoConnectedSwitch(ToggleEntity): """Neato Connected Switches.""" - def __init__(self, hass, robot, switch_type): + def __init__(self, neato, robot, switch_type): """Initialize the Neato Connected switches.""" self.type = switch_type self.robot = robot - self.neato = hass.data[NEATO_LOGIN] - self._robot_name = "{} {}".format(self.robot.name, SWITCH_TYPES[self.type][0]) - try: - self._state = self.robot.state - except ( - requests.exceptions.ConnectionError, - requests.exceptions.HTTPError, - ) as ex: - _LOGGER.warning("Neato connection error: %s", ex) - self._state = None + self.neato = neato + self._available = self.neato.logged_in if self.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 self._clean_state = None self._robot_serial = self.robot.serial def update(self): """Update the states of Neato switches.""" - _LOGGER.debug("Running switch update") - self.neato.update_robots() - try: - self._state = self.robot.state - except ( - requests.exceptions.ConnectionError, - requests.exceptions.HTTPError, - ) as ex: - _LOGGER.warning("Neato connection error: %s", ex) + 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 + _LOGGER.error("Neato switch connection error: %s", ex) + self._state = None + self._available = False + return + + self._available = True _LOGGER.debug("self._state=%s", self._state) if self.type == SWITCH_TYPE_SCHEDULE: _LOGGER.debug("State: %s", self._state) @@ -79,7 +90,7 @@ class NeatoConnectedSwitch(ToggleEntity): @property def available(self): """Return True if entity is available.""" - return self._state + return self._available @property def unique_id(self): @@ -94,12 +105,23 @@ class NeatoConnectedSwitch(ToggleEntity): return True return False + @property + def device_info(self): + """Device info for neato robot.""" + return {"identifiers": {(NEATO_DOMAIN, self._robot_serial)}} + def turn_on(self, **kwargs): """Turn the switch on.""" if self.type == SWITCH_TYPE_SCHEDULE: - self.robot.enable_schedule() + try: + self.robot.enable_schedule() + except NeatoRobotException as ex: + _LOGGER.error("Neato switch connection error: %s", ex) def turn_off(self, **kwargs): """Turn the switch off.""" if self.type == SWITCH_TYPE_SCHEDULE: - self.robot.disable_schedule() + try: + self.robot.disable_schedule() + except NeatoRobotException as ex: + _LOGGER.error("Neato switch connection error: %s", ex) diff --git a/homeassistant/components/neato/vacuum.py b/homeassistant/components/neato/vacuum.py index f284b2eda1e..40ed79042c7 100644 --- a/homeassistant/components/neato/vacuum.py +++ b/homeassistant/components/neato/vacuum.py @@ -2,12 +2,10 @@ from datetime import timedelta import logging -import requests +from pybotvac.exceptions import NeatoRobotException import voluptuous as vol from homeassistant.components.vacuum import ( - ATTR_BATTERY_ICON, - ATTR_BATTERY_LEVEL, ATTR_STATUS, DOMAIN, STATE_CLEANING, @@ -31,20 +29,22 @@ from homeassistant.const import ATTR_ENTITY_ID, ATTR_MODE import homeassistant.helpers.config_validation as cv from homeassistant.helpers.service import extract_entity_ids -from . import ( +from .const import ( ACTION, ALERTS, ERRORS, MODE, + NEATO_DOMAIN, NEATO_LOGIN, NEATO_MAP_DATA, NEATO_PERSISTENT_MAPS, NEATO_ROBOTS, + SCAN_INTERVAL_MINUTES, ) _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(minutes=5) +SCAN_INTERVAL = timedelta(minutes=SCAN_INTERVAL_MINUTES) SUPPORT_NEATO = ( SUPPORT_BATTERY @@ -65,6 +65,9 @@ ATTR_CLEAN_BATTERY_START = "battery_level_at_clean_start" ATTR_CLEAN_BATTERY_END = "battery_level_at_clean_end" ATTR_CLEAN_SUSP_COUNT = "clean_suspension_count" ATTR_CLEAN_SUSP_TIME = "clean_suspension_time" +ATTR_CLEAN_PAUSE_TIME = "clean_pause_time" +ATTR_CLEAN_ERROR_TIME = "clean_error_time" +ATTR_LAUNCHED_FROM = "launched_from" ATTR_NAVIGATION = "navigation" ATTR_CATEGORY = "category" @@ -83,17 +86,25 @@ SERVICE_NEATO_CUSTOM_CLEANING_SCHEMA = vol.Schema( ) -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 Neato vacuum.""" + pass + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up Neato vacuum with config entry.""" dev = [] + neato = hass.data.get(NEATO_LOGIN) + mapdata = hass.data.get(NEATO_MAP_DATA) + persistent_maps = hass.data.get(NEATO_PERSISTENT_MAPS) for robot in hass.data[NEATO_ROBOTS]: - dev.append(NeatoConnectedVacuum(hass, robot)) + dev.append(NeatoConnectedVacuum(neato, robot, mapdata, persistent_maps)) if not dev: return _LOGGER.debug("Adding vacuums %s", dev) - add_entities(dev, True) + async_add_entities(dev, True) def neato_custom_cleaning_service(call): """Zone cleaning service that allows user to change options.""" @@ -103,7 +114,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None): navigation = call.data.get(ATTR_NAVIGATION) category = call.data.get(ATTR_CATEGORY) zone = call.data.get(ATTR_ZONE) - robot.neato_custom_cleaning(mode, navigation, category, zone) + try: + robot.neato_custom_cleaning(mode, navigation, category, zone) + except NeatoRobotException as ex: + _LOGGER.error("Neato vacuum connection error: %s", ex) def service_to_entities(call): """Return the known devices that a service call mentions.""" @@ -111,7 +125,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): entities = [entity for entity in dev if entity.entity_id in entity_ids] return entities - hass.services.register( + hass.services.async_register( DOMAIN, SERVICE_NEATO_CUSTOM_CLEANING, neato_custom_cleaning_service, @@ -122,44 +136,55 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class NeatoConnectedVacuum(StateVacuumDevice): """Representation of a Neato Connected Vacuum.""" - def __init__(self, hass, robot): + def __init__(self, neato, robot, mapdata, persistent_maps): """Initialize the Neato Connected Vacuum.""" self.robot = robot - self.neato = hass.data[NEATO_LOGIN] + self.neato = neato + self._available = self.neato.logged_in if self.neato is not None else False + self._mapdata = mapdata self._name = f"{self.robot.name}" + self._robot_has_map = self.robot.has_persistent_maps + self._robot_maps = persistent_maps + self._robot_serial = self.robot.serial self._status_state = None self._clean_state = None self._state = None - self._mapdata = hass.data[NEATO_MAP_DATA] - self.clean_time_start = None - self.clean_time_stop = None - self.clean_area = None - self.clean_battery_start = None - self.clean_battery_end = None - self.clean_suspension_charge_count = None - self.clean_suspension_time = None - self._available = False + self._clean_time_start = None + self._clean_time_stop = None + self._clean_area = None + self._clean_battery_start = None + self._clean_battery_end = None + self._clean_susp_charge_count = None + self._clean_susp_time = None + self._clean_pause_time = None + self._clean_error_time = None + self._launched_from = None self._battery_level = None - self._robot_serial = self.robot.serial - self._robot_maps = hass.data[NEATO_PERSISTENT_MAPS] self._robot_boundaries = {} - self._robot_has_map = self.robot.has_persistent_maps + self._robot_stats = None def update(self): """Update the states of Neato Vacuums.""" - _LOGGER.debug("Running Neato Vacuums update") - self.neato.update_robots() - try: - self._state = self.robot.state - self._available = True - except ( - requests.exceptions.ConnectionError, - requests.exceptions.HTTPError, - ) as ex: - _LOGGER.warning("Neato connection error: %s", ex) + 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._state = self.robot.state + except NeatoRobotException as ex: + if self._available: # print only once when available + _LOGGER.error("Neato vacuum connection error: %s", ex) + self._state = None + self._available = False + return + + self._available = True _LOGGER.debug("self._state=%s", self._state) if "alert" in self._state: robot_alert = ALERTS.get(self._state["alert"]) @@ -202,34 +227,33 @@ class NeatoConnectedVacuum(StateVacuumDevice): if not self._mapdata.get(self._robot_serial, {}).get("maps", []): return - self.clean_time_start = ( - self._mapdata[self._robot_serial]["maps"][0]["start_at"].strip("Z") - ).replace("T", " ") - self.clean_time_stop = ( - self._mapdata[self._robot_serial]["maps"][0]["end_at"].strip("Z") - ).replace("T", " ") - self.clean_area = self._mapdata[self._robot_serial]["maps"][0]["cleaned_area"] - self.clean_suspension_charge_count = self._mapdata[self._robot_serial]["maps"][ - 0 - ]["suspended_cleaning_charging_count"] - self.clean_suspension_time = self._mapdata[self._robot_serial]["maps"][0][ - "time_in_suspended_cleaning" - ] - self.clean_battery_start = self._mapdata[self._robot_serial]["maps"][0][ - "run_charge_at_start" - ] - self.clean_battery_end = self._mapdata[self._robot_serial]["maps"][0][ - "run_charge_at_end" - ] - if self._robot_has_map: - if self._state["availableServices"]["maps"] != "basic-1": - if self._robot_maps[self._robot_serial]: - allmaps = self._robot_maps[self._robot_serial] - for maps in allmaps: - self._robot_boundaries = self.robot.get_map_boundaries( - maps["id"] - ).json() + mapdata = self._mapdata[self._robot_serial]["maps"][0] + self._clean_time_start = (mapdata["start_at"].strip("Z")).replace("T", " ") + self._clean_time_stop = (mapdata["end_at"].strip("Z")).replace("T", " ") + self._clean_area = mapdata["cleaned_area"] + self._clean_susp_charge_count = mapdata["suspended_cleaning_charging_count"] + self._clean_susp_time = mapdata["time_in_suspended_cleaning"] + self._clean_pause_time = mapdata["time_in_pause"] + self._clean_error_time = mapdata["time_in_error"] + self._clean_battery_start = mapdata["run_charge_at_start"] + self._clean_battery_end = mapdata["run_charge_at_end"] + self._launched_from = mapdata["launched_from"] + + if ( + self._robot_has_map + and self._state["availableServices"]["maps"] != "basic-1" + and self._robot_maps[self._robot_serial] + ): + allmaps = self._robot_maps[self._robot_serial] + for maps in allmaps: + try: + self._robot_boundaries = self.robot.get_map_boundaries( + maps["id"] + ).json() + except NeatoRobotException as ex: + _LOGGER.error("Could not fetch map boundaries: %s", ex) + self._robot_boundaries = {} @property def name(self): @@ -251,6 +275,11 @@ class NeatoConnectedVacuum(StateVacuumDevice): """Return if the robot is available.""" return self._available + @property + def icon(self): + """Return neato specific icon.""" + return "mdi:robot-vacuum-variant" + @property def state(self): """Return the status of the vacuum cleaner.""" @@ -268,57 +297,87 @@ class NeatoConnectedVacuum(StateVacuumDevice): if self._status_state is not None: data[ATTR_STATUS] = self._status_state - - if self.battery_level is not None: - data[ATTR_BATTERY_LEVEL] = self.battery_level - data[ATTR_BATTERY_ICON] = self.battery_icon - - if self.clean_time_start is not None: - data[ATTR_CLEAN_START] = self.clean_time_start - if self.clean_time_stop is not None: - data[ATTR_CLEAN_STOP] = self.clean_time_stop - if self.clean_area is not None: - data[ATTR_CLEAN_AREA] = self.clean_area - if self.clean_suspension_charge_count is not None: - data[ATTR_CLEAN_SUSP_COUNT] = self.clean_suspension_charge_count - if self.clean_suspension_time is not None: - data[ATTR_CLEAN_SUSP_TIME] = self.clean_suspension_time - if self.clean_battery_start is not None: - data[ATTR_CLEAN_BATTERY_START] = self.clean_battery_start - if self.clean_battery_end is not None: - data[ATTR_CLEAN_BATTERY_END] = self.clean_battery_end + if self._clean_time_start is not None: + data[ATTR_CLEAN_START] = self._clean_time_start + if self._clean_time_stop is not None: + data[ATTR_CLEAN_STOP] = self._clean_time_stop + if self._clean_area is not None: + data[ATTR_CLEAN_AREA] = self._clean_area + if self._clean_susp_charge_count is not None: + data[ATTR_CLEAN_SUSP_COUNT] = self._clean_susp_charge_count + if self._clean_susp_time is not None: + data[ATTR_CLEAN_SUSP_TIME] = self._clean_susp_time + if self._clean_pause_time is not None: + data[ATTR_CLEAN_PAUSE_TIME] = self._clean_pause_time + if self._clean_error_time is not None: + data[ATTR_CLEAN_ERROR_TIME] = self._clean_error_time + if self._clean_battery_start is not None: + data[ATTR_CLEAN_BATTERY_START] = self._clean_battery_start + if self._clean_battery_end is not None: + data[ATTR_CLEAN_BATTERY_END] = self._clean_battery_end + if self._launched_from is not None: + data[ATTR_LAUNCHED_FROM] = self._launched_from return data + @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"], + } + def start(self): """Start cleaning or resume cleaning.""" - if self._state["state"] == 1: - self.robot.start_cleaning() - elif self._state["state"] == 3: - self.robot.resume_cleaning() + try: + if self._state["state"] == 1: + self.robot.start_cleaning() + elif self._state["state"] == 3: + self.robot.resume_cleaning() + except NeatoRobotException as ex: + _LOGGER.error("Neato vacuum connection error: %s", ex) def pause(self): """Pause the vacuum.""" - self.robot.pause_cleaning() + try: + self.robot.pause_cleaning() + except NeatoRobotException as ex: + _LOGGER.error("Neato vacuum connection error: %s", ex) def return_to_base(self, **kwargs): """Set the vacuum cleaner to return to the dock.""" - if self._clean_state == STATE_CLEANING: - self.robot.pause_cleaning() - self._clean_state = STATE_RETURNING - self.robot.send_to_base() + try: + if self._clean_state == STATE_CLEANING: + self.robot.pause_cleaning() + self._clean_state = STATE_RETURNING + self.robot.send_to_base() + except NeatoRobotException as ex: + _LOGGER.error("Neato vacuum connection error: %s", ex) def stop(self, **kwargs): """Stop the vacuum cleaner.""" - self.robot.stop_cleaning() + try: + self.robot.stop_cleaning() + except NeatoRobotException as ex: + _LOGGER.error("Neato vacuum connection error: %s", ex) def locate(self, **kwargs): """Locate the robot by making it emit a sound.""" - self.robot.locate() + try: + self.robot.locate() + except NeatoRobotException as ex: + _LOGGER.error("Neato vacuum connection error: %s", ex) def clean_spot(self, **kwargs): """Run a spot cleaning starting from the base.""" - self.robot.start_spot_cleaning() + try: + self.robot.start_spot_cleaning() + except NeatoRobotException as ex: + _LOGGER.error("Neato vacuum connection error: %s", ex) def neato_custom_cleaning(self, mode, navigation, category, zone=None, **kwargs): """Zone cleaning service call.""" @@ -334,4 +393,7 @@ class NeatoConnectedVacuum(StateVacuumDevice): return self._clean_state = STATE_CLEANING - self.robot.start_cleaning(mode, navigation, category, boundary_id) + try: + self.robot.start_cleaning(mode, navigation, category, boundary_id) + except NeatoRobotException as ex: + _LOGGER.error("Neato vacuum connection error: %s", ex) diff --git a/homeassistant/components/nest/.translations/ru.json b/homeassistant/components/nest/.translations/ru.json index ac88ed224ed..ba49b788b9a 100644 --- a/homeassistant/components/nest/.translations/ru.json +++ b/homeassistant/components/nest/.translations/ru.json @@ -7,10 +7,10 @@ "no_flows": "\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 Nest \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/nest/)." }, "error": { - "internal_error": "\u0412\u043d\u0443\u0442\u0440\u0435\u043d\u043d\u044f\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043a\u043e\u0434\u0430", - "invalid_code": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043e\u0434", + "internal_error": "\u0412\u043d\u0443\u0442\u0440\u0435\u043d\u043d\u044f\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043a\u043e\u0434\u0430.", + "invalid_code": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043e\u0434.", "timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043a\u043e\u0434\u0430.", - "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043a\u043e\u0434\u0430" + "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043a\u043e\u0434\u0430." }, "step": { "init": { diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index cf1ba36aa89..32bbd009417 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -4,6 +4,8 @@ import socket from datetime import datetime, timedelta import threading +from nest import Nest +from nest.nest import AuthorizationError, APIError import voluptuous as vol from homeassistant import config_entries @@ -142,7 +144,6 @@ async def async_setup(hass, config): async def async_setup_entry(hass, entry): """Set up Nest from a config entry.""" - from nest import Nest nest = Nest(access_token=entry.data["tokens"]["access_token"]) @@ -286,8 +287,6 @@ class NestDevice: def initialize(self): """Initialize Nest.""" - from nest.nest import AuthorizationError, APIError - try: # Do not optimize next statement, it is here for initialize # persistence Nest API connection. @@ -302,8 +301,6 @@ class NestDevice: def structures(self): """Generate a list of structures.""" - from nest.nest import AuthorizationError, APIError - try: for structure in self.nest.structures: if structure.name not in self.local_structure: @@ -332,8 +329,6 @@ class NestDevice: def _devices(self, device_type): """Generate a list of Nest devices.""" - from nest.nest import AuthorizationError, APIError - try: for structure in self.nest.structures: if structure.name not in self.local_structure: diff --git a/homeassistant/components/nest/climate.py b/homeassistant/components/nest/climate.py index eec7108cdea..795ce5c80e9 100644 --- a/homeassistant/components/nest/climate.py +++ b/homeassistant/components/nest/climate.py @@ -1,6 +1,7 @@ """Support for Nest thermostats.""" import logging +from nest.nest import APIError import voluptuous as vol from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice @@ -232,7 +233,6 @@ class NestThermostat(ClimateDevice): def set_temperature(self, **kwargs): """Set new target temperature.""" - import nest temp = None target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) @@ -247,7 +247,7 @@ class NestThermostat(ClimateDevice): try: if temp is not None: self.device.target = temp - except nest.nest.APIError as api_error: + except APIError as api_error: _LOGGER.error("An error occurred while setting temperature: %s", api_error) # restore target temperature self.schedule_update_ha_state(True) diff --git a/homeassistant/components/nest/local_auth.py b/homeassistant/components/nest/local_auth.py index 51d826c242f..38d1827326d 100644 --- a/homeassistant/components/nest/local_auth.py +++ b/homeassistant/components/nest/local_auth.py @@ -2,6 +2,8 @@ import asyncio from functools import partial +from nest.nest import NestAuth, AUTHORIZE_URL, AuthorizationError + from homeassistant.core import callback from . import config_flow from .const import DOMAIN @@ -21,14 +23,11 @@ def initialize(hass, client_id, client_secret): async def generate_auth_url(client_id, flow_id): """Generate an authorize url.""" - from nest.nest import AUTHORIZE_URL - return AUTHORIZE_URL.format(client_id, flow_id) async def resolve_auth_code(hass, client_id, client_secret, code): """Resolve an authorization code.""" - from nest.nest import NestAuth, AuthorizationError result = asyncio.Future() auth = NestAuth( diff --git a/homeassistant/components/nest/services.yaml b/homeassistant/components/nest/services.yaml index 0015c83342d..e10e6264643 100644 --- a/homeassistant/components/nest/services.yaml +++ b/homeassistant/components/nest/services.yaml @@ -1,16 +1,37 @@ -set_mode: - description: 'Set the home/away mode for a Nest structure. Set to away mode will - also set Estimated Arrival Time if provided. Set ETA will cause the thermostat - to begin warming or cooling the home before the user arrives. After ETA set other - Automation can read ETA sensor as a signal to prepare the home for the user''s - arrival. +# Describes the format for available Nest services - ' +set_away_mode: + description: Set the away mode for a Nest structure. fields: - eta: {description: Optional Estimated Arrival Time from now., example: '0:10'} - eta_window: {description: Optional ETA window. Default is 1 minute., example: '0:5'} - home_mode: {description: home or away, example: home} - structure: {description: Optional structure name. Default set all structures managed - by Home Assistant., example: My Home} - trip_id: {description: Optional identity of a trip. Using the same trip_ID will - update the estimation., example: trip_back_home} + away_mode: + description: New mode to set. Valid modes are "away" or "home". + example: "away" + structure: + description: Name(s) of structure(s) to change. Defaults to all structures if not specified. + example: "Apartment" + +set_eta: + description: Set or update the estimated time of arrival window for a Nest structure. + fields: + eta: + description: Estimated time of arrival from now. + example: "00:10:30" + eta_window: + description: Estimated time of arrival window. Default is 1 minute. + example: "00:05" + trip_id: + description: Unique ID for the trip. Default is auto-generated using a timestamp. + example: "Leave Work" + structure: + description: Name(s) of structure(s) to change. Defaults to all structures if not specified. + example: "Apartment" + +cancel_eta: + description: Cancel an existing estimated time of arrival window for a Nest structure. + fields: + trip_id: + description: Unique ID for the trip. + example: "Leave Work" + structure: + description: Name(s) of structure(s) to change. Defaults to all structures if not specified. + example: "Apartment" diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index 28d422557da..4b9f0690ac5 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -3,6 +3,7 @@ import logging from datetime import timedelta from urllib.error import HTTPError +import pyatmo import voluptuous as vol from homeassistant.const import ( @@ -89,7 +90,6 @@ SCHEMA_SERVICE_DROPWEBHOOK = vol.Schema({}) def setup(hass, config): """Set up the Netatmo devices.""" - import pyatmo hass.data[DATA_PERSONS] = {} try: @@ -254,8 +254,6 @@ class CameraData: @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Call the Netatmo API to update the data.""" - import pyatmo - self.camera_data = pyatmo.CameraData(self.auth, size=100) @Throttle(MIN_TIME_BETWEEN_EVENT_UPDATES) diff --git a/homeassistant/components/netatmo/binary_sensor.py b/homeassistant/components/netatmo/binary_sensor.py index 591cd790ecf..1a40d3952e9 100644 --- a/homeassistant/components/netatmo/binary_sensor.py +++ b/homeassistant/components/netatmo/binary_sensor.py @@ -1,6 +1,7 @@ """Support for the Netatmo binary sensors.""" import logging +from pyatmo import NoDevice import voluptuous as vol from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice @@ -58,15 +59,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None): module_name = None - import pyatmo - auth = hass.data[DATA_NETATMO_AUTH] try: data = CameraData(hass, auth, home) if not data.get_camera_names(): return None - except pyatmo.NoDevice: + except NoDevice: return None welcome_sensors = config.get(CONF_WELCOME_SENSORS, WELCOME_SENSOR_TYPES) diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index 60428961cb9..ecc38add3b4 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -1,6 +1,7 @@ """Support for the Netatmo cameras.""" import logging +from pyatmo import NoDevice import requests import voluptuous as vol @@ -38,7 +39,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): home = config.get(CONF_HOME) verify_ssl = config.get(CONF_VERIFY_SSL, True) quality = config.get(CONF_QUALITY, DEFAULT_QUALITY) - import pyatmo auth = hass.data[DATA_NETATMO_AUTH] @@ -60,7 +60,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ] ) data.get_persons() - except pyatmo.NoDevice: + except NoDevice: return None diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 1465058652d..8ba13a03889 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -3,6 +3,7 @@ from datetime import timedelta import logging from typing import Optional, List +import pyatmo import requests import voluptuous as vol @@ -103,8 +104,6 @@ NA_VALVE = "NRV" def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the NetAtmo Thermostat.""" - import pyatmo - homes_conf = config.get(CONF_HOMES) auth = hass.data[DATA_NETATMO_AUTH] @@ -365,8 +364,6 @@ class HomeData: def setup(self): """Retrieve HomeData by NetAtmo API.""" - import pyatmo - try: self.homedata = pyatmo.HomeData(self.auth) self.home_id = self.homedata.gethomeId(self.home) @@ -408,8 +405,6 @@ class ThermostatData: def setup(self): """Retrieve HomeData and HomeStatus by NetAtmo API.""" - import pyatmo - try: self.homedata = pyatmo.HomeData(self.auth) self.homestatus = pyatmo.HomeStatus(self.auth, home_id=self.home_id) @@ -423,8 +418,6 @@ class ThermostatData: @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Call the NetAtmo API to update the data.""" - import pyatmo - try: self.homestatus = pyatmo.HomeStatus(self.auth, home_id=self.home_id) except TypeError: diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index 83091368aff..efb2840216b 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -3,7 +3,7 @@ "name": "Netatmo", "documentation": "https://www.home-assistant.io/integrations/netatmo", "requirements": [ - "pyatmo==2.2.1" + "pyatmo==2.3.2" ], "dependencies": [ "webhook" diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 9e68c078cdc..70b6297388c 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -4,6 +4,7 @@ import threading from datetime import timedelta from time import time +import pyatmo import requests import voluptuous as vol @@ -79,6 +80,7 @@ SENSOR_TYPES = { "gustangle": ["Gust Angle", "", "mdi:compass", None], "gustangle_value": ["Gust Angle Value", "º", "mdi:compass", None], "guststrength": ["Gust Strength", "km/h", "mdi:weather-windy", None], + "reachable": ["Reachability", "", "mdi:signal", None], "rf_status": ["Radio", "", "mdi:signal", None], "rf_status_lvl": ["Radio_lvl", "", "mdi:signal", None], "wifi_status": ["Wifi", "", "mdi:wifi", None], @@ -174,8 +176,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if _dev: add_entities(_dev, True) - import pyatmo - for data_class in [pyatmo.WeatherStationData, pyatmo.HomeCoachData]: try: data = NetatmoData(auth, data_class, config.get(CONF_STATION)) @@ -376,6 +376,8 @@ class NetatmoSensor(Entity): self._state = "N (%d\xb0)" % data["GustAngle"] elif self.type == "guststrength": self._state = data["GustStrength"] + elif self.type == "reachable": + self._state = data["reachable"] elif self.type == "rf_status_lvl": self._state = data["rf_status"] elif self.type == "rf_status": @@ -512,8 +514,6 @@ class NetatmoPublicData: @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Request an update from the Netatmo API.""" - import pyatmo - data = pyatmo.PublicData( self.auth, LAT_NE=self.lat_ne, @@ -559,12 +559,10 @@ class NetatmoData: if time() < self._next_update or not self._update_in_progress.acquire(False): return try: - from pyatmo import NoDevice - try: self.station_data = self.data_class(self.auth) _LOGGER.debug("%s detected!", str(self.data_class.__name__)) - except NoDevice: + except pyatmo.NoDevice: _LOGGER.warning( "No Weather or HomeCoach devices found for %s", str(self.station) ) diff --git a/homeassistant/components/netgear/device_tracker.py b/homeassistant/components/netgear/device_tracker.py index b52e446ba5d..2e20f6423a5 100644 --- a/homeassistant/components/netgear/device_tracker.py +++ b/homeassistant/components/netgear/device_tracker.py @@ -1,6 +1,7 @@ """Support for Netgear routers.""" import logging +from pynetgear import Netgear import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -71,14 +72,13 @@ class NetgearDeviceScanner(DeviceScanner): accesspoints, ): """Initialize the scanner.""" - import pynetgear self.tracked_devices = devices self.excluded_devices = excluded_devices self.tracked_accesspoints = accesspoints self.last_results = [] - self._api = pynetgear.Netgear(password, host, username, port, ssl) + self._api = Netgear(password, host, username, port, ssl) _LOGGER.info("Logging in") diff --git a/homeassistant/components/netgear_lte/__init__.py b/homeassistant/components/netgear_lte/__init__.py index 2514b37657f..4758a13c391 100644 --- a/homeassistant/components/netgear_lte/__init__.py +++ b/homeassistant/components/netgear_lte/__init__.py @@ -5,6 +5,7 @@ import logging import aiohttp import attr +import eternalegypt import voluptuous as vol from homeassistant.const import ( @@ -139,7 +140,6 @@ class ModemData: async def async_update(self): """Call the API to update the data.""" - import eternalegypt try: self.data = await self.modem.information() @@ -264,7 +264,6 @@ async def async_setup(hass, config): async def _setup_lte(hass, lte_config): """Set up a Netgear LTE modem.""" - import eternalegypt host = lte_config[CONF_HOST] password = lte_config[CONF_PASSWORD] @@ -322,7 +321,6 @@ async def _login(hass, modem_data, password): async def _retry_login(hass, modem_data, password): """Sleep and retry setup.""" - import eternalegypt _LOGGER.warning("Could not connect to %s. Will keep trying", modem_data.host) diff --git a/homeassistant/components/netgear_lte/notify.py b/homeassistant/components/netgear_lte/notify.py index 4f13662519d..9700ee3c715 100644 --- a/homeassistant/components/netgear_lte/notify.py +++ b/homeassistant/components/netgear_lte/notify.py @@ -2,6 +2,7 @@ import logging import attr +import eternalegypt from homeassistant.components.notify import ATTR_TARGET, BaseNotificationService, DOMAIN @@ -27,7 +28,6 @@ class NetgearNotifyService(BaseNotificationService): async def async_send_message(self, message="", **kwargs): """Send a message to a user.""" - import eternalegypt modem_data = self.hass.data[DATA_KEY].get_modem_data(self.config) if not modem_data: diff --git a/homeassistant/components/neurio_energy/sensor.py b/homeassistant/components/neurio_energy/sensor.py index eac716573db..894bfae6180 100644 --- a/homeassistant/components/neurio_energy/sensor.py +++ b/homeassistant/components/neurio_energy/sensor.py @@ -1,15 +1,16 @@ """Support for monitoring a Neurio energy sensor.""" -import logging from datetime import timedelta +import logging +import neurio import requests.exceptions import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_API_KEY, POWER_WATT, ENERGY_KILO_WATT_HOUR +from homeassistant.const import CONF_API_KEY, ENERGY_KILO_WATT_HOUR, POWER_WATT +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -69,8 +70,6 @@ class NeurioData: def __init__(self, api_key, api_secret, sensor_id): """Initialize the data.""" - import neurio - self.api_key = api_key self.api_secret = api_secret self.sensor_id = sensor_id diff --git a/homeassistant/components/niko_home_control/light.py b/homeassistant/components/niko_home_control/light.py index 4cb84956002..265e51d6e67 100644 --- a/homeassistant/components/niko_home_control/light.py +++ b/homeassistant/components/niko_home_control/light.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +import nikohomecontrol import voluptuous as vol # Import the device class from the component that you want to support @@ -20,8 +21,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_HOST): cv.string}) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Niko Home Control light platform.""" - import nikohomecontrol - host = config[CONF_HOST] try: diff --git a/homeassistant/components/nilu/air_quality.py b/homeassistant/components/nilu/air_quality.py index 8d3d61befd5..8e851592de3 100644 --- a/homeassistant/components/nilu/air_quality.py +++ b/homeassistant/components/nilu/air_quality.py @@ -2,6 +2,22 @@ from datetime import timedelta import logging +from niluclient import ( + CO, + CO2, + NO, + NO2, + NOX, + OZONE, + PM1, + PM10, + PM25, + POLLUTION_INDEX, + SO2, + create_location_client, + create_station_client, + lookup_stations_in_area, +) import voluptuous as vol from homeassistant.components.air_quality import PLATFORM_SCHEMA, AirQualityEntity @@ -95,8 +111,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the NILU air quality sensor.""" - import niluclient as nilu - name = config.get(CONF_NAME) area = config.get(CONF_AREA) stations = config.get(CONF_STATION) @@ -105,15 +119,15 @@ def setup_platform(hass, config, add_entities, discovery_info=None): sensors = [] if area: - stations = nilu.lookup_stations_in_area(area) + stations = lookup_stations_in_area(area) elif not area and not stations: latitude = config.get(CONF_LATITUDE, hass.config.latitude) longitude = config.get(CONF_LONGITUDE, hass.config.longitude) - location_client = nilu.create_location_client(latitude, longitude) + location_client = create_location_client(latitude, longitude) stations = location_client.station_names for station in stations: - client = NiluData(nilu.create_station_client(station)) + client = NiluData(create_station_client(station)) client.update() if client.data.sensors: sensors.append(NiluSensor(client, name, show_on_map)) @@ -178,71 +192,51 @@ class NiluSensor(AirQualityEntity): @property def carbon_monoxide(self) -> str: """Return the CO (carbon monoxide) level.""" - from niluclient import CO - return self.get_component_state(CO) @property def carbon_dioxide(self) -> str: """Return the CO2 (carbon dioxide) level.""" - from niluclient import CO2 - return self.get_component_state(CO2) @property def nitrogen_oxide(self) -> str: """Return the N2O (nitrogen oxide) level.""" - from niluclient import NOX - return self.get_component_state(NOX) @property def nitrogen_monoxide(self) -> str: """Return the NO (nitrogen monoxide) level.""" - from niluclient import NO - return self.get_component_state(NO) @property def nitrogen_dioxide(self) -> str: """Return the NO2 (nitrogen dioxide) level.""" - from niluclient import NO2 - return self.get_component_state(NO2) @property def ozone(self) -> str: """Return the O3 (ozone) level.""" - from niluclient import OZONE - return self.get_component_state(OZONE) @property def particulate_matter_2_5(self) -> str: """Return the particulate matter 2.5 level.""" - from niluclient import PM25 - return self.get_component_state(PM25) @property def particulate_matter_10(self) -> str: """Return the particulate matter 10 level.""" - from niluclient import PM10 - return self.get_component_state(PM10) @property def particulate_matter_0_1(self) -> str: """Return the particulate matter 0.1 level.""" - from niluclient import PM1 - return self.get_component_state(PM1) @property def sulphur_dioxide(self) -> str: """Return the SO2 (sulphur dioxide) level.""" - from niluclient import SO2 - return self.get_component_state(SO2) def get_component_state(self, component_name: str) -> str: @@ -254,14 +248,12 @@ class NiluSensor(AirQualityEntity): def update(self) -> None: """Update the sensor.""" - import niluclient as nilu - self._api.update() sensors = self._api.data.sensors.values() if sensors: max_index = max([s.pollution_index for s in sensors]) self._max_aqi = max_index - self._attrs[ATTR_POLLUTION_INDEX] = nilu.POLLUTION_INDEX[self._max_aqi] + self._attrs[ATTR_POLLUTION_INDEX] = POLLUTION_INDEX[self._max_aqi] self._attrs[ATTR_AREA] = self._api.data.area diff --git a/homeassistant/components/nissan_leaf/__init__.py b/homeassistant/components/nissan_leaf/__init__.py index 38b7018af6c..0c72f4f43ea 100644 --- a/homeassistant/components/nissan_leaf/__init__.py +++ b/homeassistant/components/nissan_leaf/__init__.py @@ -3,7 +3,7 @@ from datetime import datetime, timedelta import asyncio import logging import sys - +from pycarwings2 import CarwingsError, Session import voluptuous as vol from homeassistant.const import CONF_PASSWORD, CONF_USERNAME @@ -95,7 +95,6 @@ START_CHARGE_LEAF_SCHEMA = vol.Schema({vol.Required(ATTR_VIN): cv.string}) def setup(hass, config): """Set up the Nissan Leaf component.""" - import pycarwings2 async def async_handle_update(service): """Handle service to update leaf data from Nissan servers.""" @@ -148,7 +147,7 @@ def setup(hass, config): try: # This might need to be made async (somehow) causes # homeassistant to be slow to start - sess = pycarwings2.Session(username, password, region) + sess = Session(username, password, region) leaf = sess.get_leaf() except KeyError: _LOGGER.error( @@ -156,7 +155,7 @@ def setup(hass, config): " do you actually have a Leaf connected to your account?" ) return False - except pycarwings2.CarwingsError: + except CarwingsError: _LOGGER.error( "An unknown error occurred while connecting to Nissan: %s", sys.exc_info()[0], @@ -274,7 +273,6 @@ class LeafDataStore: async def async_refresh_data(self, now): """Refresh the leaf data and update the datastore.""" - from pycarwings2 import CarwingsError if self.request_in_progress: _LOGGER.debug("Refresh currently in progress for %s", self.leaf.nickname) @@ -339,7 +337,6 @@ class LeafDataStore: async def async_get_battery(self): """Request battery update from Nissan servers.""" - from pycarwings2 import CarwingsError try: # Request battery update from the car @@ -389,7 +386,6 @@ class LeafDataStore: async def async_get_climate(self): """Request climate data from Nissan servers.""" - from pycarwings2 import CarwingsError try: return await self.hass.async_add_executor_job( diff --git a/homeassistant/components/norway_air/air_quality.py b/homeassistant/components/norway_air/air_quality.py index 9b30ad5aaa8..8e6c13260e5 100644 --- a/homeassistant/components/norway_air/air_quality.py +++ b/homeassistant/components/norway_air/air_quality.py @@ -1,14 +1,14 @@ """Sensor for checking the air quality forecast around Norway.""" +from datetime import timedelta import logging -from datetime import timedelta +import metno import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.air_quality import PLATFORM_SCHEMA, AirQualityEntity from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.helpers.aiohttp_client import async_get_clientsession - +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -71,8 +71,6 @@ class AirSensor(AirQualityEntity): def __init__(self, name, coordinates, forecast, session): """Initialize the sensor.""" - import metno - self._name = name self._api = metno.AirQualityData(coordinates, forecast, session) diff --git a/homeassistant/components/notion/.translations/nn.json b/homeassistant/components/notion/.translations/nn.json new file mode 100644 index 00000000000..6d373424c28 --- /dev/null +++ b/homeassistant/components/notion/.translations/nn.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Notion" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/notion/.translations/ru.json b/homeassistant/components/notion/.translations/ru.json index 7345cf46295..6c1d5f5d8d7 100644 --- a/homeassistant/components/notion/.translations/ru.json +++ b/homeassistant/components/notion/.translations/ru.json @@ -2,14 +2,14 @@ "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\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" + "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." }, "step": { "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u041b\u043e\u0433\u0438\u043d / \u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b" + "username": "\u041b\u043e\u0433\u0438\u043d / \u0410\u0434\u0440\u0435\u0441 \u044d\u043b. \u043f\u043e\u0447\u0442\u044b" }, "title": "Notion" } diff --git a/homeassistant/components/nuheat/__init__.py b/homeassistant/components/nuheat/__init__.py index f83611d3e40..88e10270d18 100644 --- a/homeassistant/components/nuheat/__init__.py +++ b/homeassistant/components/nuheat/__init__.py @@ -1,11 +1,11 @@ """Support for NuHeat thermostats.""" import logging +import nuheat import voluptuous as vol -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_DEVICES -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers import discovery +from homeassistant.const import CONF_DEVICES, CONF_PASSWORD, CONF_USERNAME +from homeassistant.helpers import config_validation as cv, discovery _LOGGER = logging.getLogger(__name__) @@ -29,8 +29,6 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): """Set up the NuHeat thermostat component.""" - import nuheat - conf = config[DOMAIN] username = conf.get(CONF_USERNAME) password = conf.get(CONF_PASSWORD) diff --git a/homeassistant/components/nuheat/climate.py b/homeassistant/components/nuheat/climate.py index 19780a35a20..5a4e4e233d1 100644 --- a/homeassistant/components/nuheat/climate.py +++ b/homeassistant/components/nuheat/climate.py @@ -9,9 +9,9 @@ from homeassistant.components.climate.const import ( HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_OFF, + PRESET_NONE, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, - PRESET_NONE, ) from homeassistant.const import ( ATTR_ENTITY_ID, diff --git a/homeassistant/components/oasa_telematics/sensor.py b/homeassistant/components/oasa_telematics/sensor.py index 0c16f3769d5..4bf6b395d5f 100644 --- a/homeassistant/components/oasa_telematics/sensor.py +++ b/homeassistant/components/oasa_telematics/sensor.py @@ -1,13 +1,14 @@ """Support for OASA Telematics from telematics.oasa.gr.""" -import logging from datetime import timedelta +import logging from operator import itemgetter +import oasatelematics 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, 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 from homeassistant.util import dt as dt_util @@ -128,8 +129,6 @@ class OASATelematicsData: def __init__(self, stop_id, route_id): """Initialize the data object.""" - import oasatelematics - self.stop_id = stop_id self.route_id = route_id self.info = self.empty_result() diff --git a/homeassistant/components/ohmconnect/sensor.py b/homeassistant/components/ohmconnect/sensor.py index 10b622d16c9..a9606e25bad 100644 --- a/homeassistant/components/ohmconnect/sensor.py +++ b/homeassistant/components/ohmconnect/sensor.py @@ -1,15 +1,16 @@ """Support for OhmConnect.""" -import logging from datetime import timedelta +import logging +import defusedxml.ElementTree as ET import requests import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_NAME import homeassistant.helpers.config_validation as cv -from homeassistant.util import Throttle from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -64,8 +65,6 @@ class OhmconnectSensor(Entity): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data from OhmConnect.""" - import defusedxml.ElementTree as ET - try: url = ("https://login.ohmconnect.com" "/verify-ohm-hour/{}").format( self._ohmid diff --git a/homeassistant/components/onkyo/manifest.json b/homeassistant/components/onkyo/manifest.json index 5bb116dece8..ef28c14a8b0 100644 --- a/homeassistant/components/onkyo/manifest.json +++ b/homeassistant/components/onkyo/manifest.json @@ -3,7 +3,7 @@ "name": "Onkyo", "documentation": "https://www.home-assistant.io/integrations/onkyo", "requirements": [ - "onkyo-eiscp==1.2.4" + "onkyo-eiscp==1.2.7" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/onkyo/media_player.py b/homeassistant/components/onkyo/media_player.py index af92f6c5f05..86f0f418c3f 100644 --- a/homeassistant/components/onkyo/media_player.py +++ b/homeassistant/components/onkyo/media_player.py @@ -3,6 +3,8 @@ import logging from typing import List import voluptuous as vol +import eiscp +from eiscp import eISCP from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA from homeassistant.components.media_player.const import ( @@ -29,9 +31,11 @@ _LOGGER = logging.getLogger(__name__) CONF_SOURCES = "sources" CONF_MAX_VOLUME = "max_volume" +CONF_RECEIVER_MAX_VOLUME = "receiver_max_volume" DEFAULT_NAME = "Onkyo Receiver" -SUPPORTED_MAX_VOLUME = 80 +SUPPORTED_MAX_VOLUME = 100 +DEFAULT_RECEIVER_MAX_VOLUME = 80 SUPPORT_ONKYO = ( SUPPORT_VOLUME_SET @@ -75,8 +79,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_MAX_VOLUME, default=SUPPORTED_MAX_VOLUME): vol.All( - vol.Coerce(int), vol.Range(min=1, max=SUPPORTED_MAX_VOLUME) + vol.Coerce(int), vol.Range(min=1, max=100) ), + vol.Optional( + CONF_RECEIVER_MAX_VOLUME, default=DEFAULT_RECEIVER_MAX_VOLUME + ): vol.All(vol.Coerce(int), vol.Range(min=0)), vol.Optional(CONF_SOURCES, default=DEFAULT_SOURCES): {cv.string: cv.string}, } ) @@ -133,9 +140,6 @@ def determine_zones(receiver): def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Onkyo platform.""" - import eiscp - from eiscp import eISCP - host = config.get(CONF_HOST) hosts = [] @@ -164,6 +168,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): config.get(CONF_SOURCES), name=config.get(CONF_NAME), max_volume=config.get(CONF_MAX_VOLUME), + receiver_max_volume=config.get(CONF_RECEIVER_MAX_VOLUME), ) ) KNOWN_HOSTS.append(host) @@ -179,6 +184,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): receiver, config.get(CONF_SOURCES), name=f"{config[CONF_NAME]} Zone 2", + max_volume=config.get(CONF_MAX_VOLUME), + receiver_max_volume=config.get(CONF_RECEIVER_MAX_VOLUME), ) ) # Add Zone3 if available @@ -190,6 +197,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): receiver, config.get(CONF_SOURCES), name=f"{config[CONF_NAME]} Zone 3", + max_volume=config.get(CONF_MAX_VOLUME), + receiver_max_volume=config.get(CONF_RECEIVER_MAX_VOLUME), ) ) except OSError: @@ -205,7 +214,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class OnkyoDevice(MediaPlayerDevice): """Representation of an Onkyo device.""" - def __init__(self, receiver, sources, name=None, max_volume=SUPPORTED_MAX_VOLUME): + def __init__( + self, + receiver, + sources, + name=None, + max_volume=SUPPORTED_MAX_VOLUME, + receiver_max_volume=DEFAULT_RECEIVER_MAX_VOLUME, + ): """Initialize the Onkyo Receiver.""" self._receiver = receiver self._muted = False @@ -215,6 +231,7 @@ class OnkyoDevice(MediaPlayerDevice): name or f"{receiver.info['model_name']}_{receiver.info['identifier']}" ) self._max_volume = max_volume + self._receiver_max_volume = receiver_max_volume self._current_source = None self._source_list = list(sources.values()) self._source_mapping = sources @@ -264,14 +281,17 @@ class OnkyoDevice(MediaPlayerDevice): if source in self._source_mapping: self._current_source = self._source_mapping[source] break - self._current_source = "_".join([i for i in current_source_tuples[1]]) + self._current_source = "_".join(current_source_tuples[1]) if preset_raw and self._current_source.lower() == "radio": self._attributes[ATTR_PRESET] = preset_raw[1] elif ATTR_PRESET in self._attributes: del self._attributes[ATTR_PRESET] self._muted = bool(mute_raw[1] == "on") - self._volume = volume_raw[1] / self._max_volume + # AMP_VOL/MAX_RECEIVER_VOL*(MAX_VOL/100) + self._volume = ( + volume_raw[1] / self._receiver_max_volume * (self._max_volume / 100) + ) if not hdmi_out_raw: return @@ -325,10 +345,15 @@ class OnkyoDevice(MediaPlayerDevice): """ Set volume level, input is range 0..1. - Onkyo ranges from 1-80 however 80 is usually far too loud - so allow the user to specify the upper range with CONF_MAX_VOLUME + However full volume on the amp is usually far too loud so allow the user to specify the upper range + with CONF_MAX_VOLUME. we change as per max_volume set by user. This means that if max volume is 80 then full + volume in HA will give 80% volume on the receiver. Then we convert + that to the correct scale for the receiver. """ - self.command(f"volume {int(volume * self._max_volume)}") + # HA_VOL * (MAX VOL / 100) * MAX_RECEIVER_VOL + self.command( + f"volume {int(volume * (self._max_volume / 100) * self._receiver_max_volume)}" + ) def volume_up(self): """Increase volume by 1 step.""" @@ -369,11 +394,19 @@ class OnkyoDevice(MediaPlayerDevice): class OnkyoDeviceZone(OnkyoDevice): """Representation of an Onkyo device's extra zone.""" - def __init__(self, zone, receiver, sources, name=None): + def __init__( + self, + zone, + receiver, + sources, + name=None, + max_volume=SUPPORTED_MAX_VOLUME, + receiver_max_volume=DEFAULT_RECEIVER_MAX_VOLUME, + ): """Initialize the Zone with the zone identifier.""" self._zone = zone self._supports_volume = True - super().__init__(receiver, sources, name) + super().__init__(receiver, sources, name, max_volume, receiver_max_volume) def update(self): """Get the latest state from the device.""" @@ -413,14 +446,17 @@ class OnkyoDeviceZone(OnkyoDevice): if source in self._source_mapping: self._current_source = self._source_mapping[source] break - self._current_source = "_".join([i for i in current_source_tuples[1]]) + self._current_source = "_".join(current_source_tuples[1]) self._muted = bool(mute_raw[1] == "on") if preset_raw and self._current_source.lower() == "radio": self._attributes[ATTR_PRESET] = preset_raw[1] elif ATTR_PRESET in self._attributes: del self._attributes[ATTR_PRESET] if self._supports_volume: - self._volume = volume_raw[1] / 80.0 + # AMP_VOL/MAX_RECEIVER_VOL*(MAX_VOL/100) + self._volume = ( + volume_raw[1] / self._receiver_max_volume * (self._max_volume / 100) + ) @property def supported_features(self): @@ -434,8 +470,18 @@ class OnkyoDeviceZone(OnkyoDevice): self.command(f"zone{self._zone}.power=standby") def set_volume_level(self, volume): - """Set volume level, input is range 0..1. Onkyo ranges from 1-80.""" - self.command(f"zone{self._zone}.volume={int(volume * 80)}") + """ + Set volume level, input is range 0..1. + + However full volume on the amp is usually far too loud so allow the user to specify the upper range + with CONF_MAX_VOLUME. we change as per max_volume set by user. This means that if max volume is 80 then full + volume in HA will give 80% volume on the receiver. Then we convert + that to the correct scale for the receiver. + """ + # HA_VOL * (MAX VOL / 100) * MAX_RECEIVER_VOL + self.command( + f"zone{self._zone}.volume={int(volume * (self._max_volume / 100) * self._receiver_max_volume)}" + ) def volume_up(self): """Increase volume by 1 step.""" diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index 29af1049fae..c73886c13c0 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -8,21 +8,29 @@ import asyncio import datetime as dt import logging import os -import voluptuous as vol +from aiohttp.client_exceptions import ClientConnectionError, ServerDisconnectedError +from haffmpeg.camera import CameraMjpeg +from haffmpeg.tools import IMAGE_JPEG, ImageFrame +import onvif +from onvif import ONVIFCamera, exceptions +import voluptuous as vol +from zeep.exceptions import Fault + +from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_STREAM, Camera +from homeassistant.components.camera.const import DOMAIN +from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS, DATA_FFMPEG from homeassistant.const import ( - CONF_NAME, + ATTR_ENTITY_ID, CONF_HOST, - CONF_USERNAME, + CONF_NAME, CONF_PASSWORD, CONF_PORT, - ATTR_ENTITY_ID, + CONF_USERNAME, ) -from homeassistant.components.camera import Camera, PLATFORM_SCHEMA, SUPPORT_STREAM -from homeassistant.components.camera.const import DOMAIN -from homeassistant.components.ffmpeg import DATA_FFMPEG, CONF_EXTRA_ARGUMENTS -import homeassistant.helpers.config_validation as cv +from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.service import async_extract_entity_ids import homeassistant.util.dt as dt_util @@ -122,9 +130,6 @@ class ONVIFHassCamera(Camera): _LOGGER.debug("Importing dependencies") - import onvif - from onvif import ONVIFCamera - _LOGGER.debug("Setting up the ONVIF camera component") self._username = config.get(CONF_USERNAME) @@ -156,10 +161,6 @@ class ONVIFHassCamera(Camera): Initializes the camera by obtaining the input uri and connecting to the camera. Also retrieves the ONVIF profiles. """ - from aiohttp.client_exceptions import ClientConnectionError - from homeassistant.exceptions import PlatformNotReady - from zeep.exceptions import Fault - try: _LOGGER.debug("Updating service addresses") await self._camera.update_xaddrs() @@ -169,7 +170,7 @@ class ONVIFHassCamera(Camera): self.setup_ptz() except ClientConnectionError as err: _LOGGER.warning( - "Couldn't connect to camera '%s', but will " "retry later. Error: %s", + "Couldn't connect to camera '%s', but will retry later. Error: %s", self._name, err, ) @@ -184,8 +185,6 @@ class ONVIFHassCamera(Camera): async def async_check_date_and_time(self): """Warns if camera and system date not synced.""" - from aiohttp.client_exceptions import ServerDisconnectedError - _LOGGER.debug("Setting up the ONVIF device management service") devicemgmt = self._camera.create_devicemgmt_service() @@ -228,8 +227,6 @@ class ONVIFHassCamera(Camera): async def async_obtain_input_uri(self): """Set the input uri for the camera.""" - from onvif import exceptions - _LOGGER.debug( "Connecting with ONVIF Camera: %s on port %s", self._host, self._port ) @@ -289,8 +286,6 @@ class ONVIFHassCamera(Camera): async def async_perform_ptz(self, pan, tilt, zoom): """Perform a PTZ action on the camera.""" - from onvif import exceptions - if self._ptz_service is None: _LOGGER.warning("PTZ actions are not supported on camera '%s'", self._name) return @@ -332,7 +327,6 @@ class ONVIFHassCamera(Camera): async def async_camera_image(self): """Return a still image response from the camera.""" - from haffmpeg.tools import ImageFrame, IMAGE_JPEG _LOGGER.debug("Retrieving image from camera '%s'", self._name) @@ -347,8 +341,6 @@ class ONVIFHassCamera(Camera): async def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from the camera.""" - from haffmpeg.camera import CameraMjpeg - _LOGGER.debug("Handling mjpeg stream from camera '%s'", self._name) ffmpeg_manager = self.hass.data[DATA_FFMPEG] diff --git a/homeassistant/components/opencv/image_processing.py b/homeassistant/components/opencv/image_processing.py index 3c72af4f368..4a1b830a324 100644 --- a/homeassistant/components/opencv/image_processing.py +++ b/homeassistant/components/opencv/image_processing.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +import numpy import requests import voluptuous as vol @@ -15,6 +16,15 @@ from homeassistant.components.image_processing import ( from homeassistant.core import split_entity_id import homeassistant.helpers.config_validation as cv +try: + # Verify that the OpenCV python package is pre-installed + import cv2 + + CV2_IMPORTED = True +except ImportError: + CV2_IMPORTED = False + + _LOGGER = logging.getLogger(__name__) ATTR_MATCHES = "matches" @@ -86,11 +96,7 @@ def _get_default_classifier(dest_path): def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the OpenCV image processing platform.""" - try: - # Verify that the OpenCV python package is pre-installed - # pylint: disable=unused-import,unused-variable - import cv2 # noqa - except ImportError: + if not CV2_IMPORTED: _LOGGER.error( "No OpenCV library found! Install or compile for your system " "following instructions here: http://opencv.org/releases.html" @@ -154,9 +160,6 @@ class OpenCVImageProcessor(ImageProcessingEntity): def process_image(self, image): """Process the image.""" - import cv2 # pylint: disable=import-error - import numpy - cv_image = cv2.imdecode(numpy.asarray(bytearray(image)), cv2.IMREAD_UNCHANGED) for name, classifier in self._classifiers.items(): diff --git a/homeassistant/components/opencv/manifest.json b/homeassistant/components/opencv/manifest.json index 40421674a4b..bd82da000cf 100644 --- a/homeassistant/components/opencv/manifest.json +++ b/homeassistant/components/opencv/manifest.json @@ -3,7 +3,7 @@ "name": "Opencv", "documentation": "https://www.home-assistant.io/integrations/opencv", "requirements": [ - "numpy==1.17.1", + "numpy==1.17.3", "opencv-python-headless==4.1.1.26" ], "dependencies": [], diff --git a/homeassistant/components/openevse/sensor.py b/homeassistant/components/openevse/sensor.py index d29dec224bd..0ac655cd448 100644 --- a/homeassistant/components/openevse/sensor.py +++ b/homeassistant/components/openevse/sensor.py @@ -1,17 +1,18 @@ """Support for monitoring an OpenEVSE Charger.""" import logging +import openevsewifi from requests import RequestException import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - TEMP_CELSIUS, CONF_HOST, - ENERGY_KILO_WATT_HOUR, CONF_MONITORED_VARIABLES, + ENERGY_KILO_WATT_HOUR, + TEMP_CELSIUS, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -38,8 +39,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the OpenEVSE sensor.""" - import openevsewifi - host = config.get(CONF_HOST) monitored_variables = config.get(CONF_MONITORED_VARIABLES) diff --git a/homeassistant/components/openhardwaremonitor/sensor.py b/homeassistant/components/openhardwaremonitor/sensor.py index fc228ee26fb..0729943a770 100644 --- a/homeassistant/components/openhardwaremonitor/sensor.py +++ b/homeassistant/components/openhardwaremonitor/sensor.py @@ -7,6 +7,7 @@ import voluptuous as vol 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 from homeassistant.util import Throttle @@ -38,6 +39,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Open Hardware Monitor platform.""" data = OpenHardwareMonitorData(config, hass) + if data.data is None: + raise PlatformNotReady add_entities(data.devices, True) @@ -130,7 +133,7 @@ class OpenHardwareMonitorData: response = requests.get(data_url, timeout=30) self.data = response.json() except requests.exceptions.ConnectionError: - _LOGGER.error("ConnectionError: Is OpenHardwareMonitor running?") + _LOGGER.debug("ConnectionError: Is OpenHardwareMonitor running?") def initialize(self, now): """Parse of the sensors and adding of devices.""" diff --git a/homeassistant/components/opentherm_gw/.translations/ca.json b/homeassistant/components/opentherm_gw/.translations/ca.json index 0224d663a83..07567149063 100644 --- a/homeassistant/components/opentherm_gw/.translations/ca.json +++ b/homeassistant/components/opentherm_gw/.translations/ca.json @@ -19,5 +19,16 @@ } }, "title": "Passarel\u00b7la d'OpenTherm" + }, + "options": { + "step": { + "init": { + "data": { + "floor_temperature": "Temperatura de la planta", + "precision": "Precisi\u00f3" + }, + "description": "Opcions del la passarel\u00b7la d'enlla\u00e7 d\u2019OpenTherm" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/da.json b/homeassistant/components/opentherm_gw/.translations/da.json index b8abb48af4e..152e38a5bba 100644 --- a/homeassistant/components/opentherm_gw/.translations/da.json +++ b/homeassistant/components/opentherm_gw/.translations/da.json @@ -16,5 +16,16 @@ } }, "title": "OpenTherm Gateway" + }, + "options": { + "step": { + "init": { + "data": { + "floor_temperature": "Gulvtemperatur", + "precision": "Pr\u00e6cision" + }, + "description": "Indstillinger for OpenTherm Gateway" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/de.json b/homeassistant/components/opentherm_gw/.translations/de.json index 274dd46488b..3b18aa71b6c 100644 --- a/homeassistant/components/opentherm_gw/.translations/de.json +++ b/homeassistant/components/opentherm_gw/.translations/de.json @@ -10,8 +10,22 @@ "init": { "data": { "device": "Pfad oder URL", + "floor_temperature": "Boden-Temperatur", "id": "ID", - "name": "Name" + "name": "Name", + "precision": "Genauigkeit der Temperatur" + }, + "title": "OpenTherm Gateway" + } + }, + "title": "OpenTherm Gateway" + }, + "options": { + "step": { + "init": { + "data": { + "floor_temperature": "Boden-Temperatur", + "precision": "Genauigkeit" } } } diff --git a/homeassistant/components/opentherm_gw/.translations/en.json b/homeassistant/components/opentherm_gw/.translations/en.json index 65d7d9e92bb..a7e143505a8 100644 --- a/homeassistant/components/opentherm_gw/.translations/en.json +++ b/homeassistant/components/opentherm_gw/.translations/en.json @@ -19,5 +19,16 @@ } }, "title": "OpenTherm Gateway" + }, + "options": { + "step": { + "init": { + "data": { + "floor_temperature": "Floor Temperature", + "precision": "Precision" + }, + "description": "Options for the OpenTherm Gateway" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/es.json b/homeassistant/components/opentherm_gw/.translations/es.json index 8ad9d89b07a..bb8a8b20f36 100644 --- a/homeassistant/components/opentherm_gw/.translations/es.json +++ b/homeassistant/components/opentherm_gw/.translations/es.json @@ -19,5 +19,16 @@ } }, "title": "Gateway OpenTherm" + }, + "options": { + "step": { + "init": { + "data": { + "floor_temperature": "Temperatura del suelo", + "precision": "Precisi\u00f3n" + }, + "description": "Opciones para OpenTherm Gateway" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/fr.json b/homeassistant/components/opentherm_gw/.translations/fr.json index 82b9a7aee88..edde63d62b4 100644 --- a/homeassistant/components/opentherm_gw/.translations/fr.json +++ b/homeassistant/components/opentherm_gw/.translations/fr.json @@ -10,6 +10,7 @@ "init": { "data": { "device": "Chemin ou URL", + "floor_temperature": "Temp\u00e9rature du sol", "id": "ID", "name": "Nom", "precision": "Pr\u00e9cision de la temp\u00e9rature climatique" @@ -18,5 +19,16 @@ } }, "title": "Passerelle OpenTherm" + }, + "options": { + "step": { + "init": { + "data": { + "floor_temperature": "Temp\u00e9rature du sol", + "precision": "Pr\u00e9cision" + }, + "description": "Options pour la passerelle OpenTherm" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/ko.json b/homeassistant/components/opentherm_gw/.translations/ko.json new file mode 100644 index 00000000000..f370427625d --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/ko.json @@ -0,0 +1,34 @@ +{ + "config": { + "error": { + "already_configured": "OpenTherm Gateway \uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "id_exists": "OpenTherm Gateway id \uac00 \uc774\ubbf8 \uc874\uc7ac\ud569\ub2c8\ub2e4", + "serial_error": "\uae30\uae30 \uc5f0\uacb0 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4", + "timeout": "\uc5f0\uacb0 \uc2dc\ub3c4 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "step": { + "init": { + "data": { + "device": "\uacbd\ub85c \ub610\ub294 URL", + "floor_temperature": "\uc2e4\ub0b4\uc628\ub3c4 \uc18c\uc218\uc810 \ubc84\ub9bc", + "id": "ID", + "name": "\uc774\ub984", + "precision": "\uc2e4\ub0b4\uc628\ub3c4 \uc815\ubc00\ub3c4" + }, + "title": "OpenTherm Gateway" + } + }, + "title": "OpenTherm Gateway" + }, + "options": { + "step": { + "init": { + "data": { + "floor_temperature": "\uc628\ub3c4 \uc18c\uc218\uc810 \ubc84\ub9bc", + "precision": "\uc815\ubc00\ub3c4" + }, + "description": "OpenTherm Gateway \uc635\uc158" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/lb.json b/homeassistant/components/opentherm_gw/.translations/lb.json index ec1f719a6cc..505815dcb4d 100644 --- a/homeassistant/components/opentherm_gw/.translations/lb.json +++ b/homeassistant/components/opentherm_gw/.translations/lb.json @@ -19,5 +19,16 @@ } }, "title": "OpenTherm Gateway" + }, + "options": { + "step": { + "init": { + "data": { + "floor_temperature": "Buedem Temperatur", + "precision": "Pr\u00e4zisioun" + }, + "description": "Optioune fir OpenTherm Gateway" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/nl.json b/homeassistant/components/opentherm_gw/.translations/nl.json index 4fec1baba7b..dbed3326b4a 100644 --- a/homeassistant/components/opentherm_gw/.translations/nl.json +++ b/homeassistant/components/opentherm_gw/.translations/nl.json @@ -1,14 +1,34 @@ { "config": { + "error": { + "already_configured": "Gateway al geconfigureerd", + "id_exists": "Gateway id bestaat al", + "serial_error": "Fout bij het verbinden met het apparaat", + "timeout": "Er is een time-out opgetreden voor de verbindingspoging" + }, "step": { "init": { "data": { "device": "Pad of URL", - "id": "ID" + "floor_temperature": "Vloertemperatuur", + "id": "ID", + "name": "Naam", + "precision": "Klimaattemperatuur precisie" }, "title": "OpenTherm Gateway" } }, "title": "OpenTherm Gateway" + }, + "options": { + "step": { + "init": { + "data": { + "floor_temperature": "Vloertemperatuur", + "precision": "Precisie" + }, + "description": "Opties voor de OpenTherm Gateway" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/no.json b/homeassistant/components/opentherm_gw/.translations/no.json index 6104aa7de72..9eb4444cbf1 100644 --- a/homeassistant/components/opentherm_gw/.translations/no.json +++ b/homeassistant/components/opentherm_gw/.translations/no.json @@ -19,5 +19,16 @@ } }, "title": "OpenTherm Gateway" + }, + "options": { + "step": { + "init": { + "data": { + "floor_temperature": "Etasje Temperatur", + "precision": "Presisjon" + }, + "description": "Alternativer for OpenTherm Gateway" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/pl.json b/homeassistant/components/opentherm_gw/.translations/pl.json index 7e4a0eed013..e4403420b11 100644 --- a/homeassistant/components/opentherm_gw/.translations/pl.json +++ b/homeassistant/components/opentherm_gw/.translations/pl.json @@ -1,11 +1,33 @@ { "config": { + "error": { + "already_configured": "Bramka jest ju\u017c skonfigurowana", + "id_exists": "Identyfikator bramki ju\u017c istnieje", + "serial_error": "B\u0142\u0105d po\u0142\u0105czenia z urz\u0105dzeniem", + "timeout": "Up\u0142yn\u0105\u0142 limit czasu pr\u00f3by po\u0142\u0105czenia" + }, "step": { "init": { "data": { "device": "\u015acie\u017cka lub adres URL", - "name": "Nazwa" - } + "floor_temperature": "Temperatura pod\u0142ogi", + "id": "Identyfikator", + "name": "Nazwa", + "precision": "Precyzja temperatury" + }, + "title": "Bramka OpenTherm" + } + }, + "title": "Bramka OpenTherm" + }, + "options": { + "step": { + "init": { + "data": { + "floor_temperature": "Temperatura pod\u0142ogi", + "precision": "Precyzja" + }, + "description": "Opcje dla bramki OpenTherm" } } } diff --git a/homeassistant/components/opentherm_gw/.translations/pt.json b/homeassistant/components/opentherm_gw/.translations/pt.json new file mode 100644 index 00000000000..960e3a9cf5c --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/pt.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "init": { + "data": { + "id": "", + "name": "Nome" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/ru.json b/homeassistant/components/opentherm_gw/.translations/ru.json index 718322ec171..0719857a7d3 100644 --- a/homeassistant/components/opentherm_gw/.translations/ru.json +++ b/homeassistant/components/opentherm_gw/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "id_exists": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442.", "serial_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443.", "timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f." @@ -19,5 +19,16 @@ } }, "title": "OpenTherm" + }, + "options": { + "step": { + "init": { + "data": { + "floor_temperature": "\u0422\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 \u043f\u043e\u043b\u0430", + "precision": "\u0422\u043e\u0447\u043d\u043e\u0441\u0442\u044c" + }, + "description": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0434\u043b\u044f \u0448\u043b\u044e\u0437\u0430 Opentherm" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/sl.json b/homeassistant/components/opentherm_gw/.translations/sl.json index 5de551d5d0c..426459237aa 100644 --- a/homeassistant/components/opentherm_gw/.translations/sl.json +++ b/homeassistant/components/opentherm_gw/.translations/sl.json @@ -19,5 +19,16 @@ } }, "title": "OpenTherm Prehod" + }, + "options": { + "step": { + "init": { + "data": { + "floor_temperature": "Temperatura nadstropja", + "precision": "Natan\u010dnost" + }, + "description": "Mo\u017enosti za prehod OpenTherm" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/zh-Hant.json b/homeassistant/components/opentherm_gw/.translations/zh-Hant.json index 648f156e864..0d2842ce767 100644 --- a/homeassistant/components/opentherm_gw/.translations/zh-Hant.json +++ b/homeassistant/components/opentherm_gw/.translations/zh-Hant.json @@ -19,5 +19,16 @@ } }, "title": "OpenTherm \u9598\u9053\u5668" + }, + "options": { + "step": { + "init": { + "data": { + "floor_temperature": "\u6a13\u5c64\u6eab\u5ea6", + "precision": "\u6e96\u78ba\u5ea6" + }, + "description": "OpenTherm \u9598\u9053\u5668\u9078\u9805" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/__init__.py b/homeassistant/components/opentherm_gw/__init__.py index a32c375ac65..643f80ae8f9 100644 --- a/homeassistant/components/opentherm_gw/__init__.py +++ b/homeassistant/components/opentherm_gw/__init__.py @@ -6,6 +6,7 @@ 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 @@ -16,13 +17,13 @@ from homeassistant.const import ( ATTR_TEMPERATURE, ATTR_TIME, CONF_DEVICE, + CONF_ID, CONF_NAME, EVENT_HOMEASSISTANT_STOP, PRECISION_HALVES, PRECISION_TENTHS, PRECISION_WHOLE, ) -from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import async_dispatcher_send import homeassistant.helpers.config_validation as cv @@ -36,6 +37,7 @@ from .const import ( CONF_PRECISION, DATA_GATEWAYS, DATA_OPENTHERM_GW, + DOMAIN, SERVICE_RESET_GATEWAY, SERVICE_SET_CLOCK, SERVICE_SET_CONTROL_SETPOINT, @@ -50,8 +52,6 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -DOMAIN = "opentherm_gw" - CLIMATE_SCHEMA = vol.Schema( { vol.Optional(CONF_PRECISION): vol.In( @@ -75,25 +75,46 @@ CONFIG_SCHEMA = vol.Schema( ) +async def options_updated(hass, entry): + """Handle options update.""" + gateway = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][entry.data[CONF_ID]] + async_dispatcher_send(hass, gateway.options_update_signal, entry) + + +async def async_setup_entry(hass, config_entry): + """Set up the OpenTherm Gateway component.""" + if DATA_OPENTHERM_GW not in hass.data: + hass.data[DATA_OPENTHERM_GW] = {DATA_GATEWAYS: {}} + + gateway = OpenThermGatewayDevice(hass, config_entry) + hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][config_entry.data[CONF_ID]] = gateway + + config_entry.add_update_listener(options_updated) + + # Schedule directly on the loop to avoid blocking HA startup. + hass.loop.create_task(gateway.connect_and_subscribe()) + + for comp in [COMP_BINARY_SENSOR, COMP_CLIMATE, COMP_SENSOR]: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, comp) + ) + + register_services(hass) + return True + + async def async_setup(hass, config): """Set up the OpenTherm Gateway component.""" - conf = config[DOMAIN] - hass.data[DATA_OPENTHERM_GW] = {DATA_GATEWAYS: {}} - for gw_id, cfg in conf.items(): - gateway = OpenThermGatewayDevice(hass, gw_id, cfg) - hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][gw_id] = gateway - hass.async_create_task( - async_load_platform(hass, COMP_CLIMATE, DOMAIN, gw_id, config) - ) - hass.async_create_task( - async_load_platform(hass, COMP_BINARY_SENSOR, DOMAIN, gw_id, config) - ) - hass.async_create_task( - async_load_platform(hass, COMP_SENSOR, DOMAIN, gw_id, config) - ) - # Schedule directly on the loop to avoid blocking HA startup. - hass.loop.create_task(gateway.connect_and_subscribe(cfg[CONF_DEVICE])) - register_services(hass) + if not hass.config_entries.async_entries(DOMAIN) and DOMAIN in config: + conf = config[DOMAIN] + for device_id, device_config in conf.items(): + device_config[CONF_ID] = device_id + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=device_config + ) + ) return True @@ -326,20 +347,22 @@ def register_services(hass): class OpenThermGatewayDevice: """OpenTherm Gateway device class.""" - def __init__(self, hass, gw_id, config): + def __init__(self, hass, config_entry): """Initialize the OpenTherm Gateway.""" self.hass = hass - self.gw_id = gw_id - self.name = config.get(CONF_NAME, gw_id) - self.climate_config = config[CONF_CLIMATE] + self.device_path = config_entry.data[CONF_DEVICE] + self.gw_id = config_entry.data[CONF_ID] + self.name = config_entry.data[CONF_NAME] + self.climate_config = config_entry.options self.status = {} - self.update_signal = f"{DATA_OPENTHERM_GW}_{gw_id}_update" + 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() - async def connect_and_subscribe(self, device_path): + async def connect_and_subscribe(self): """Connect to serial device and subscribe report handler.""" - await self.gateway.connect(self.hass.loop, device_path) - _LOGGER.debug("Connected to OpenTherm Gateway at %s", device_path) + await self.gateway.connect(self.hass.loop, self.device_path) + _LOGGER.debug("Connected to OpenTherm Gateway at %s", self.device_path) async def cleanup(event): """Reset overrides on the gateway.""" diff --git a/homeassistant/components/opentherm_gw/binary_sensor.py b/homeassistant/components/opentherm_gw/binary_sensor.py index 614829265e2..36867feda61 100644 --- a/homeassistant/components/opentherm_gw/binary_sensor.py +++ b/homeassistant/components/opentherm_gw/binary_sensor.py @@ -2,6 +2,7 @@ import logging from homeassistant.components.binary_sensor import ENTITY_ID_FORMAT, BinarySensorDevice +from homeassistant.const import CONF_ID from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import async_generate_entity_id @@ -12,18 +13,21 @@ from .const import BINARY_SENSOR_INFO, DATA_GATEWAYS, DATA_OPENTHERM_GW _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the OpenTherm Gateway binary sensors.""" - if discovery_info is None: - return - gw_dev = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][discovery_info] sensors = [] for var, info in BINARY_SENSOR_INFO.items(): device_class = info[0] friendly_name_format = info[1] sensors.append( - OpenThermBinarySensor(gw_dev, var, device_class, friendly_name_format) + OpenThermBinarySensor( + hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][config_entry.data[CONF_ID]], + var, + device_class, + friendly_name_format, + ) ) + async_add_entities(sensors) diff --git a/homeassistant/components/opentherm_gw/climate.py b/homeassistant/components/opentherm_gw/climate.py index fab028560bb..44f143d64da 100644 --- a/homeassistant/components/opentherm_gw/climate.py +++ b/homeassistant/components/opentherm_gw/climate.py @@ -17,6 +17,7 @@ from homeassistant.components.climate.const import ( ) from homeassistant.const import ( ATTR_TEMPERATURE, + CONF_ID, PRECISION_HALVES, PRECISION_TENTHS, PRECISION_WHOLE, @@ -33,23 +34,28 @@ _LOGGER = logging.getLogger(__name__) SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the opentherm_gw device.""" - gw_dev = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][discovery_info] +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up an OpenTherm Gateway climate entity.""" + ents = [] + ents.append( + OpenThermClimate( + hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][config_entry.data[CONF_ID]], + config_entry.options, + ) + ) - gateway = OpenThermClimate(gw_dev) - async_add_entities([gateway]) + async_add_entities(ents) class OpenThermClimate(ClimateDevice): """Representation of a climate device.""" - def __init__(self, gw_dev): + def __init__(self, gw_dev, options): """Initialize the device.""" self._gateway = gw_dev self.friendly_name = gw_dev.name - self.floor_temp = gw_dev.climate_config[CONF_FLOOR_TEMP] - self.temp_precision = gw_dev.climate_config.get(CONF_PRECISION) + self.floor_temp = options[CONF_FLOOR_TEMP] + self.temp_precision = options.get(CONF_PRECISION) self._current_operation = None self._current_temperature = None self._hvac_mode = HVAC_MODE_HEAT @@ -60,12 +66,22 @@ class OpenThermClimate(ClimateDevice): self._away_state_a = False self._away_state_b = False + @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.async_schedule_update_ha_state() + async def async_added_to_hass(self): """Connect to the OpenTherm Gateway device.""" - _LOGGER.debug("Added device %s", self.friendly_name) + _LOGGER.debug("Added OpenTherm Gateway climate device %s", self.friendly_name) async_dispatcher_connect( self.hass, self._gateway.update_signal, self.receive_report ) + async_dispatcher_connect( + self.hass, self._gateway.options_update_signal, self.update_options + ) @callback def receive_report(self, status): diff --git a/homeassistant/components/opentherm_gw/config_flow.py b/homeassistant/components/opentherm_gw/config_flow.py new file mode 100644 index 00000000000..2d7a65bbd84 --- /dev/null +++ b/homeassistant/components/opentherm_gw/config_flow.py @@ -0,0 +1,142 @@ +"""OpenTherm Gateway config flow.""" +import asyncio +from serial import SerialException + +import pyotgw +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import ( + CONF_DEVICE, + CONF_ID, + CONF_NAME, + PRECISION_HALVES, + PRECISION_TENTHS, + PRECISION_WHOLE, +) +from homeassistant.core import callback + +import homeassistant.helpers.config_validation as cv + +from . import DOMAIN +from .const import CONF_FLOOR_TEMP, CONF_PRECISION + + +class OpenThermGwConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """OpenTherm Gateway Config Flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return OpenThermGwOptionsFlow(config_entry) + + async def async_step_init(self, info=None): + """Handle config flow initiation.""" + if info: + name = info[CONF_NAME] + device = info[CONF_DEVICE] + gw_id = cv.slugify(info.get(CONF_ID, name)) + + entries = [e.data for e in self.hass.config_entries.async_entries(DOMAIN)] + + if gw_id in [e[CONF_ID] for e in entries]: + return self._show_form({"base": "id_exists"}) + + if device in [e[CONF_DEVICE] for e in entries]: + return self._show_form({"base": "already_configured"}) + + async def test_connection(): + """Try to connect to the OpenTherm Gateway.""" + otgw = pyotgw.pyotgw() + status = await otgw.connect(self.hass.loop, device) + await otgw.disconnect() + return status.get(pyotgw.OTGW_ABOUT) + + try: + res = await asyncio.wait_for(test_connection(), timeout=10) + except asyncio.TimeoutError: + return self._show_form({"base": "timeout"}) + except SerialException: + return self._show_form({"base": "serial_error"}) + + if res: + return self._create_entry(gw_id, name, device) + + return self._show_form() + + async def async_step_user(self, info=None): + """Handle manual initiation of the config flow.""" + return await self.async_step_init(info) + + async def async_step_import(self, import_config): + """ + Import an OpenTherm Gateway device as a config entry. + + This flow is triggered by `async_setup` for configured devices. + """ + formatted_config = { + CONF_NAME: import_config.get(CONF_NAME, import_config[CONF_ID]), + CONF_DEVICE: import_config[CONF_DEVICE], + CONF_ID: import_config[CONF_ID], + } + return await self.async_step_init(info=formatted_config) + + def _show_form(self, errors=None): + """Show the config flow form with possible errors.""" + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Required(CONF_NAME): str, + vol.Required(CONF_DEVICE): str, + vol.Optional(CONF_ID): str, + } + ), + errors=errors or {}, + ) + + def _create_entry(self, gw_id, name, device): + """Create entry for the OpenTherm Gateway device.""" + return self.async_create_entry( + title=name, data={CONF_ID: gw_id, CONF_DEVICE: device, CONF_NAME: name} + ) + + +class OpenThermGwOptionsFlow(config_entries.OptionsFlow): + """Handle opentherm_gw options.""" + + def __init__(self, config_entry): + """Initialize the options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Manage the opentherm_gw options.""" + if user_input is not None: + if user_input.get(CONF_PRECISION) == 0: + user_input[CONF_PRECISION] = None + return self.async_create_entry(title="", data=user_input) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Optional( + CONF_PRECISION, + default=self.config_entry.options.get(CONF_PRECISION, 0), + ): vol.All( + vol.Coerce(float), + vol.In( + [0, PRECISION_TENTHS, PRECISION_HALVES, PRECISION_WHOLE] + ), + ), + vol.Optional( + CONF_FLOOR_TEMP, + default=self.config_entry.options.get(CONF_FLOOR_TEMP, False), + ): bool, + } + ), + ) diff --git a/homeassistant/components/opentherm_gw/const.py b/homeassistant/components/opentherm_gw/const.py index 60042b92867..bd9b372de33 100644 --- a/homeassistant/components/opentherm_gw/const.py +++ b/homeassistant/components/opentherm_gw/const.py @@ -18,6 +18,8 @@ DEVICE_CLASS_COLD = "cold" DEVICE_CLASS_HEAT = "heat" DEVICE_CLASS_PROBLEM = "problem" +DOMAIN = "opentherm_gw" + SERVICE_RESET_GATEWAY = "reset_gateway" SERVICE_SET_CLOCK = "set_clock" SERVICE_SET_CONTROL_SETPOINT = "set_control_setpoint" diff --git a/homeassistant/components/opentherm_gw/manifest.json b/homeassistant/components/opentherm_gw/manifest.json index 9c7f165c6df..a632096cd75 100644 --- a/homeassistant/components/opentherm_gw/manifest.json +++ b/homeassistant/components/opentherm_gw/manifest.json @@ -3,10 +3,11 @@ "name": "Opentherm Gateway", "documentation": "https://www.home-assistant.io/integrations/opentherm_gw", "requirements": [ - "pyotgw==0.4b4" + "pyotgw==0.5b0" ], "dependencies": [], "codeowners": [ "@mvn23" - ] -} + ], + "config_flow": true +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/sensor.py b/homeassistant/components/opentherm_gw/sensor.py index 1449caf5def..c77a73cd180 100644 --- a/homeassistant/components/opentherm_gw/sensor.py +++ b/homeassistant/components/opentherm_gw/sensor.py @@ -2,6 +2,7 @@ import logging from homeassistant.components.sensor import ENTITY_ID_FORMAT +from homeassistant.const import CONF_ID from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity, async_generate_entity_id @@ -12,19 +13,23 @@ from .const import DATA_GATEWAYS, DATA_OPENTHERM_GW, SENSOR_INFO _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the OpenTherm Gateway sensors.""" - if discovery_info is None: - return - gw_dev = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][discovery_info] sensors = [] for var, info in SENSOR_INFO.items(): device_class = info[0] unit = info[1] friendly_name_format = info[2] sensors.append( - OpenThermSensor(gw_dev, var, device_class, unit, friendly_name_format) + OpenThermSensor( + hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][config_entry.data[CONF_ID]], + var, + device_class, + unit, + friendly_name_format, + ) ) + async_add_entities(sensors) diff --git a/homeassistant/components/opentherm_gw/strings.json b/homeassistant/components/opentherm_gw/strings.json new file mode 100644 index 00000000000..1c246432fb1 --- /dev/null +++ b/homeassistant/components/opentherm_gw/strings.json @@ -0,0 +1,32 @@ +{ + "config": { + "title": "OpenTherm Gateway", + "step": { + "init": { + "title": "OpenTherm Gateway", + "data": { + "name": "Name", + "device": "Path or URL", + "id": "ID" + } + } + }, + "error": { + "already_configured": "Gateway already configured", + "id_exists": "Gateway id already exists", + "serial_error": "Error connecting to device", + "timeout": "Connection attempt timed out" + } + }, + "options": { + "step": { + "init": { + "description": "Options for the OpenTherm Gateway", + "data": { + "floor_temperature": "Floor Temperature", + "precision": "Precision" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv/.translations/ru.json b/homeassistant/components/openuv/.translations/ru.json index 58d57b28056..27d2921a7d4 100644 --- a/homeassistant/components/openuv/.translations/ru.json +++ b/homeassistant/components/openuv/.translations/ru.json @@ -2,7 +2,7 @@ "config": { "error": { "identifier_exists": "\u041a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u044b \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u044b.", - "invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API" + "invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API." }, "step": { "user": { diff --git a/homeassistant/components/openweathermap/sensor.py b/homeassistant/components/openweathermap/sensor.py index 51dc92623f3..23f88f59aad 100644 --- a/homeassistant/components/openweathermap/sensor.py +++ b/homeassistant/components/openweathermap/sensor.py @@ -2,6 +2,8 @@ from datetime import timedelta import logging +from pyowm import OWM +from pyowm.exceptions.api_call_error import APICallError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -56,7 +58,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the OpenWeatherMap sensor.""" - from pyowm import OWM if None in (hass.config.latitude, hass.config.longitude): _LOGGER.error("Latitude or longitude not set in Home Assistant config") @@ -127,8 +128,6 @@ class OpenWeatherMapSensor(Entity): def update(self): """Get the latest data from OWM and updates the states.""" - from pyowm.exceptions.api_call_error import APICallError - try: self.owa_client.update() except APICallError: @@ -201,8 +200,6 @@ class WeatherData: @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data from OpenWeatherMap.""" - from pyowm.exceptions.api_call_error import APICallError - try: obs = self.owm.weather_at_coords(self.latitude, self.longitude) except (APICallError, TypeError): diff --git a/homeassistant/components/openweathermap/weather.py b/homeassistant/components/openweathermap/weather.py index a51ea26607d..69ca965d660 100644 --- a/homeassistant/components/openweathermap/weather.py +++ b/homeassistant/components/openweathermap/weather.py @@ -2,6 +2,8 @@ from datetime import timedelta import logging +from pyowm import OWM +from pyowm.exceptions.api_call_error import APICallError import voluptuous as vol from homeassistant.components.weather import ( @@ -71,7 +73,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the OpenWeatherMap weather platform.""" - import pyowm longitude = config.get(CONF_LONGITUDE, round(hass.config.longitude, 5)) latitude = config.get(CONF_LATITUDE, round(hass.config.latitude, 5)) @@ -79,8 +80,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): mode = config.get(CONF_MODE) try: - owm = pyowm.OWM(config.get(CONF_API_KEY)) - except pyowm.exceptions.api_call_error.APICallError: + owm = OWM(config.get(CONF_API_KEY)) + except APICallError: _LOGGER.error("Error while connecting to OpenWeatherMap") return False @@ -225,8 +226,6 @@ class OpenWeatherMapWeather(WeatherEntity): def update(self): """Get the latest data from OWM and updates the states.""" - from pyowm.exceptions.api_call_error import APICallError - try: self._owm.update() self._owm.update_forecast() @@ -263,8 +262,6 @@ class WeatherData: @Throttle(MIN_TIME_BETWEEN_FORECAST_UPDATES) def update_forecast(self): """Get the latest forecast from OpenWeatherMap.""" - from pyowm.exceptions.api_call_error import APICallError - try: if self._mode == "daily": fcd = self.owm.daily_forecast_at_coords( diff --git a/homeassistant/components/orangepi_gpio/__init__.py b/homeassistant/components/orangepi_gpio/__init__.py index 7547342d898..71d8d65d8b8 100644 --- a/homeassistant/components/orangepi_gpio/__init__.py +++ b/homeassistant/components/orangepi_gpio/__init__.py @@ -1,18 +1,20 @@ """Support for controlling GPIO pins of a Orange Pi.""" + import logging +from OPi import GPIO + from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP +from .const import PIN_MODES + _LOGGER = logging.getLogger(__name__) -CONF_PIN_MODE = "pin_mode" DOMAIN = "orangepi_gpio" -PIN_MODES = ["pc", "zeroplus", "zeroplus2", "deo", "neocore2"] -def setup(hass, config): +async def async_setup(hass, config): """Set up the Orange Pi GPIO component.""" - from OPi import GPIO def cleanup_gpio(event): """Stuff to do before stopping.""" @@ -20,68 +22,31 @@ def setup(hass, config): def prepare_gpio(event): """Stuff to do when home assistant starts.""" - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, cleanup_gpio) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, cleanup_gpio) - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, prepare_gpio) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, prepare_gpio) return True def setup_mode(mode): """Set GPIO pin mode.""" - from OPi import GPIO - - if mode == "pc": - import orangepi.pc - - GPIO.setmode(orangepi.pc.BOARD) - elif mode == "zeroplus": - import orangepi.zeroplus - - GPIO.setmode(orangepi.zeroplus.BOARD) - elif mode == "zeroplus2": - import orangepi.zeroplus - - GPIO.setmode(orangepi.zeroplus2.BOARD) - elif mode == "duo": - import nanopi.duo - - GPIO.setmode(nanopi.duo.BOARD) - elif mode == "neocore2": - import nanopi.neocore2 - - GPIO.setmode(nanopi.neocore2.BOARD) - - -def setup_output(port): - """Set up a GPIO as output.""" - from OPi import GPIO - - GPIO.setup(port, GPIO.OUT) + _LOGGER.debug("Setting GPIO pin mode as %s", PIN_MODES[mode]) + GPIO.setmode(PIN_MODES[mode]) def setup_input(port): """Set up a GPIO as input.""" - from OPi import GPIO - + _LOGGER.debug("Setting up GPIO pin %i as input", port) GPIO.setup(port, GPIO.IN) -def write_output(port, value): - """Write a value to a GPIO.""" - from OPi import GPIO - - GPIO.output(port, value) - - def read_input(port): """Read a value from a GPIO.""" - from OPi import GPIO - + _LOGGER.debug("Reading GPIO pin %i", port) return GPIO.input(port) def edge_detect(port, event_callback): """Add detection for RISING and FALLING events.""" - from OPi import GPIO - + _LOGGER.debug("Add callback for GPIO pin %i", port) GPIO.add_event_detect(port, GPIO.BOTH, callback=event_callback) diff --git a/homeassistant/components/orangepi_gpio/binary_sensor.py b/homeassistant/components/orangepi_gpio/binary_sensor.py index b89faf3e7d4..b89442a571c 100644 --- a/homeassistant/components/orangepi_gpio/binary_sensor.py +++ b/homeassistant/components/orangepi_gpio/binary_sensor.py @@ -1,50 +1,52 @@ """Support for binary sensor using Orange Pi GPIO.""" -import logging -from homeassistant.components import orangepi_gpio -from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA -from homeassistant.const import DEVICE_DEFAULT_NAME +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice -from . import CONF_PIN_MODE -from .const import CONF_INVERT_LOGIC, CONF_PORTS, PORT_SCHEMA - -_LOGGER = logging.getLogger(__name__) +from . import edge_detect, read_input, setup_input, setup_mode +from .const import CONF_INVERT_LOGIC, CONF_PIN_MODE, CONF_PORTS, PORT_SCHEMA PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(PORT_SCHEMA) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Orange Pi GPIO devices.""" - pin_mode = config[CONF_PIN_MODE] - orangepi_gpio.setup_mode(pin_mode) - - invert_logic = config[CONF_INVERT_LOGIC] - +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the Orange Pi GPIO platform.""" binary_sensors = [] + invert_logic = config[CONF_INVERT_LOGIC] + pin_mode = config[CONF_PIN_MODE] ports = config[CONF_PORTS] + + setup_mode(pin_mode) + for port_num, port_name in ports.items(): - binary_sensors.append(OPiGPIOBinarySensor(port_name, port_num, invert_logic)) - add_entities(binary_sensors, True) + binary_sensors.append( + OPiGPIOBinarySensor(hass, port_name, port_num, invert_logic) + ) + async_add_entities(binary_sensors) class OPiGPIOBinarySensor(BinarySensorDevice): """Represent a binary sensor that uses Orange Pi GPIO.""" - def __init__(self, name, port, invert_logic): + def __init__(self, hass, name, port, invert_logic): """Initialize the Orange Pi binary sensor.""" - self._name = name or DEVICE_DEFAULT_NAME + self._name = name self._port = port self._invert_logic = invert_logic self._state = None - orangepi_gpio.setup_input(self._port) + async def async_added_to_hass(self): + """Run when entity about to be added to hass.""" - def read_gpio(port): - """Read state from GPIO.""" - self._state = orangepi_gpio.read_input(self._port) - self.schedule_update_ha_state() + def gpio_edge_listener(port): + """Update GPIO when edge change is detected.""" + self.schedule_update_ha_state(True) - orangepi_gpio.edge_detect(self._port, read_gpio) + def setup_entity(): + setup_input(self._port) + edge_detect(self._port, gpio_edge_listener) + self.schedule_update_ha_state(True) + + await self.hass.async_add_executor_job(setup_entity) @property def should_poll(self): @@ -62,5 +64,5 @@ class OPiGPIOBinarySensor(BinarySensorDevice): return self._state != self._invert_logic def update(self): - """Update the GPIO state.""" - self._state = orangepi_gpio.read_input(self._port) + """Update state with new GPIO data.""" + self._state = read_input(self._port) diff --git a/homeassistant/components/orangepi_gpio/const.py b/homeassistant/components/orangepi_gpio/const.py index 6bb9ab1df1e..47ddf5b7085 100644 --- a/homeassistant/components/orangepi_gpio/const.py +++ b/homeassistant/components/orangepi_gpio/const.py @@ -1,19 +1,55 @@ """Constants for Orange Pi GPIO.""" + +from nanopi import duo, neocore2 +from orangepi import ( + lite, + lite2, + one, + oneplus, + pc, + pc2, + pcplus, + pi3, + plus2e, + prime, + r1, + winplus, + zero, + zeroplus, + zeroplus2, +) import voluptuous as vol from homeassistant.helpers import config_validation as cv -from . import CONF_PIN_MODE, PIN_MODES - CONF_INVERT_LOGIC = "invert_logic" +CONF_PIN_MODE = "pin_mode" CONF_PORTS = "ports" - DEFAULT_INVERT_LOGIC = False +PIN_MODES = { + "lite": lite.BOARD, + "lite2": lite2.BOARD, + "one": one.BOARD, + "oneplus": oneplus.BOARD, + "pc": pc.BOARD, + "pc2": pc2.BOARD, + "pcplus": pcplus.BOARD, + "pi3": pi3.BOARD, + "plus2e": plus2e.BOARD, + "prime": prime.BOARD, + "r1": r1.BOARD, + "winplus": winplus.BOARD, + "zero": zero.BOARD, + "zeroplus": zeroplus.BOARD, + "zeroplus2": zeroplus2.BOARD, + "duo": duo.BOARD, + "neocore2": neocore2.BOARD, +} _SENSORS_SCHEMA = vol.Schema({cv.positive_int: cv.string}) PORT_SCHEMA = { vol.Required(CONF_PORTS): _SENSORS_SCHEMA, - vol.Required(CONF_PIN_MODE): vol.In(PIN_MODES), + vol.Required(CONF_PIN_MODE): vol.In(PIN_MODES.keys()), vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean, } diff --git a/homeassistant/components/orangepi_gpio/manifest.json b/homeassistant/components/orangepi_gpio/manifest.json index 51bca8fbbbe..52c8f8f509f 100644 --- a/homeassistant/components/orangepi_gpio/manifest.json +++ b/homeassistant/components/orangepi_gpio/manifest.json @@ -3,7 +3,7 @@ "name": "Orangepi GPIO", "documentation": "https://www.home-assistant.io/integrations/orangepi_gpio", "requirements": [ - "OPi.GPIO==0.3.6" + "OPi.GPIO==0.4.0" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/oru/__init__.py b/homeassistant/components/oru/__init__.py new file mode 100644 index 00000000000..d1517ab0bf1 --- /dev/null +++ b/homeassistant/components/oru/__init__.py @@ -0,0 +1 @@ +"""The Orange and Rockland Utility smart energy meter integration.""" diff --git a/homeassistant/components/oru/manifest.json b/homeassistant/components/oru/manifest.json new file mode 100644 index 00000000000..ff5e74fd260 --- /dev/null +++ b/homeassistant/components/oru/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "oru", + "name": "Orange and Rockland Utility Smart Energy Meter Sensor", + "documentation": "https://www.home-assistant.io/integrations/oru", + "dependencies": [], + "codeowners": ["@bvlaicu"], + "requirements": ["oru==0.1.9"] +} \ No newline at end of file diff --git a/homeassistant/components/oru/sensor.py b/homeassistant/components/oru/sensor.py new file mode 100644 index 00000000000..e68d8e1c45a --- /dev/null +++ b/homeassistant/components/oru/sensor.py @@ -0,0 +1,92 @@ +"""Platform for sensor integration.""" +from datetime import timedelta +import logging + +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 +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) + +CONF_METER_NUMBER = "meter_number" + +SCAN_INTERVAL = timedelta(minutes=15) + +SENSOR_NAME = "ORU Current Energy Usage" +SENSOR_ICON = "mdi:counter" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_METER_NUMBER): cv.string}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the sensor platform.""" + + meter_number = config[CONF_METER_NUMBER] + + try: + meter = Meter(meter_number) + + except MeterError: + _LOGGER.error("Unable to create Oru meter") + return + + add_entities([CurrentEnergyUsageSensor(meter)], True) + + _LOGGER.debug("Oru meter_number = %s", meter_number) + + +class CurrentEnergyUsageSensor(Entity): + """Representation of the sensor.""" + + def __init__(self, meter): + """Initialize the sensor.""" + self._state = None + self._available = None + self.meter = meter + + @property + def unique_id(self): + """Return a unique, HASS-friendly identifier for this entity.""" + return self.meter.meter_id + + @property + def name(self): + """Return the name of the sensor.""" + return SENSOR_NAME + + @property + def icon(self): + """Return the icon of the sensor.""" + return SENSOR_ICON + + @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 ENERGY_KILO_WATT_HOUR + + def update(self): + """Fetch new state data for the sensor.""" + try: + last_read = self.meter.last_read() + + self._state = last_read + self._available = True + + _LOGGER.debug( + "%s = %s %s", self.name, self._state, self.unit_of_measurement + ) + except MeterError as err: + self._available = False + + _LOGGER.error("Unexpected oru meter error: %s", err) diff --git a/homeassistant/components/osramlightify/light.py b/homeassistant/components/osramlightify/light.py index 9a2da2bce06..05064861844 100644 --- a/homeassistant/components/osramlightify/light.py +++ b/homeassistant/components/osramlightify/light.py @@ -3,14 +3,15 @@ import logging import random import socket +from lightify import Lightify import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, + ATTR_EFFECT, ATTR_HS_COLOR, ATTR_TRANSITION, - ATTR_EFFECT, EFFECT_RANDOM, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, @@ -20,7 +21,6 @@ from homeassistant.components.light import ( SUPPORT_TRANSITION, Light, ) - from homeassistant.const import CONF_HOST import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util @@ -71,11 +71,9 @@ DEFAULT_KELVIN = 2700 def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Osram Lightify lights.""" - import lightify - host = config[CONF_HOST] try: - bridge = lightify.Lightify(host, log_level=logging.NOTSET) + bridge = Lightify(host, log_level=logging.NOTSET) except socket.error as err: msg = "Error connecting to bridge: {} due to: {}".format(host, str(err)) _LOGGER.exception(msg) diff --git a/homeassistant/components/otp/sensor.py b/homeassistant/components/otp/sensor.py index a175155e6f2..3c4cd464d44 100644 --- a/homeassistant/components/otp/sensor.py +++ b/homeassistant/components/otp/sensor.py @@ -1,13 +1,14 @@ """Support for One-Time Password (OTP).""" -import time import logging +import time +import pyotp import voluptuous as vol -from homeassistant.core import callback -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_NAME, CONF_TOKEN +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -41,8 +42,6 @@ class TOTPSensor(Entity): def __init__(self, name, token): """Initialize the sensor.""" - import pyotp - self._name = name self._otp = pyotp.TOTP(token) self._state = None diff --git a/homeassistant/components/owntracks/.translations/ru.json b/homeassistant/components/owntracks/.translations/ru.json index 6ebaa31cacf..31c3e77279d 100644 --- a/homeassistant/components/owntracks/.translations/ru.json +++ b/homeassistant/components/owntracks/.translations/ru.json @@ -4,7 +4,7 @@ "one_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." }, "create_entry": { - "default": "\u0415\u0441\u043b\u0438 \u0412\u0430\u0448\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0430 \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0435 Android, \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 [OwnTracks]({android_url}), \u0437\u0430\u0442\u0435\u043c preferences -> connection. \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0442\u0430\u043a, \u043a\u0430\u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043e \u043d\u0438\u0436\u0435:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: ``\n - Device ID: ``\n\n\u0415\u0441\u043b\u0438 \u0412\u0430\u0448\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0430 iOS, \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 [OwnTracks]({ios_url}), \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u043d\u0430 \u0437\u043d\u0430\u0447\u0435\u043a (i) \u0432 \u043b\u0435\u0432\u043e\u043c \u0432\u0435\u0440\u0445\u043d\u0435\u043c \u0443\u0433\u043b\u0443 -> settings. \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0442\u0430\u043a, \u043a\u0430\u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043e \u043d\u0438\u0436\u0435:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication\n - UserID: ``\n\n{secret}\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." + "default": "\u0415\u0441\u043b\u0438 \u0412\u0430\u0448\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0430 \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0435 Android, \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 [OwnTracks]({android_url}), \u0437\u0430\u0442\u0435\u043c preferences -> connection. \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0442\u0430\u043a, \u043a\u0430\u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043e \u043d\u0438\u0436\u0435:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: ``\n - Device ID: ``\n\n\u0415\u0441\u043b\u0438 \u0412\u0430\u0448\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0430 iOS, \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 [OwnTracks]({ios_url}), \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u043d\u0430 \u0437\u043d\u0430\u0447\u043e\u043a (i) \u0432 \u043b\u0435\u0432\u043e\u043c \u0432\u0435\u0440\u0445\u043d\u0435\u043c \u0443\u0433\u043b\u0443 -> settings. \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0442\u0430\u043a, \u043a\u0430\u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043e \u043d\u0438\u0436\u0435:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication\n - UserID: ``\n\n{secret}\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." }, "step": { "user": { diff --git a/homeassistant/components/owntracks/config_flow.py b/homeassistant/components/owntracks/config_flow.py index 67553ef608f..343a6d90b52 100644 --- a/homeassistant/components/owntracks/config_flow.py +++ b/homeassistant/components/owntracks/config_flow.py @@ -78,7 +78,10 @@ class OwnTracksFlow(config_entries.ConfigFlow): async def _get_webhook_id(self): """Generate webhook ID.""" webhook_id = self.hass.components.webhook.async_generate_id() - if self.hass.components.cloud.async_active_subscription(): + if ( + "cloud" in self.hass.config.components + and 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/messages.py b/homeassistant/components/owntracks/messages.py index 7ef31be1327..465d2762f74 100644 --- a/homeassistant/components/owntracks/messages.py +++ b/homeassistant/components/owntracks/messages.py @@ -79,6 +79,8 @@ def _parse_see_args(message, subscribe_topic): kwargs["attributes"]["address"] = message["addr"] if "cog" in message: kwargs["attributes"]["course"] = message["cog"] + if "bs" in message: + kwargs["attributes"]["battery_status"] = message["bs"] if "t" in message: if message["t"] in ("c", "u"): kwargs["source_type"] = SOURCE_TYPE_GPS diff --git a/homeassistant/components/panasonic_bluray/media_player.py b/homeassistant/components/panasonic_bluray/media_player.py index 393ecb827cc..4a816252580 100644 --- a/homeassistant/components/panasonic_bluray/media_player.py +++ b/homeassistant/components/panasonic_bluray/media_player.py @@ -2,9 +2,10 @@ from datetime import timedelta import logging +from panacotta import PanasonicBD 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, @@ -53,9 +54,7 @@ class PanasonicBluRay(MediaPlayerDevice): def __init__(self, ip, name): """Initialize the Panasonic Blue-ray device.""" - import panacotta - - self._device = panacotta.PanasonicBD(ip) + self._device = PanasonicBD(ip) self._name = name self._state = STATE_OFF self._position = 0 diff --git a/homeassistant/components/panasonic_viera/media_player.py b/homeassistant/components/panasonic_viera/media_player.py index d0b013c3bf3..0b19a8fa552 100644 --- a/homeassistant/components/panasonic_viera/media_player.py +++ b/homeassistant/components/panasonic_viera/media_player.py @@ -1,9 +1,11 @@ """Support for interface with a Panasonic Viera TV.""" import logging +from panasonic_viera import RemoteControl import voluptuous as vol +import wakeonlan -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_URL, SUPPORT_NEXT_TRACK, @@ -62,8 +64,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Panasonic Viera TV platform.""" - from panasonic_viera import RemoteControl - mac = config.get(CONF_MAC) name = config.get(CONF_NAME) port = config.get(CONF_PORT) @@ -95,8 +95,6 @@ class PanasonicVieraTVDevice(MediaPlayerDevice): def __init__(self, mac, name, remote, host, app_power, uuid=None): """Initialize the Panasonic device.""" - import wakeonlan - # Save a reference to the imported class self._wol = wakeonlan self._mac = mac diff --git a/homeassistant/components/pandora/media_player.py b/homeassistant/components/pandora/media_player.py index c242670ba48..417903c46e0 100644 --- a/homeassistant/components/pandora/media_player.py +++ b/homeassistant/components/pandora/media_player.py @@ -6,6 +6,8 @@ import re import shutil import signal +import pexpect + from homeassistant import util from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( @@ -104,8 +106,6 @@ class PandoraMediaPlayer(MediaPlayerDevice): def turn_on(self): """Turn the media player on.""" - import pexpect - if self._player_state != STATE_OFF: return self._pianobar = pexpect.spawn("pianobar") @@ -136,8 +136,6 @@ class PandoraMediaPlayer(MediaPlayerDevice): def turn_off(self): """Turn the media player off.""" - import pexpect - if self._pianobar is None: _LOGGER.info("Pianobar subprocess already stopped") return @@ -226,8 +224,6 @@ class PandoraMediaPlayer(MediaPlayerDevice): def _send_station_list_command(self): """Send a station list command.""" - import pexpect - self._pianobar.send("s") try: self._pianobar.expect("Select station:", timeout=1) @@ -248,8 +244,6 @@ class PandoraMediaPlayer(MediaPlayerDevice): def _query_for_playing_status(self): """Query system for info about current track.""" - import pexpect - self._clear_buffer() self._pianobar.send("i") try: @@ -372,8 +366,6 @@ class PandoraMediaPlayer(MediaPlayerDevice): This is necessary because there are a bunch of 00:00 in the buffer """ - import pexpect - try: while not self._pianobar.expect(".+", timeout=0.1): pass diff --git a/homeassistant/components/persistent_notification/__init__.py b/homeassistant/components/persistent_notification/__init__.py index 6b9c7c44ddf..33f17b18a80 100644 --- a/homeassistant/components/persistent_notification/__init__.py +++ b/homeassistant/components/persistent_notification/__init__.py @@ -52,11 +52,6 @@ STATE = "notifying" STATUS_UNREAD = "unread" STATUS_READ = "read" -WS_TYPE_GET_NOTIFICATIONS = "persistent_notification/get" -SCHEMA_WS_GET = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( - {vol.Required("type"): WS_TYPE_GET_NOTIFICATIONS} -) - @bind_hass def create(hass, message, title=None, notification_id=None): @@ -198,14 +193,13 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool: DOMAIN, SERVICE_MARK_READ, mark_read_service, SCHEMA_SERVICE_MARK_READ ) - hass.components.websocket_api.async_register_command( - WS_TYPE_GET_NOTIFICATIONS, websocket_get_notifications, SCHEMA_WS_GET - ) + hass.components.websocket_api.async_register_command(websocket_get_notifications) return True @callback +@websocket_api.websocket_command({vol.Required("type"): "persistent_notification/get"}) def websocket_get_notifications( hass: HomeAssistant, connection: websocket_api.ActiveConnection, diff --git a/homeassistant/components/piglow/light.py b/homeassistant/components/piglow/light.py index 31ece4a36a9..27bbb81d31f 100644 --- a/homeassistant/components/piglow/light.py +++ b/homeassistant/components/piglow/light.py @@ -2,18 +2,19 @@ import logging import subprocess +import piglow 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__) @@ -29,23 +30,20 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Piglow Light platform.""" - import piglow - if subprocess.getoutput("i2cdetect -q -y 1 | grep -o 54") != "54": _LOGGER.error("A Piglow device was not found") return False name = config.get(CONF_NAME) - add_entities([PiglowLight(piglow, name)]) + add_entities([PiglowLight(name)]) class PiglowLight(Light): """Representation of an Piglow Light.""" - def __init__(self, piglow, name): + def __init__(self, name): """Initialize an PiglowLight.""" - self._piglow = piglow self._name = name self._is_on = False self._brightness = 255 @@ -88,7 +86,7 @@ class PiglowLight(Light): def turn_on(self, **kwargs): """Instruct the light to turn on.""" - self._piglow.clear() + piglow.clear() if ATTR_BRIGHTNESS in kwargs: self._brightness = kwargs[ATTR_BRIGHTNESS] @@ -99,16 +97,16 @@ class PiglowLight(Light): rgb = color_util.color_hsv_to_RGB( self._hs_color[0], self._hs_color[1], self._brightness / 255 * 100 ) - self._piglow.red(rgb[0]) - self._piglow.green(rgb[1]) - self._piglow.blue(rgb[2]) - self._piglow.show() + piglow.red(rgb[0]) + piglow.green(rgb[1]) + piglow.blue(rgb[2]) + piglow.show() self._is_on = True self.schedule_update_ha_state() def turn_off(self, **kwargs): """Instruct the light to turn off.""" - self._piglow.clear() - self._piglow.show() + piglow.clear() + piglow.show() self._is_on = False self.schedule_update_ha_state() diff --git a/homeassistant/components/pjlink/media_player.py b/homeassistant/components/pjlink/media_player.py index 44b4055e032..6474165a6cd 100644 --- a/homeassistant/components/pjlink/media_player.py +++ b/homeassistant/components/pjlink/media_player.py @@ -105,10 +105,12 @@ class PjLinkDevice(MediaPlayerDevice): pwstate = projector.get_power() if pwstate in ("on", "warm-up"): self._pwstate = STATE_ON + self._muted = projector.get_mute()[1] + self._current_source = format_input_source(*projector.get_input()) else: self._pwstate = STATE_OFF - self._muted = projector.get_mute()[1] - self._current_source = format_input_source(*projector.get_input()) + self._muted = False + self._current_source = None except KeyError as err: if str(err) == "'OK'": self._pwstate = STATE_OFF diff --git a/homeassistant/components/plaato/.translations/ru.json b/homeassistant/components/plaato/.translations/ru.json index 59964fdedd6..dc06e3ddab0 100644 --- a/homeassistant/components/plaato/.translations/ru.json +++ b/homeassistant/components/plaato/.translations/ru.json @@ -5,7 +5,7 @@ "one_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." }, "create_entry": { - "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0432\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c webhooks \u0434\u043b\u044f Plaato Airlock\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." + "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0432\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c webhooks \u0434\u043b\u044f Plaato Airlock.\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." }, "step": { "user": { diff --git a/homeassistant/components/plex/.translations/ca.json b/homeassistant/components/plex/.translations/ca.json index a3ba5185371..7a8cf7a1424 100644 --- a/homeassistant/components/plex/.translations/ca.json +++ b/homeassistant/components/plex/.translations/ca.json @@ -4,6 +4,7 @@ "all_configured": "Tots els servidors enlla\u00e7ats ja estan configurats", "already_configured": "Aquest servidor Plex ja est\u00e0 configurat", "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", "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 1da4b4b4b49..99d5d4d1685 100644 --- a/homeassistant/components/plex/.translations/da.json +++ b/homeassistant/components/plex/.translations/da.json @@ -4,6 +4,7 @@ "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", "invalid_import": "Importeret konfiguration er ugyldig", "token_request_timeout": "Timeout ved hentning af token", "unknown": "Mislykkedes af ukendt \u00e5rsag" @@ -32,6 +33,10 @@ "description": "Flere servere til r\u00e5dighed, v\u00e6lg en:", "title": "V\u00e6lg Plex-server" }, + "start_website_auth": { + "description": "Forts\u00e6t for at autorisere p\u00e5 plex.tv.", + "title": "Tilslut Plex-server" + }, "user": { "data": { "manual_setup": "Manuel ops\u00e6tning", diff --git a/homeassistant/components/plex/.translations/de.json b/homeassistant/components/plex/.translations/de.json index 95083102273..4b24e6c78a6 100644 --- a/homeassistant/components/plex/.translations/de.json +++ b/homeassistant/components/plex/.translations/de.json @@ -1,24 +1,60 @@ { "config": { "abort": { - "discovery_no_file": "Es wurde keine alte Konfigurationsdatei gefunden" + "all_configured": "Alle verkn\u00fcpften Server sind bereits konfiguriert", + "already_configured": "Dieser Plex-Server ist bereits konfiguriert", + "already_in_progress": "Plex wird konfiguriert", + "discovery_no_file": "Es wurde keine alte Konfigurationsdatei gefunden", + "invalid_import": "Die importierte Konfiguration ist ung\u00fcltig", + "token_request_timeout": "Zeit\u00fcberschreitung beim Erhalt des Tokens", + "unknown": "Aus unbekanntem Grund fehlgeschlagen" + }, + "error": { + "faulty_credentials": "Autorisation fehlgeschlagen", + "no_servers": "Keine Server sind mit dem Konto verbunden", + "no_token": "Bereitstellen eines Tokens oder Ausw\u00e4hlen der manuellen Einrichtung", + "not_found": "Plex-Server nicht gefunden" }, "step": { "manual_setup": { + "data": { + "host": "Host", + "port": "Port", + "ssl": "SSL verwenden", + "token": "Token (falls erforderlich)", + "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" + }, "title": "Plex Server" }, + "select_server": { + "data": { + "server": "Server" + }, + "description": "Mehrere Server verf\u00fcgbar, w\u00e4hlen Sie einen aus:", + "title": "Plex-Server ausw\u00e4hlen" + }, "start_website_auth": { "description": "Weiter zur Autorisierung unter plex.tv.", "title": "Plex Server verbinden" }, "user": { - "description": "Fahren Sie mit der Autorisierung unter plex.tv fort oder konfigurieren Sie einen Server manuell." + "data": { + "manual_setup": "Manuelle Einrichtung", + "token": "Plex Token" + }, + "description": "Fahren Sie mit der Autorisierung unter plex.tv fort oder konfigurieren Sie einen Server manuell.", + "title": "Plex Server verbinden" } - } + }, + "title": "Plex" }, "options": { "step": { "plex_mp_settings": { + "data": { + "show_all_controls": "Alle Steuerelemente anzeigen", + "use_episode_art": "Episode-Bilder verwenden" + }, "description": "Optionen f\u00fcr Plex-Media-Player" } } diff --git a/homeassistant/components/plex/.translations/fr.json b/homeassistant/components/plex/.translations/fr.json index c9e61dcf2e9..c06d314ec72 100644 --- a/homeassistant/components/plex/.translations/fr.json +++ b/homeassistant/components/plex/.translations/fr.json @@ -4,6 +4,7 @@ "all_configured": "Tous les serveurs li\u00e9s sont d\u00e9j\u00e0 configur\u00e9s", "already_configured": "Ce serveur Plex est d\u00e9j\u00e0 configur\u00e9", "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", "token_request_timeout": "D\u00e9lai d'obtention du jeton", "unknown": "\u00c9chec pour une raison inconnue" @@ -32,6 +33,10 @@ "description": "Plusieurs serveurs disponibles, s\u00e9lectionnez-en un:", "title": "S\u00e9lectionnez le serveur Plex" }, + "start_website_auth": { + "description": "Continuer d'autoriser sur plex.tv.", + "title": "Connecter un serveur Plex" + }, "user": { "data": { "manual_setup": "Installation manuelle", diff --git a/homeassistant/components/plex/.translations/ko.json b/homeassistant/components/plex/.translations/ko.json index 171c656566d..f8e78945802 100644 --- a/homeassistant/components/plex/.translations/ko.json +++ b/homeassistant/components/plex/.translations/ko.json @@ -4,15 +4,28 @@ "all_configured": "\uc774\ubbf8 \uad6c\uc131\ub41c \ubaa8\ub4e0 \uc5f0\uacb0\ub41c \uc11c\ubc84", "already_configured": "\uc774 Plex \uc11c\ubc84\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "already_in_progress": "Plex \ub97c \uad6c\uc131 \uc911\uc785\ub2c8\ub2e4", + "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", + "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" }, "error": { "faulty_credentials": "\uc778\uc99d\uc5d0 \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4", "no_servers": "\uacc4\uc815\uc5d0 \uc5f0\uacb0\ub41c \uc11c\ubc84\uac00 \uc5c6\uc2b5\ub2c8\ub2e4", + "no_token": "\ud1a0\ud070\uc744 \uc785\ub825\ud558\uac70\ub098 \uc218\ub3d9 \uc124\uc815\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694", "not_found": "Plex \uc11c\ubc84\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4" }, "step": { + "manual_setup": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "port": "\ud3ec\ud2b8", + "ssl": "SSL \uc0ac\uc6a9", + "token": "\ud1a0\ud070 (\ud544\uc694\ud55c \uacbd\uc6b0)", + "verify_ssl": "SSL \uc778\uc99d\uc11c \uac80\uc99d" + }, + "title": "Plex \uc11c\ubc84" + }, "select_server": { "data": { "server": "\uc11c\ubc84" @@ -20,14 +33,30 @@ "description": "\uc5ec\ub7ec \uc11c\ubc84\uac00 \uc0ac\uc6a9 \uac00\ub2a5\ud569\ub2c8\ub2e4. \ud558\ub098\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694:", "title": "Plex \uc11c\ubc84 \uc120\ud0dd" }, + "start_website_auth": { + "description": "plex.tv \uc5d0\uc11c \uc778\uc99d\uc744 \uc9c4\ud589\ud574\uc8fc\uc138\uc694.", + "title": "Plex \uc11c\ubc84 \uc5f0\uacb0" + }, "user": { "data": { + "manual_setup": "\uc218\ub3d9 \uc124\uc815", "token": "Plex \ud1a0\ud070" }, - "description": "\uc790\ub3d9 \uc124\uc815\uc744 \uc704\ud574 Plex \ud1a0\ud070\uc744 \uc785\ub825\ud558\uac70\ub098 \uc11c\ubc84\ub97c \uc218\ub3d9\uc73c\ub85c \uad6c\uc131\ud574\uc8fc\uc138\uc694.", + "description": "plex.tv \uc5d0\uc11c \uc778\uc99d\uc744 \uc9c4\ud589\ud558\uac70\ub098 \uc11c\ubc84\ub97c \uc218\ub3d9\uc73c\ub85c \uc124\uc815\ud574\uc8fc\uc138\uc694.", "title": "Plex \uc11c\ubc84 \uc5f0\uacb0" } }, "title": "Plex" + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "\ubaa8\ub4e0 \ucee8\ud2b8\ub864 \ud45c\uc2dc\ud558\uae30", + "use_episode_art": "\uc5d0\ud53c\uc18c\ub4dc \uc544\ud2b8 \uc0ac\uc6a9" + }, + "description": "Plex \ubbf8\ub514\uc5b4 \ud50c\ub808\uc774\uc5b4 \uc635\uc158" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/nl.json b/homeassistant/components/plex/.translations/nl.json new file mode 100644 index 00000000000..c971ebb4762 --- /dev/null +++ b/homeassistant/components/plex/.translations/nl.json @@ -0,0 +1,62 @@ +{ + "config": { + "abort": { + "all_configured": "Alle gekoppelde servers zijn al geconfigureerd", + "already_configured": "Deze Plex-server is al geconfigureerd", + "already_in_progress": "Plex wordt geconfigureerd", + "discovery_no_file": "Geen legacy configuratiebestand gevonden", + "invalid_import": "Ge\u00efmporteerde configuratie is ongeldig", + "token_request_timeout": "Time-out verkrijgen van token", + "unknown": "Mislukt om onbekende reden" + }, + "error": { + "faulty_credentials": "Autorisatie mislukt", + "no_servers": "Geen servers gekoppeld aan account", + "no_token": "Geef een token op of selecteer handmatige installatie", + "not_found": "Plex-server niet gevonden" + }, + "step": { + "manual_setup": { + "data": { + "host": "Host", + "port": "Poort", + "ssl": "Gebruik SSL", + "token": "Token (indien nodig)", + "verify_ssl": "Controleer SSL-certificaat" + }, + "title": "Plex server" + }, + "select_server": { + "data": { + "server": "Server" + }, + "description": "Meerdere servers beschikbaar, selecteer er een:", + "title": "Selecteer Plex server" + }, + "start_website_auth": { + "description": "Ga verder met autoriseren bij plex.tv.", + "title": "Verbind de Plex server" + }, + "user": { + "data": { + "manual_setup": "Handmatig setup", + "token": "Plex token" + }, + "description": "Ga verder met autoriseren bij plex.tv of configureer een server.", + "title": "Verbind de Plex server" + } + }, + "title": "Plex" + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "Toon alle bedieningselementen", + "use_episode_art": "Gebruik aflevering kunst" + }, + "description": "Opties voor Plex-mediaspelers" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/no.json b/homeassistant/components/plex/.translations/no.json index 18c4e865a84..8ebd2b69bb9 100644 --- a/homeassistant/components/plex/.translations/no.json +++ b/homeassistant/components/plex/.translations/no.json @@ -24,7 +24,7 @@ "token": "Token (hvis n\u00f8dvendig)", "verify_ssl": "Verifisere SSL-sertifikat" }, - "title": "Plex server" + "title": "Plex-server" }, "select_server": { "data": { @@ -35,7 +35,7 @@ }, "start_website_auth": { "description": "Fortsett \u00e5 autorisere p\u00e5 plex.tv.", - "title": "Koble til Plex server" + "title": "Koble til Plex-server" }, "user": { "data": { diff --git a/homeassistant/components/plex/.translations/pl.json b/homeassistant/components/plex/.translations/pl.json index 9b75a0061e8..0b94e3eacb6 100644 --- a/homeassistant/components/plex/.translations/pl.json +++ b/homeassistant/components/plex/.translations/pl.json @@ -4,6 +4,7 @@ "all_configured": "Wszystkie znalezione serwery s\u0105 ju\u017c skonfigurowane.", "already_configured": "Serwer Plex jest ju\u017c skonfigurowany", "already_in_progress": "Plex jest konfigurowany", + "discovery_no_file": "Nie znaleziono pliku konfiguracyjnego", "invalid_import": "Zaimportowana konfiguracja jest nieprawid\u0142owa", "token_request_timeout": "Przekroczono limit czasu na uzyskanie tokena", "unknown": "Nieznany b\u0142\u0105d" @@ -32,6 +33,10 @@ "description": "Dost\u0119pnych jest wiele serwer\u00f3w, wybierz jeden:", "title": "Wybierz serwer Plex" }, + "start_website_auth": { + "description": "Kontynuuj, by dokona\u0107 autoryzacji w plex.tv.", + "title": "Po\u0142\u0105cz z serwerem Plex" + }, "user": { "data": { "manual_setup": "Konfiguracja r\u0119czna", @@ -48,7 +53,7 @@ "plex_mp_settings": { "data": { "show_all_controls": "Poka\u017c wszystkie elementy steruj\u0105ce", - "use_episode_art": "U\u017cyj grafiki episodu" + "use_episode_art": "U\u017cyj grafiki odcinka" }, "description": "Opcje dla odtwarzaczy multimedialnych Plex" } diff --git a/homeassistant/components/plex/.translations/pt.json b/homeassistant/components/plex/.translations/pt.json new file mode 100644 index 00000000000..4312910653f --- /dev/null +++ b/homeassistant/components/plex/.translations/pt.json @@ -0,0 +1,21 @@ +{ + "config": { + "step": { + "manual_setup": { + "data": { + "host": "Servidor", + "port": "Porta" + } + } + } + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "Mostrar todos os controles" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/ru.json b/homeassistant/components/plex/.translations/ru.json index fe773f72be9..bce55d35baa 100644 --- a/homeassistant/components/plex/.translations/ru.json +++ b/homeassistant/components/plex/.translations/ru.json @@ -5,15 +5,15 @@ "already_configured": "\u042d\u0442\u043e\u0442 \u0441\u0435\u0440\u0432\u0435\u0440 Plex \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d.", "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", - "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" + "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.", + "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_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" + "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_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." }, "step": { "manual_setup": { @@ -34,7 +34,7 @@ "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u0435\u0440\u0432\u0435\u0440 Plex" }, "start_website_auth": { - "description": "\u041f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u044c \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044e \u043d\u0430 plex.tv.", + "description": "\u041f\u0440\u043e\u0439\u0434\u0438\u0442\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044e \u043d\u0430 plex.tv.", "title": "Plex" }, "user": { diff --git a/homeassistant/components/plex/.translations/sl.json b/homeassistant/components/plex/.translations/sl.json index 9be270a017c..7426e7f95ed 100644 --- a/homeassistant/components/plex/.translations/sl.json +++ b/homeassistant/components/plex/.translations/sl.json @@ -42,7 +42,7 @@ "manual_setup": "Ro\u010dna nastavitev", "token": "Plex \u017eeton" }, - "description": "Vnesite \u017eeton Plex za samodejno nastavitev ali ro\u010dno konfigurirajte stre\u017enik.", + "description": "Nadaljujte z avtorizacijo na plex.tv ali ro\u010dno konfigurirajte stre\u017enik.", "title": "Pove\u017eite stre\u017enik Plex" } }, diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index ed94b6913bc..1aaa8a8e3aa 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -3,6 +3,7 @@ import asyncio import logging import plexapi.exceptions +from plexwebsocket import PlexWebsocket import requests.exceptions import voluptuous as vol @@ -15,8 +16,14 @@ from homeassistant.const import ( CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL, + EVENT_HOMEASSISTANT_STOP, ) from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from .const import ( CONF_USE_EPISODE_ART, @@ -26,12 +33,14 @@ from .const import ( DEFAULT_PORT, DEFAULT_SSL, DEFAULT_VERIFY_SSL, + DISPATCHERS, DOMAIN as PLEX_DOMAIN, PLATFORMS, PLEX_MEDIA_PLAYER_OPTIONS, PLEX_SERVER_CONFIG, - REFRESH_LISTENERS, + PLEX_UPDATE_PLATFORMS_SIGNAL, SERVERS, + WEBSOCKETS, ) from .server import PlexServer @@ -64,7 +73,7 @@ _LOGGER = logging.getLogger(__package__) def setup(hass, config): """Set up the Plex component.""" - hass.data.setdefault(PLEX_DOMAIN, {SERVERS: {}, REFRESH_LISTENERS: {}}) + hass.data.setdefault(PLEX_DOMAIN, {SERVERS: {}, DISPATCHERS: {}, WEBSOCKETS: {}}) plex_config = config.get(PLEX_DOMAIN, {}) if plex_config: @@ -104,7 +113,7 @@ async def async_setup_entry(hass, entry): ) hass.config_entries.async_update_entry(entry, options=options) - plex_server = PlexServer(server_config, entry.options) + plex_server = PlexServer(hass, server_config, entry.options) try: await hass.async_add_executor_job(plex_server.connect) except requests.exceptions.ConnectionError as error: @@ -129,7 +138,8 @@ async def async_setup_entry(hass, entry): _LOGGER.debug( "Connected to: %s (%s)", plex_server.friendly_name, plex_server.url_in_use ) - hass.data[PLEX_DOMAIN][SERVERS][plex_server.machine_identifier] = plex_server + server_id = plex_server.machine_identifier + hass.data[PLEX_DOMAIN][SERVERS][server_id] = plex_server for platform in PLATFORMS: hass.async_create_task( @@ -138,6 +148,30 @@ async def async_setup_entry(hass, entry): entry.add_update_listener(async_options_updated) + unsub = async_dispatcher_connect( + hass, + PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id), + plex_server.update_platforms, + ) + hass.data[PLEX_DOMAIN][DISPATCHERS].setdefault(server_id, []) + hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub) + + def update_plex(): + async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) + + session = async_get_clientsession(hass) + websocket = PlexWebsocket(plex_server.plex_server, update_plex, session) + hass.loop.create_task(websocket.listen()) + hass.data[PLEX_DOMAIN][WEBSOCKETS][server_id] = websocket + + def close_websocket_session(_): + websocket.close() + + unsub = hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, close_websocket_session + ) + hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub) + return True @@ -145,8 +179,12 @@ async def async_unload_entry(hass, entry): """Unload a config entry.""" server_id = entry.data[CONF_SERVER_IDENTIFIER] - cancel = hass.data[PLEX_DOMAIN][REFRESH_LISTENERS].pop(server_id) - await hass.async_add_executor_job(cancel) + websocket = hass.data[PLEX_DOMAIN][WEBSOCKETS].pop(server_id) + websocket.close() + + dispatchers = hass.data[PLEX_DOMAIN][DISPATCHERS].pop(server_id) + for unsub in dispatchers: + unsub() tasks = [ hass.config_entries.async_forward_entry_unload(entry, platform) diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py index 9e74756977d..c03b958b2da 100644 --- a/homeassistant/components/plex/config_flow.py +++ b/homeassistant/components/plex/config_flow.py @@ -19,6 +19,7 @@ from homeassistant.util.json import load_json from .const import ( # pylint: disable=unused-import AUTH_CALLBACK_NAME, AUTH_CALLBACK_PATH, + CONF_CLIENT_IDENTIFIER, CONF_SERVER, CONF_SERVER_IDENTIFIER, CONF_USE_EPISODE_ART, @@ -65,6 +66,7 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.available_servers = None self.plexauth = None self.token = None + self.client_id = None async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" @@ -79,7 +81,7 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors = {} self.current_login = server_config - plex_server = PlexServer(server_config) + plex_server = PlexServer(self.hass, server_config) try: await self.hass.async_add_executor_job(plex_server.connect) @@ -116,6 +118,8 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): token = server_config.get(CONF_TOKEN) entry_config = {CONF_URL: url} + if self.client_id: + entry_config[CONF_CLIENT_IDENTIFIER] = self.client_id if token: entry_config[CONF_TOKEN] = token if url.startswith("https"): @@ -216,6 +220,7 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_external_step_done(next_step_id="timed_out") self.token = token + self.client_id = self.plexauth.client_identifier return self.async_external_step_done(next_step_id="use_external_token") async def async_step_timed_out(self, user_input=None): diff --git a/homeassistant/components/plex/const.py b/homeassistant/components/plex/const.py index 0b436c4e208..d3c79e60bc4 100644 --- a/homeassistant/components/plex/const.py +++ b/homeassistant/components/plex/const.py @@ -2,20 +2,27 @@ from homeassistant.const import __version__ DOMAIN = "plex" -NAME_FORMAT = "Plex {}" +NAME_FORMAT = "Plex ({})" DEFAULT_PORT = 32400 DEFAULT_SSL = False DEFAULT_VERIFY_SSL = True +DISPATCHERS = "dispatchers" PLATFORMS = ["media_player", "sensor"] -REFRESH_LISTENERS = "refresh_listeners" SERVERS = "servers" +WEBSOCKETS = "websockets" PLEX_CONFIG_FILE = "plex.conf" PLEX_MEDIA_PLAYER_OPTIONS = "plex_mp_options" PLEX_SERVER_CONFIG = "server_config" +PLEX_NEW_MP_SIGNAL = "plex_new_mp_signal.{}" +PLEX_UPDATE_MEDIA_PLAYER_SIGNAL = "plex_update_mp_signal.{}" +PLEX_UPDATE_PLATFORMS_SIGNAL = "plex_update_platforms_signal.{}" +PLEX_UPDATE_SENSOR_SIGNAL = "plex_update_sensor_signal.{}" + +CONF_CLIENT_IDENTIFIER = "client_id" CONF_SERVER = "server" CONF_SERVER_IDENTIFIER = "server_id" CONF_USE_EPISODE_ART = "use_episode_art" diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index d4f2ae0517a..8edccda75e0 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -5,7 +5,8 @@ "documentation": "https://www.home-assistant.io/integrations/plex", "requirements": [ "plexapi==3.0.6", - "plexauth==0.0.4" + "plexauth==0.0.5", + "plexwebsocket==0.0.3" ], "dependencies": [ "http" diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index a49e4c9c057..32bf7b65fff 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -1,5 +1,4 @@ """Support to interface with the Plex API.""" -from datetime import timedelta import json import logging from xml.etree.ElementTree import ParseError @@ -29,14 +28,17 @@ from homeassistant.const import ( STATE_PAUSED, STATE_PLAYING, ) -from homeassistant.helpers.event import track_time_interval +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util import dt as dt_util from .const import ( CONF_SERVER_IDENTIFIER, + DISPATCHERS, DOMAIN as PLEX_DOMAIN, NAME_FORMAT, - REFRESH_LISTENERS, + PLEX_NEW_MP_SIGNAL, + PLEX_UPDATE_MEDIA_PLAYER_SIGNAL, SERVERS, ) @@ -53,142 +55,55 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Plex media_player from a config entry.""" - - def add_entities(entities, update_before_add=False): - """Sync version of async add entities.""" - hass.add_job(async_add_entities, entities, update_before_add) - - hass.async_add_executor_job(_setup_platform, hass, config_entry, add_entities) - - -def _setup_platform(hass, config_entry, add_entities_callback): - """Set up the Plex media_player platform.""" server_id = config_entry.data[CONF_SERVER_IDENTIFIER] - plexserver = hass.data[PLEX_DOMAIN][SERVERS][server_id] - plex_clients = {} - plex_sessions = {} - hass.data[PLEX_DOMAIN][REFRESH_LISTENERS][server_id] = track_time_interval( - hass, lambda now: update_devices(), timedelta(seconds=10) + + def async_new_media_players(new_entities): + _async_add_entities( + hass, config_entry, async_add_entities, server_id, new_entities + ) + + unsub = async_dispatcher_connect( + hass, PLEX_NEW_MP_SIGNAL.format(server_id), async_new_media_players ) - - def update_devices(): - """Update the devices objects.""" - try: - devices = plexserver.clients() - except plexapi.exceptions.BadRequest: - _LOGGER.exception("Error listing plex devices") - return - except requests.exceptions.RequestException as ex: - _LOGGER.warning( - "Could not connect to Plex server: %s (%s)", - plexserver.friendly_name, - ex, - ) - return - - new_plex_clients = [] - available_client_ids = [] - for device in devices: - # For now, let's allow all deviceClass types - if device.deviceClass in ["badClient"]: - continue - - available_client_ids.append(device.machineIdentifier) - - if device.machineIdentifier not in plex_clients: - new_client = PlexClient( - plexserver, device, None, plex_sessions, update_devices - ) - plex_clients[device.machineIdentifier] = new_client - _LOGGER.debug("New device: %s", device.machineIdentifier) - new_plex_clients.append(new_client) - else: - _LOGGER.debug("Refreshing device: %s", device.machineIdentifier) - plex_clients[device.machineIdentifier].refresh(device, None) - - # add devices with a session and no client (ex. PlexConnect Apple TV's) - try: - sessions = plexserver.sessions() - except plexapi.exceptions.BadRequest: - _LOGGER.exception("Error listing plex sessions") - return - except requests.exceptions.RequestException as ex: - _LOGGER.warning( - "Could not connect to Plex server: %s (%s)", - plexserver.friendly_name, - ex, - ) - return - - plex_sessions.clear() - for session in sessions: - for player in session.players: - plex_sessions[player.machineIdentifier] = session, player - - for machine_identifier, (session, player) in plex_sessions.items(): - if machine_identifier in available_client_ids: - # Avoid using session if already added as a device. - _LOGGER.debug("Skipping session, device exists: %s", machine_identifier) - continue - - if ( - machine_identifier not in plex_clients - and machine_identifier is not None - ): - new_client = PlexClient( - plexserver, player, session, plex_sessions, update_devices - ) - plex_clients[machine_identifier] = new_client - _LOGGER.debug("New session: %s", machine_identifier) - new_plex_clients.append(new_client) - else: - _LOGGER.debug("Refreshing session: %s", machine_identifier) - plex_clients[machine_identifier].refresh(None, session) - - for client in plex_clients.values(): - # force devices to idle that do not have a valid session - if client.session is None: - client.force_idle() - - client.set_availability( - client.machine_identifier in available_client_ids - or client.machine_identifier in plex_sessions - ) - - if client not in new_plex_clients: - client.schedule_update_ha_state() - - if new_plex_clients: - add_entities_callback(new_plex_clients) + hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub) -class PlexClient(MediaPlayerDevice): +@callback +def _async_add_entities( + hass, config_entry, async_add_entities, server_id, new_entities +): + """Set up Plex media_player entities.""" + entities = [] + plexserver = hass.data[PLEX_DOMAIN][SERVERS][server_id] + for entity_params in new_entities: + plex_mp = PlexMediaPlayer(plexserver, **entity_params) + entities.append(plex_mp) + + async_add_entities(entities, True) + + +class PlexMediaPlayer(MediaPlayerDevice): """Representation of a Plex device.""" - def __init__(self, plex_server, device, session, plex_sessions, update_devices): + def __init__(self, plex_server, device, session=None): """Initialize the Plex device.""" + self.plex_server = plex_server + self.device = device + self.session = session self._app_name = "" - self._device = None self._available = False - self._marked_unavailable = None self._device_protocol_capabilities = None self._is_player_active = False - self._is_player_available = False - self._player = None - self._machine_identifier = None + self._machine_identifier = device.machineIdentifier self._make = "" self._name = None self._player_state = "idle" self._previous_volume_level = 1 # Used in fake muting - self._session = None self._session_type = None self._session_username = None self._state = STATE_IDLE self._volume_level = 1 # since we can't retrieve remotely self._volume_muted = False # since we can't retrieve remotely - self.plex_server = plex_server - self.plex_sessions = plex_sessions - self.update_devices = update_devices # General self._media_content_id = None self._media_content_rating = None @@ -208,7 +123,22 @@ class PlexClient(MediaPlayerDevice): self._media_season = None self._media_series_title = None - self.refresh(device, session) + async def async_added_to_hass(self): + """Run when about to be added to hass.""" + server_id = self.plex_server.machine_identifier + unsub = async_dispatcher_connect( + self.hass, + PLEX_UPDATE_MEDIA_PLAYER_SIGNAL.format(self.unique_id), + self.async_refresh_media_player, + ) + self.hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub) + + @callback + def async_refresh_media_player(self, device, session): + """Set instance objects and trigger an entity state update.""" + self.device = device + self.session = session + self.async_schedule_update_ha_state(True) def _clear_media_details(self): """Set all Media Items to None.""" @@ -232,52 +162,46 @@ class PlexClient(MediaPlayerDevice): # Clear library Name self._app_name = "" - def refresh(self, device, session): + def update(self): """Refresh key device data.""" self._clear_media_details() - if session: # Not being triggered by Chrome or FireTablet Plex App - self._session = session - if device: - self._device = device + self._available = self.device or self.session + name_base = None + + if self.device: try: - device_url = self._device.url("/") + device_url = self.device.url("/") except plexapi.exceptions.BadRequest: device_url = "127.0.0.1" if "127.0.0.1" in device_url: - self._device.proxyThroughServer() - self._session = None - self._machine_identifier = self._device.machineIdentifier - self._name = NAME_FORMAT.format(self._device.title or DEVICE_DEFAULT_NAME) - self._device_protocol_capabilities = self._device.protocolCapabilities + self.device.proxyThroughServer() + name_base = self.device.title or self.device.product + self._device_protocol_capabilities = self.device.protocolCapabilities + self._player_state = self.device.state - # set valid session, preferring device session - if self._device.machineIdentifier in self.plex_sessions: - self._session = self.plex_sessions.get( - self._device.machineIdentifier, [None, None] - )[0] - - if self._session: - if ( - self._device is not None - and self._device.machineIdentifier is not None - and self._session.players - ): - self._is_player_available = True - self._player = [ + if not self.session: + self.force_idle() + else: + session_device = next( + ( p - for p in self._session.players - if p.machineIdentifier == self._device.machineIdentifier - ][0] - self._name = NAME_FORMAT.format(self._player.title) - self._player_state = self._player.state - self._session_username = self._session.usernames[0] - self._make = self._player.device + for p in self.session.players + if p.machineIdentifier == self.device.machineIdentifier + ), + None, + ) + if session_device: + self._make = session_device.device or "" + self._player_state = session_device.state + name_base = name_base or session_device.title or session_device.product else: - self._is_player_available = False + _LOGGER.warning("No player associated with active session") + + self._session_username = self.session.usernames[0] # Calculate throttled position for proper progress display. - position = int(self._session.viewOffset / 1000) + position = int(self.session.viewOffset / 1000) now = dt_util.utcnow() if self._media_position is not None: pos_diff = position - self._media_position @@ -289,21 +213,22 @@ class PlexClient(MediaPlayerDevice): self._media_position_updated_at = now self._media_position = position - self._media_content_id = self._session.ratingKey - self._media_content_rating = getattr(self._session, "contentRating", None) + self._media_content_id = self.session.ratingKey + self._media_content_rating = getattr(self.session, "contentRating", None) + self._name = self._name or NAME_FORMAT.format(name_base or DEVICE_DEFAULT_NAME) self._set_player_state() - if self._is_player_active and self._session is not None: - self._session_type = self._session.type - self._media_duration = int(self._session.duration / 1000) + if self._is_player_active and self.session is not None: + self._session_type = self.session.type + self._media_duration = int(self.session.duration / 1000) # title (movie name, tv episode name, music song name) - self._media_title = self._session.title + self._media_title = self.session.title # media type self._set_media_type() self._app_name = ( - self._session.section().title - if self._session.section() is not None + self.session.section().title + if self.session.section() is not None else "" ) self._set_media_image() @@ -311,33 +236,21 @@ class PlexClient(MediaPlayerDevice): self._session_type = None def _set_media_image(self): - thumb_url = self._session.thumbUrl + thumb_url = self.session.thumbUrl if ( self.media_content_type is MEDIA_TYPE_TVSHOW and not self.plex_server.use_episode_art ): - thumb_url = self._session.url(self._session.grandparentThumb) + thumb_url = self.session.url(self.session.grandparentThumb) if thumb_url is None: _LOGGER.debug( - "Using media art because media thumb " "was not found: %s", - self.entity_id, + "Using media art because media thumb was not found: %s", self.name ) - thumb_url = self.session.url(self._session.art) + thumb_url = self.session.url(self.session.art) self._media_image_url = thumb_url - def set_availability(self, available): - """Set the device as available/unavailable noting time.""" - if not available: - self._clear_media_details() - if self._marked_unavailable is None: - self._marked_unavailable = dt_util.utcnow() - else: - self._marked_unavailable = None - - self._available = available - def _set_player_state(self): if self._player_state == "playing": self._is_player_active = True @@ -357,41 +270,41 @@ class PlexClient(MediaPlayerDevice): self._media_content_type = MEDIA_TYPE_TVSHOW # season number (00) - if callable(self._session.season): - self._media_season = str((self._session.season()).index).zfill(2) - elif self._session.parentIndex is not None: - self._media_season = self._session.parentIndex.zfill(2) + if callable(self.session.season): + self._media_season = str((self.session.season()).index).zfill(2) + elif self.session.parentIndex is not None: + self._media_season = self.session.parentIndex.zfill(2) else: self._media_season = None # show name - self._media_series_title = self._session.grandparentTitle + self._media_series_title = self.session.grandparentTitle # episode number (00) - if self._session.index is not None: - self._media_episode = str(self._session.index).zfill(2) + if self.session.index is not None: + self._media_episode = str(self.session.index).zfill(2) elif self._session_type == "movie": self._media_content_type = MEDIA_TYPE_MOVIE - if self._session.year is not None and self._media_title is not None: - self._media_title += " (" + str(self._session.year) + ")" + if self.session.year is not None and self._media_title is not None: + self._media_title += " (" + str(self.session.year) + ")" elif self._session_type == "track": self._media_content_type = MEDIA_TYPE_MUSIC - self._media_album_name = self._session.parentTitle - self._media_album_artist = self._session.grandparentTitle - self._media_track = self._session.index - self._media_artist = self._session.originalTitle + self._media_album_name = self.session.parentTitle + self._media_album_artist = self.session.grandparentTitle + self._media_track = self.session.index + self._media_artist = self.session.originalTitle # use album artist if track artist is missing if self._media_artist is None: _LOGGER.debug( - "Using album artist because track artist " "was not found: %s", - self.entity_id, + "Using album artist because track artist was not found: %s", + self.name, ) self._media_artist = self._media_album_artist def force_idle(self): """Force client to idle.""" self._state = STATE_IDLE - self._session = None + self.session = None self._clear_media_details() @property @@ -402,7 +315,7 @@ class PlexClient(MediaPlayerDevice): @property def unique_id(self): """Return the id of this plex client.""" - return self.machine_identifier + return self._machine_identifier @property def available(self): @@ -414,31 +327,11 @@ class PlexClient(MediaPlayerDevice): """Return the name of the device.""" return self._name - @property - def machine_identifier(self): - """Return the machine identifier of the device.""" - return self._machine_identifier - @property def app_name(self): """Return the library name of playing media.""" return self._app_name - @property - def device(self): - """Return the device, if any.""" - return self._device - - @property - def marked_unavailable(self): - """Return time device was marked unavailable.""" - return self._marked_unavailable - - @property - def session(self): - """Return the session, if any.""" - return self._session - @property def state(self): """Return the state of the device.""" @@ -462,8 +355,7 @@ class PlexClient(MediaPlayerDevice): """Return the content type of current playing media.""" if self._session_type == "clip": _LOGGER.debug( - "Clip content type detected, " "compatibility may vary: %s", - self.entity_id, + "Clip content type detected, compatibility may vary: %s", self.name ) return MEDIA_TYPE_TVSHOW if self._session_type == "episode": @@ -560,8 +452,8 @@ class PlexClient(MediaPlayerDevice): # no mute support if self.make.lower() == "shield android tv": _LOGGER.debug( - "Shield Android TV client detected, disabling mute " "controls: %s", - self.entity_id, + "Shield Android TV client detected, disabling mute controls: %s", + self.name, ) return ( SUPPORT_PAUSE @@ -579,7 +471,7 @@ class PlexClient(MediaPlayerDevice): _LOGGER.debug( "Tivo client detected, only enabling pause, play, " "stop, and off controls: %s", - self.entity_id, + self.name, ) return SUPPORT_PAUSE | SUPPORT_PLAY | SUPPORT_STOP | SUPPORT_TURN_OFF @@ -603,7 +495,7 @@ class PlexClient(MediaPlayerDevice): if self.device and "playback" in self._device_protocol_capabilities: self.device.setVolume(int(volume * 100), self._active_media_plexapi_type) self._volume_level = volume # store since we can't retrieve - self.update_devices() + self.plex_server.update_platforms() @property def volume_level(self): @@ -642,19 +534,19 @@ class PlexClient(MediaPlayerDevice): """Send play command.""" if self.device and "playback" in self._device_protocol_capabilities: self.device.play(self._active_media_plexapi_type) - self.update_devices() + self.plex_server.update_platforms() def media_pause(self): """Send pause command.""" if self.device and "playback" in self._device_protocol_capabilities: self.device.pause(self._active_media_plexapi_type) - self.update_devices() + self.plex_server.update_platforms() def media_stop(self): """Send stop command.""" if self.device and "playback" in self._device_protocol_capabilities: self.device.stop(self._active_media_plexapi_type) - self.update_devices() + self.plex_server.update_platforms() def turn_off(self): """Turn the client off.""" @@ -665,13 +557,13 @@ class PlexClient(MediaPlayerDevice): """Send next track command.""" if self.device and "playback" in self._device_protocol_capabilities: self.device.skipNext(self._active_media_plexapi_type) - self.update_devices() + self.plex_server.update_platforms() def media_previous_track(self): """Send previous track command.""" if self.device and "playback" in self._device_protocol_capabilities: self.device.skipPrevious(self._active_media_plexapi_type) - self.update_devices() + self.plex_server.update_platforms() def play_media(self, media_type, media_id, **kwargs): """Play a piece of media.""" @@ -706,7 +598,7 @@ class PlexClient(MediaPlayerDevice): except requests.exceptions.ConnectTimeout: _LOGGER.error("Timed out playing on %s", self.name) - self.update_devices() + self.plex_server.update_platforms() def _get_music_media(self, library_name, src): """Find music media and return a Plex media object.""" diff --git a/homeassistant/components/plex/sensor.py b/homeassistant/components/plex/sensor.py index 7d5b54356a0..287f0edf39a 100644 --- a/homeassistant/components/plex/sensor.py +++ b/homeassistant/components/plex/sensor.py @@ -1,19 +1,21 @@ """Support for Plex media server monitoring.""" -from datetime import timedelta import logging -import plexapi.exceptions -import requests.exceptions - +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from homeassistant.util import Throttle -from .const import CONF_SERVER_IDENTIFIER, DOMAIN as PLEX_DOMAIN, SERVERS +from .const import ( + CONF_SERVER_IDENTIFIER, + DISPATCHERS, + DOMAIN as PLEX_DOMAIN, + NAME_FORMAT, + PLEX_UPDATE_SENSOR_SIGNAL, + SERVERS, +) _LOGGER = logging.getLogger(__name__) -MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1) - async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Plex sensor platform. @@ -26,8 +28,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Plex sensor from a config entry.""" server_id = config_entry.data[CONF_SERVER_IDENTIFIER] - sensor = PlexSensor(hass.data[PLEX_DOMAIN][SERVERS][server_id]) - async_add_entities([sensor], True) + plexserver = hass.data[PLEX_DOMAIN][SERVERS][server_id] + sensor = PlexSensor(plexserver) + async_add_entities([sensor]) class PlexSensor(Entity): @@ -35,12 +38,29 @@ class PlexSensor(Entity): def __init__(self, plex_server): """Initialize the sensor.""" + self.sessions = [] self._state = None self._now_playing = [] self._server = plex_server - self._name = f"Plex ({plex_server.friendly_name})" + self._name = NAME_FORMAT.format(plex_server.friendly_name) self._unique_id = f"sensor-{plex_server.machine_identifier}" + async def async_added_to_hass(self): + """Run when about to be added to hass.""" + server_id = self._server.machine_identifier + unsub = async_dispatcher_connect( + self.hass, + PLEX_UPDATE_SENSOR_SIGNAL.format(server_id), + self.async_refresh_sensor, + ) + self.hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub) + + @callback + def async_refresh_sensor(self, sessions): + """Set instance object and trigger an entity state update.""" + self.sessions = sessions + self.async_schedule_update_ha_state(True) + @property def name(self): """Return the name of the sensor.""" @@ -51,6 +71,11 @@ class PlexSensor(Entity): """Return the id of this plex client.""" return self._unique_id + @property + def should_poll(self): + """Return True if entity has to be polled for state.""" + return False + @property def state(self): """Return the state of the sensor.""" @@ -66,24 +91,10 @@ class PlexSensor(Entity): """Return the state attributes.""" return {content[0]: content[1] for content in self._now_playing} - @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Update method for Plex sensor.""" - try: - sessions = self._server.sessions() - except plexapi.exceptions.BadRequest: - _LOGGER.error( - "Error listing current Plex sessions on %s", self._server.friendly_name - ) - return - except requests.exceptions.RequestException as ex: - _LOGGER.warning( - "Temporary error connecting to %s (%s)", self._server.friendly_name, ex - ) - return - now_playing = [] - for sess in sessions: + for sess in self.sessions: user = sess.usernames[0] device = sess.players[0].title now_playing_user = f"{user} - {device}" @@ -120,5 +131,5 @@ class PlexSensor(Entity): now_playing_title += f" ({sess.year})" now_playing.append((now_playing_user, now_playing_title)) - self._state = len(sessions) + self._state = len(self.sessions) self._now_playing = now_playing diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index d9ddc28c89a..e6f77a310f1 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -1,17 +1,25 @@ """Shared class to maintain Plex server instances.""" +import logging + import plexapi.myplex import plexapi.playqueue import plexapi.server from requests import Session +import requests.exceptions from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL +from homeassistant.helpers.dispatcher import dispatcher_send from .const import ( + CONF_CLIENT_IDENTIFIER, CONF_SERVER, CONF_SHOW_ALL_CONTROLS, CONF_USE_EPISODE_ART, DEFAULT_VERIFY_SSL, + PLEX_NEW_MP_SIGNAL, + PLEX_UPDATE_MEDIA_PLAYER_SIGNAL, + PLEX_UPDATE_SENSOR_SIGNAL, X_PLEX_DEVICE_NAME, X_PLEX_PLATFORM, X_PLEX_PRODUCT, @@ -19,21 +27,23 @@ from .const import ( ) from .errors import NoServersFound, ServerNotSpecified +_LOGGER = logging.getLogger(__name__) + # Set default headers sent by plexapi plexapi.X_PLEX_DEVICE_NAME = X_PLEX_DEVICE_NAME plexapi.X_PLEX_PLATFORM = X_PLEX_PLATFORM plexapi.X_PLEX_PRODUCT = X_PLEX_PRODUCT plexapi.X_PLEX_VERSION = X_PLEX_VERSION -plexapi.myplex.BASE_HEADERS = plexapi.reset_base_headers() -plexapi.server.BASE_HEADERS = plexapi.reset_base_headers() class PlexServer: """Manages a single Plex server connection.""" - def __init__(self, server_config, options=None): + def __init__(self, hass, server_config, options=None): """Initialize a Plex server instance.""" + self._hass = hass self._plex_server = None + self._known_clients = set() self._url = server_config.get(CONF_URL) self._token = server_config.get(CONF_TOKEN) self._server_name = server_config.get(CONF_SERVER) @@ -41,6 +51,12 @@ class PlexServer: self.options = options self.server_choice = None + # Header conditionally added as it is not available in config entry v1 + if CONF_CLIENT_IDENTIFIER in server_config: + plexapi.X_PLEX_IDENTIFIER = server_config[CONF_CLIENT_IDENTIFIER] + plexapi.myplex.BASE_HEADERS = plexapi.reset_base_headers() + plexapi.server.BASE_HEADERS = plexapi.reset_base_headers() + def connect(self): """Connect to a Plex server directly, obtaining direct URL if necessary.""" @@ -76,13 +92,84 @@ class PlexServer: else: _connect_with_token() - def clients(self): - """Pass through clients call to plexapi.""" - return self._plex_server.clients() + def refresh_entity(self, machine_identifier, device, session): + """Forward refresh dispatch to media_player.""" + dispatcher_send( + self._hass, + PLEX_UPDATE_MEDIA_PLAYER_SIGNAL.format(machine_identifier), + device, + session, + ) - def sessions(self): - """Pass through sessions call to plexapi.""" - return self._plex_server.sessions() + def update_platforms(self): + """Update the platform entities.""" + _LOGGER.debug("Updating devices") + + available_clients = {} + new_clients = set() + + try: + devices = self._plex_server.clients() + sessions = self._plex_server.sessions() + except plexapi.exceptions.BadRequest: + _LOGGER.exception("Error requesting Plex client data from server") + return + except requests.exceptions.RequestException as ex: + _LOGGER.warning( + "Could not connect to Plex server: %s (%s)", self.friendly_name, ex + ) + return + + for device in devices: + available_clients[device.machineIdentifier] = {"device": device} + + if device.machineIdentifier not in self._known_clients: + new_clients.add(device.machineIdentifier) + _LOGGER.debug("New device: %s", device.machineIdentifier) + + for session in sessions: + for player in session.players: + available_clients.setdefault( + player.machineIdentifier, {"device": player} + ) + available_clients[player.machineIdentifier]["session"] = session + + if player.machineIdentifier not in self._known_clients: + new_clients.add(player.machineIdentifier) + _LOGGER.debug("New session: %s", player.machineIdentifier) + + new_entity_configs = [] + for client_id, client_data in available_clients.items(): + if client_id in new_clients: + new_entity_configs.append(client_data) + else: + self.refresh_entity( + client_id, client_data["device"], client_data.get("session") + ) + + self._known_clients.update(new_clients) + + idle_clients = self._known_clients.difference(available_clients) + for client_id in idle_clients: + self.refresh_entity(client_id, None, None) + + if new_entity_configs: + dispatcher_send( + self._hass, + PLEX_NEW_MP_SIGNAL.format(self.machine_identifier), + new_entity_configs, + ) + + dispatcher_send( + self._hass, + PLEX_UPDATE_SENSOR_SIGNAL.format(self.machine_identifier), + sessions, + ) + + @property + def plex_server(self): + """Return the plexapi PlexServer instance.""" + return self._plex_server @property def friendly_name(self): diff --git a/homeassistant/components/pocketcasts/sensor.py b/homeassistant/components/pocketcasts/sensor.py index 815e2688009..05a8f96bda7 100644 --- a/homeassistant/components/pocketcasts/sensor.py +++ b/homeassistant/components/pocketcasts/sensor.py @@ -1,13 +1,13 @@ """Support for Pocket Casts.""" +from datetime import timedelta import logging -from datetime import timedelta - +import pocketcasts 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 +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__) @@ -25,8 +25,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the pocketcasts platform for sensors.""" - import pocketcasts - username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) diff --git a/homeassistant/components/proliphix/climate.py b/homeassistant/components/proliphix/climate.py index 666f8c28241..44e31e24fb0 100644 --- a/homeassistant/components/proliphix/climate.py +++ b/homeassistant/components/proliphix/climate.py @@ -1,7 +1,8 @@ """Support for Proliphix NT10e Thermostats.""" +import proliphix 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 ( HVAC_MODE_COOL, HVAC_MODE_HEAT, @@ -9,12 +10,12 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ( + ATTR_TEMPERATURE, CONF_HOST, CONF_PASSWORD, CONF_USERNAME, PRECISION_TENTHS, TEMP_FAHRENHEIT, - ATTR_TEMPERATURE, ) import homeassistant.helpers.config_validation as cv @@ -35,8 +36,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): password = config.get(CONF_PASSWORD) host = config.get(CONF_HOST) - import proliphix - pdp = proliphix.PDP(host, username, password) add_entities([ProliphixThermostat(pdp)], True) diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py index 82db5f6725f..8eeb9325bc0 100644 --- a/homeassistant/components/prometheus/__init__.py +++ b/homeassistant/components/prometheus/__init__.py @@ -3,24 +3,25 @@ import logging import string from aiohttp import web +import prometheus_client import voluptuous as vol from homeassistant import core as hacore from homeassistant.components.climate.const import ATTR_CURRENT_TEMPERATURE from homeassistant.components.http import HomeAssistantView from homeassistant.const import ( + ATTR_DEVICE_CLASS, ATTR_TEMPERATURE, ATTR_UNIT_OF_MEASUREMENT, - ATTR_DEVICE_CLASS, CONTENT_TYPE_TEXT_PLAIN, EVENT_STATE_CHANGED, - TEMP_FAHRENHEIT, TEMP_CELSIUS, + TEMP_FAHRENHEIT, ) from homeassistant.helpers import entityfilter, state as state_helper import homeassistant.helpers.config_validation as cv -from homeassistant.util.temperature import fahrenheit_to_celsius from homeassistant.helpers.entity_values import EntityValues +from homeassistant.util.temperature import fahrenheit_to_celsius _LOGGER = logging.getLogger(__name__) @@ -64,8 +65,6 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): """Activate Prometheus component.""" - import prometheus_client - hass.http.register_view(PrometheusView(prometheus_client)) conf = config[DOMAIN] @@ -99,7 +98,7 @@ class PrometheusMetrics: def __init__( self, - prometheus_client, + prometheus_cli, entity_filter, namespace, climate_units, @@ -108,7 +107,7 @@ class PrometheusMetrics: default_metric, ): """Initialize Prometheus Metrics.""" - self.prometheus_client = prometheus_client + self.prometheus_cli = prometheus_cli self._component_config = component_config self._override_metric = override_metric self._default_metric = default_metric @@ -147,9 +146,7 @@ class PrometheusMetrics: getattr(self, handler)(state) metric = self._metric( - "state_change", - self.prometheus_client.Counter, - "The number of state changes", + "state_change", self.prometheus_cli.Counter, "The number of state changes" ) metric.labels(**self._labels(state)).inc() @@ -199,7 +196,7 @@ class PrometheusMetrics: if "battery_level" in state.attributes: metric = self._metric( "battery_level_percent", - self.prometheus_client.Gauge, + self.prometheus_cli.Gauge, "Battery level as a percentage of its capacity", ) try: @@ -211,7 +208,7 @@ class PrometheusMetrics: def _handle_binary_sensor(self, state): metric = self._metric( "binary_sensor_state", - self.prometheus_client.Gauge, + self.prometheus_cli.Gauge, "State of the binary sensor (0/1)", ) value = self.state_as_number(state) @@ -220,7 +217,7 @@ class PrometheusMetrics: def _handle_input_boolean(self, state): metric = self._metric( "input_boolean_state", - self.prometheus_client.Gauge, + self.prometheus_cli.Gauge, "State of the input boolean (0/1)", ) value = self.state_as_number(state) @@ -229,7 +226,7 @@ class PrometheusMetrics: def _handle_device_tracker(self, state): metric = self._metric( "device_tracker_state", - self.prometheus_client.Gauge, + self.prometheus_cli.Gauge, "State of the device tracker (0/1)", ) value = self.state_as_number(state) @@ -237,14 +234,14 @@ class PrometheusMetrics: def _handle_person(self, state): metric = self._metric( - "person_state", self.prometheus_client.Gauge, "State of the person (0/1)" + "person_state", self.prometheus_cli.Gauge, "State of the person (0/1)" ) value = self.state_as_number(state) metric.labels(**self._labels(state)).set(value) def _handle_light(self, state): metric = self._metric( - "light_state", self.prometheus_client.Gauge, "Load level of a light (0..1)" + "light_state", self.prometheus_cli.Gauge, "Load level of a light (0..1)" ) try: @@ -259,7 +256,7 @@ class PrometheusMetrics: def _handle_lock(self, state): metric = self._metric( - "lock_state", self.prometheus_client.Gauge, "State of the lock (0/1)" + "lock_state", self.prometheus_cli.Gauge, "State of the lock (0/1)" ) value = self.state_as_number(state) metric.labels(**self._labels(state)).set(value) @@ -271,7 +268,7 @@ class PrometheusMetrics: temp = fahrenheit_to_celsius(temp) metric = self._metric( "temperature_c", - self.prometheus_client.Gauge, + self.prometheus_cli.Gauge, "Temperature in degrees Celsius", ) metric.labels(**self._labels(state)).set(temp) @@ -282,15 +279,13 @@ class PrometheusMetrics: current_temp = fahrenheit_to_celsius(current_temp) metric = self._metric( "current_temperature_c", - self.prometheus_client.Gauge, + self.prometheus_cli.Gauge, "Current Temperature in degrees Celsius", ) metric.labels(**self._labels(state)).set(current_temp) metric = self._metric( - "climate_state", - self.prometheus_client.Gauge, - "State of the thermostat (0/1)", + "climate_state", self.prometheus_cli.Gauge, "State of the thermostat (0/1)" ) try: value = self.state_as_number(state) @@ -308,7 +303,7 @@ class PrometheusMetrics: if metric is not None: _metric = self._metric( - metric, self.prometheus_client.Gauge, f"Sensor data measured in {unit}" + metric, self.prometheus_cli.Gauge, f"Sensor data measured in {unit}" ) try: @@ -368,7 +363,7 @@ class PrometheusMetrics: def _handle_switch(self, state): metric = self._metric( - "switch_state", self.prometheus_client.Gauge, "State of the switch (0/1)" + "switch_state", self.prometheus_cli.Gauge, "State of the switch (0/1)" ) try: @@ -383,7 +378,7 @@ class PrometheusMetrics: def _handle_automation(self, state): metric = self._metric( "automation_triggered_count", - self.prometheus_client.Counter, + self.prometheus_cli.Counter, "Count of times an automation has been triggered", ) @@ -396,15 +391,15 @@ class PrometheusView(HomeAssistantView): url = API_ENDPOINT name = "api:prometheus" - def __init__(self, prometheus_client): + def __init__(self, prometheus_cli): """Initialize Prometheus view.""" - self.prometheus_client = prometheus_client + self.prometheus_cli = prometheus_cli async def get(self, request): """Handle request for Prometheus metrics.""" _LOGGER.debug("Received Prometheus metrics request") return web.Response( - body=self.prometheus_client.generate_latest(), + body=self.prometheus_cli.generate_latest(), content_type=CONTENT_TYPE_TEXT_PLAIN, ) diff --git a/homeassistant/components/proxy/camera.py b/homeassistant/components/proxy/camera.py index b1ce8ad7ac0..90487120ffe 100644 --- a/homeassistant/components/proxy/camera.py +++ b/homeassistant/components/proxy/camera.py @@ -1,12 +1,14 @@ """Proxy camera platform that enables image processing of camera data.""" import asyncio +from datetime import timedelta +import io import logging -from datetime import timedelta +from PIL import Image import voluptuous as vol from homeassistant.components.camera import PLATFORM_SCHEMA, Camera -from homeassistant.const import CONF_ENTITY_ID, CONF_NAME, CONF_MODE +from homeassistant.const import CONF_ENTITY_ID, CONF_MODE, CONF_NAME from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv import homeassistant.util.dt as dt_util @@ -58,9 +60,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= def _precheck_image(image, opts): """Perform some pre-checks on the given image.""" - from PIL import Image - import io - if not opts: raise ValueError() try: @@ -77,9 +76,6 @@ def _precheck_image(image, opts): def _resize_image(image, opts): """Resize image.""" - from PIL import Image - import io - try: img = _precheck_image(image, opts) except ValueError: @@ -125,8 +121,6 @@ def _resize_image(image, opts): def _crop_image(image, opts): """Crop image.""" - import io - try: img = _precheck_image(image, opts) except ValueError: diff --git a/homeassistant/components/proxy/manifest.json b/homeassistant/components/proxy/manifest.json index c67fd4afc09..e3f62514801 100644 --- a/homeassistant/components/proxy/manifest.json +++ b/homeassistant/components/proxy/manifest.json @@ -3,7 +3,7 @@ "name": "Proxy", "documentation": "https://www.home-assistant.io/integrations/proxy", "requirements": [ - "pillow==6.1.0" + "pillow==6.2.0" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/ps4/__init__.py b/homeassistant/components/ps4/__init__.py index 60635bba525..205059be608 100644 --- a/homeassistant/components/ps4/__init__.py +++ b/homeassistant/components/ps4/__init__.py @@ -3,8 +3,8 @@ import logging import os import voluptuous as vol -from pyps4_homeassistant.ddp import async_create_ddp_endpoint -from pyps4_homeassistant.media_art import COUNTRIES +from pyps4_2ndscreen.ddp import async_create_ddp_endpoint +from pyps4_2ndscreen.media_art import COUNTRIES from homeassistant.components.media_player.const import ( ATTR_MEDIA_CONTENT_TYPE, @@ -172,12 +172,8 @@ def load_games(hass: HomeAssistantType) -> dict: _LOGGER.error("Games file was not parsed correctly") games = {} - # If file does not exist, create empty file. - if not os.path.isfile(g_file): - _LOGGER.info("Creating PS4 Games File") - games = {} - save_games(hass, games) - else: + # If file exists + if os.path.isfile(g_file): games = _reformat_data(hass, games) return games diff --git a/homeassistant/components/ps4/config_flow.py b/homeassistant/components/ps4/config_flow.py index a4b74077793..44523aea85a 100644 --- a/homeassistant/components/ps4/config_flow.py +++ b/homeassistant/components/ps4/config_flow.py @@ -2,6 +2,9 @@ from collections import OrderedDict import logging +from pyps4_2ndscreen.errors import CredentialTimeout +from pyps4_2ndscreen.helpers import Helper +from pyps4_2ndscreen.media_art import COUNTRIES import voluptuous as vol from homeassistant import config_entries @@ -37,8 +40,6 @@ class PlayStation4FlowHandler(config_entries.ConfigFlow): def __init__(self): """Initialize the config flow.""" - from pyps4_homeassistant import Helper - self.helper = Helper() self.creds = None self.name = None @@ -61,8 +62,6 @@ class PlayStation4FlowHandler(config_entries.ConfigFlow): async def async_step_creds(self, user_input=None): """Return PS4 credentials from 2nd Screen App.""" - from pyps4_homeassistant.errors import CredentialTimeout - errors = {} if user_input is not None: try: @@ -103,8 +102,6 @@ class PlayStation4FlowHandler(config_entries.ConfigFlow): async def async_step_link(self, user_input=None): """Prompt user input. Create or edit entry.""" - from pyps4_homeassistant.media_art import COUNTRIES - regions = sorted(COUNTRIES.keys()) default_region = None errors = {} diff --git a/homeassistant/components/ps4/manifest.json b/homeassistant/components/ps4/manifest.json index 98a14d877e8..361711c400c 100644 --- a/homeassistant/components/ps4/manifest.json +++ b/homeassistant/components/ps4/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ps4", "requirements": [ - "pyps4-homeassistant==0.8.7" + "pyps4-2ndscreen==1.0.1" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/ps4/media_player.py b/homeassistant/components/ps4/media_player.py index e1ec32ddd1f..3e8b667cd13 100644 --- a/homeassistant/components/ps4/media_player.py +++ b/homeassistant/components/ps4/media_player.py @@ -2,8 +2,8 @@ import logging import asyncio -import pyps4_homeassistant.ps4 as pyps4 -from pyps4_homeassistant.errors import NotReady +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 @@ -254,7 +254,7 @@ class PS4Device(MediaPlayerDevice): async def async_get_title_data(self, title_id, name): """Get PS Store Data.""" - from pyps4_homeassistant.errors import PSDataIncomplete + from pyps4_2ndscreen.errors import PSDataIncomplete app_name = None art = None diff --git a/homeassistant/components/ptvsd/__init__.py b/homeassistant/components/ptvsd/__init__.py index 869987bfe4b..55cef1405d9 100644 --- a/homeassistant/components/ptvsd/__init__.py +++ b/homeassistant/components/ptvsd/__init__.py @@ -4,9 +4,9 @@ Enable ptvsd debugger to attach to HA. Attach ptvsd debugger by default to port 5678. """ +from asyncio import Event import logging from threading import Thread -from asyncio import Event import voluptuous as vol @@ -36,7 +36,11 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistantType, config: ConfigType): """Set up ptvsd debugger.""" - import ptvsd + + # This is a local import, since importing this at the top, will cause + # ptvsd to hook into `sys.settrace`. So does `coverage` to generate + # coverage, resulting in a battle and incomplete code test coverage. + import ptvsd # pylint: disable=import-outside-toplevel conf = config[DOMAIN] host = conf[CONF_HOST] diff --git a/homeassistant/components/pushbullet/notify.py b/homeassistant/components/pushbullet/notify.py index 70738965340..76c1e14e5a5 100644 --- a/homeassistant/components/pushbullet/notify.py +++ b/homeassistant/components/pushbullet/notify.py @@ -2,6 +2,9 @@ import logging import mimetypes +from pushbullet import PushBullet +from pushbullet import InvalidKeyError +from pushbullet import PushError import voluptuous as vol from homeassistant.const import CONF_API_KEY @@ -28,8 +31,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_API_KEY): cv.string} def get_service(hass, config, discovery_info=None): """Get the Pushbullet notification service.""" - from pushbullet import PushBullet - from pushbullet import InvalidKeyError try: pushbullet = PushBullet(config[CONF_API_KEY]) @@ -124,7 +125,6 @@ class PushBulletNotificationService(BaseNotificationService): def _push_data(self, message, title, data, pusher, email=None): """Create the message content.""" - from pushbullet import PushError if data is None: data = {} diff --git a/homeassistant/components/pushbullet/sensor.py b/homeassistant/components/pushbullet/sensor.py index 3ed53fb01f6..600b38b6eaf 100644 --- a/homeassistant/components/pushbullet/sensor.py +++ b/homeassistant/components/pushbullet/sensor.py @@ -1,6 +1,10 @@ """Pushbullet platform for sensor component.""" import logging +import threading +from pushbullet import PushBullet +from pushbullet import InvalidKeyError +from pushbullet import Listener import voluptuous as vol from homeassistant.const import CONF_API_KEY, CONF_MONITORED_CONDITIONS @@ -35,8 +39,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Pushbullet Sensor platform.""" - from pushbullet import PushBullet - from pushbullet import InvalidKeyError try: pushbullet = PushBullet(config.get(CONF_API_KEY)) @@ -95,7 +97,6 @@ class PushBulletNotificationProvider: def __init__(self, pb): """Start to retrieve pushes from the given Pushbullet instance.""" - import threading self.pushbullet = pb self._data = None @@ -123,7 +124,6 @@ class PushBulletNotificationProvider: Spawn a new Listener and links it to self.on_push. """ - from pushbullet import Listener self.listener = Listener(account=self.pushbullet, on_push=self.on_push) _LOGGER.debug("Getting pushes") diff --git a/homeassistant/components/pushover/notify.py b/homeassistant/components/pushover/notify.py index 83da9a657fe..3f78897838d 100644 --- a/homeassistant/components/pushover/notify.py +++ b/homeassistant/components/pushover/notify.py @@ -1,7 +1,9 @@ """Pushover platform for notify component.""" import logging +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 @@ -28,8 +30,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def get_service(hass, config, discovery_info=None): """Get the Pushover notification service.""" - from pushover import InitError - try: return PushoverNotificationService( hass, config[CONF_USER_KEY], config[CONF_API_KEY] @@ -44,8 +44,6 @@ class PushoverNotificationService(BaseNotificationService): def __init__(self, hass, user_key, api_token): """Initialize the service.""" - from pushover import Client - self._hass = hass self._user_key = user_key self._api_token = api_token @@ -53,8 +51,6 @@ class PushoverNotificationService(BaseNotificationService): def send_message(self, message="", **kwargs): """Send a message to a user.""" - from pushover import RequestError - # Make a copy and use empty dict if necessary data = dict(kwargs.get(ATTR_DATA) or {}) @@ -65,8 +61,6 @@ class PushoverNotificationService(BaseNotificationService): # If attachment is a URL, use requests to open it as a stream. if data[ATTR_ATTACHMENT].startswith("http"): try: - import requests - response = requests.get( data[ATTR_ATTACHMENT], stream=True, timeout=5 ) diff --git a/homeassistant/components/qrcode/__init__.py b/homeassistant/components/qrcode/__init__.py index bcc1985a2dc..55b1a2a9d6b 100644 --- a/homeassistant/components/qrcode/__init__.py +++ b/homeassistant/components/qrcode/__init__.py @@ -1 +1 @@ -"""The qrcode component.""" +"""The QR code component.""" diff --git a/homeassistant/components/qrcode/image_processing.py b/homeassistant/components/qrcode/image_processing.py index 5e1b7c11b25..018f074a6d2 100644 --- a/homeassistant/components/qrcode/image_processing.py +++ b/homeassistant/components/qrcode/image_processing.py @@ -1,15 +1,20 @@ -"""Support for the QR image processing.""" -from homeassistant.core import split_entity_id +"""Support for the QR code image processing.""" +import io + +from PIL import Image +from pyzbar import pyzbar + from homeassistant.components.image_processing import ( - ImageProcessingEntity, - CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME, + CONF_SOURCE, + ImageProcessingEntity, ) +from homeassistant.core import split_entity_id def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the demo image processing platform.""" + """Set up the QR code image processing platform.""" # pylint: disable=unused-argument entities = [] for camera in config[CONF_SOURCE]: @@ -19,7 +24,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class QrEntity(ImageProcessingEntity): - """QR image processing entity.""" + """A QR image processing entity.""" def __init__(self, camera_entity, name): """Initialize QR image processing entity.""" @@ -49,10 +54,6 @@ class QrEntity(ImageProcessingEntity): def process_image(self, image): """Process image.""" - import io - from pyzbar import pyzbar - from PIL import Image - stream = io.BytesIO(image) img = Image.open(stream) diff --git a/homeassistant/components/qrcode/manifest.json b/homeassistant/components/qrcode/manifest.json index 87e16f62987..a3130070cc3 100644 --- a/homeassistant/components/qrcode/manifest.json +++ b/homeassistant/components/qrcode/manifest.json @@ -3,7 +3,7 @@ "name": "Qrcode", "documentation": "https://www.home-assistant.io/integrations/qrcode", "requirements": [ - "pillow==6.1.0", + "pillow==6.2.0", "pyzbar==0.1.7" ], "dependencies": [], diff --git a/homeassistant/components/rainmachine/.translations/ru.json b/homeassistant/components/rainmachine/.translations/ru.json index 6248890389d..afaa55424d2 100644 --- a/homeassistant/components/rainmachine/.translations/ru.json +++ b/homeassistant/components/rainmachine/.translations/ru.json @@ -2,12 +2,12 @@ "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\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435." }, "step": { "user": { "data": { - "ip_address": "\u0418\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441", + "ip_address": "\u0414\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "port": "\u041f\u043e\u0440\u0442" }, diff --git a/homeassistant/components/raspihats/__init__.py b/homeassistant/components/raspihats/__init__.py index 963c2624362..8b7ea0a38d7 100644 --- a/homeassistant/components/raspihats/__init__.py +++ b/homeassistant/components/raspihats/__init__.py @@ -120,7 +120,9 @@ class I2CHatsManager(threading.Thread): with self._lock: i2c_hat = self._i2c_hats.get(address) if i2c_hat is None: - # pylint: disable=import-error,no-name-in-module + # This is a Pi module and can't be installed in CI without + # breaking the build. + # pylint: disable=import-outside-toplevel,import-error import raspihats.i2c_hats as module constructor = getattr(module, board) @@ -138,7 +140,9 @@ class I2CHatsManager(threading.Thread): def run(self): """Keep alive for I2C-HATs.""" - # pylint: disable=import-error,no-name-in-module + # This is a Pi module and can't be installed in CI without + # breaking the build. + # pylint: disable=import-outside-toplevel,import-error from raspihats.i2c_hats import ResponseException _LOGGER.info(log_message(self, "starting")) @@ -199,7 +203,9 @@ class I2CHatsManager(threading.Thread): def read_di(self, address, channel): """Read a value from a I2C-HAT digital input.""" - # pylint: disable=import-error,no-name-in-module + # This is a Pi module and can't be installed in CI without + # breaking the build. + # pylint: disable=import-outside-toplevel,import-error from raspihats.i2c_hats import ResponseException with self._lock: @@ -212,7 +218,9 @@ class I2CHatsManager(threading.Thread): def write_dq(self, address, channel, value): """Write a value to a I2C-HAT digital output.""" - # pylint: disable=import-error,no-name-in-module + # This is a Pi module and can't be installed in CI without + # breaking the build. + # pylint: disable=import-outside-toplevel,import-error from raspihats.i2c_hats import ResponseException with self._lock: @@ -224,7 +232,9 @@ class I2CHatsManager(threading.Thread): def read_dq(self, address, channel): """Read a value from a I2C-HAT digital output.""" - # pylint: disable=import-error,no-name-in-module + # This is a Pi module and can't be installed in CI without + # breaking the build. + # pylint: disable=import-outside-toplevel,import-error from raspihats.i2c_hats import ResponseException with self._lock: diff --git a/homeassistant/components/recollect_waste/sensor.py b/homeassistant/components/recollect_waste/sensor.py index 118b6fb3709..17496f3d361 100644 --- a/homeassistant/components/recollect_waste/sensor.py +++ b/homeassistant/components/recollect_waste/sensor.py @@ -1,11 +1,12 @@ """Support for Recollect Waste curbside collection pickup.""" import logging +import recollect_waste 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__) @@ -29,9 +30,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Recollect Waste platform.""" - import recollect_waste - - # pylint: disable=no-member client = recollect_waste.RecollectWasteClient( config[CONF_PLACE_ID], config[CONF_SERVICE_ID] ) @@ -40,7 +38,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): # with given place_id and service_id. try: client.get_next_pickup() - # pylint: disable=no-member except recollect_waste.RecollectWasteException as ex: _LOGGER.error("Recollect Waste platform error. %s", ex) return @@ -85,8 +82,6 @@ class RecollectWasteSensor(Entity): def update(self): """Update device state.""" - import recollect_waste - try: pickup_event = self.client.get_next_pickup() self._state = pickup_event.event_date @@ -96,6 +91,5 @@ class RecollectWasteSensor(Entity): ATTR_AREA_NAME: pickup_event.area_name, } ) - # pylint: disable=no-member except recollect_waste.RecollectWasteException as ex: _LOGGER.error("Recollect Waste platform error. %s", ex) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index b36e0a34fa4..10b1d04304f 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -8,8 +8,14 @@ import queue 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.engine import Engine +from sqlalchemy.event import listens_for +from sqlalchemy.orm import scoped_session, sessionmaker +from sqlalchemy.pool import StaticPool from homeassistant.const import ( ATTR_ENTITY_ID, @@ -23,6 +29,7 @@ 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 @@ -31,6 +38,7 @@ import homeassistant.util.dt as dt_util from . import migration, purge from .const import DATA_INSTANCE +from .models import Base, Events, RecorderRuns, States from .util import session_scope _LOGGER = logging.getLogger(__name__) @@ -100,11 +108,9 @@ def run_information(hass, point_in_time: Optional[datetime] = None): There is also the run that covers point_in_time. """ - from . import models - ins = hass.data[DATA_INSTANCE] - recorder_runs = models.RecorderRuns + recorder_runs = RecorderRuns if point_in_time is None or point_in_time > ins.recording_start: return ins.run_info @@ -208,10 +214,6 @@ class Recorder(threading.Thread): def run(self): """Start processing events to save.""" - from .models import States, Events - from homeassistant.components import persistent_notification - from sqlalchemy import exc - tries = 1 connected = False @@ -393,18 +395,10 @@ class Recorder(threading.Thread): def _setup_connection(self): """Ensure database is ready to fly.""" - from sqlalchemy import create_engine, event - from sqlalchemy.engine import Engine - from sqlalchemy.orm import scoped_session - from sqlalchemy.orm import sessionmaker - from sqlite3 import Connection - - from . import models - kwargs = {} # pylint: disable=unused-variable - @event.listens_for(Engine, "connect") + @listens_for(Engine, "connect") def set_sqlite_pragma(dbapi_connection, connection_record): """Set sqlite's WAL mode.""" if isinstance(dbapi_connection, Connection): @@ -416,8 +410,6 @@ class Recorder(threading.Thread): dbapi_connection.isolation_level = old_isolation if self.db_url == "sqlite://" or ":memory:" in self.db_url: - from sqlalchemy.pool import StaticPool - kwargs["connect_args"] = {"check_same_thread": False} kwargs["poolclass"] = StaticPool kwargs["pool_reset_on_return"] = None @@ -428,7 +420,7 @@ class Recorder(threading.Thread): self.engine.dispose() self.engine = create_engine(self.db_url, **kwargs) - models.Base.metadata.create_all(self.engine) + Base.metadata.create_all(self.engine) self.get_session = scoped_session(sessionmaker(bind=self.engine)) def _close_connection(self): @@ -439,8 +431,6 @@ class Recorder(threading.Thread): def _setup_run(self): """Log the start of the current run.""" - from .models import RecorderRuns - with session_scope(session=self.get_session()) as session: for run in session.query(RecorderRuns).filter_by(end=None): run.closed_incorrect = True diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json index cdb09d66067..1f00cf89f15 100644 --- a/homeassistant/components/recorder/manifest.json +++ b/homeassistant/components/recorder/manifest.json @@ -3,8 +3,8 @@ "name": "Recorder", "documentation": "https://www.home-assistant.io/integrations/recorder", "requirements": [ - "sqlalchemy==1.3.8" + "sqlalchemy==1.3.10" ], "dependencies": [], "codeowners": [] -} +} \ No newline at end of file diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 3de0430d8f3..33a01ea1ac0 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -2,6 +2,11 @@ import logging import os +from sqlalchemy import Table, text +from sqlalchemy.engine import reflection +from sqlalchemy.exc import OperationalError, SQLAlchemyError + +from .models import SchemaChanges, SCHEMA_VERSION, Base from .util import session_scope _LOGGER = logging.getLogger(__name__) @@ -10,8 +15,6 @@ PROGRESS_FILE = ".migration_progress" def migrate_schema(instance): """Check if the schema needs to be upgraded.""" - from .models import SchemaChanges, SCHEMA_VERSION - progress_path = instance.hass.config.path(PROGRESS_FILE) with session_scope(session=instance.get_session()) as session: @@ -60,11 +63,7 @@ def _create_index(engine, table_name, index_name): The index name should match the name given for the index within the table definition described in the models """ - from sqlalchemy import Table - from sqlalchemy.exc import OperationalError - from . import models - - table = Table(table_name, models.Base.metadata) + table = Table(table_name, Base.metadata) _LOGGER.debug("Looking up index for table %s", table_name) # Look up the index object by name from the table is the models index = next(idx for idx in table.indexes if idx.name == index_name) @@ -99,9 +98,6 @@ def _drop_index(engine, table_name, index_name): string here is generated from the method parameters without sanitizing. DO NOT USE THIS FUNCTION IN ANY OPERATION THAT TAKES USER INPUT. """ - from sqlalchemy import text - from sqlalchemy.exc import SQLAlchemyError - _LOGGER.debug("Dropping index %s from table %s", index_name, table_name) success = False @@ -159,9 +155,6 @@ def _drop_index(engine, table_name, index_name): def _add_columns(engine, table_name, columns_def): """Add columns to a table.""" - from sqlalchemy import text - from sqlalchemy.exc import OperationalError - _LOGGER.info( "Adding columns %s to table %s. Note: this can take several " "minutes on large databases and slow computers. Please " @@ -277,9 +270,6 @@ def _inspect_schema_version(engine, session): version 1 are present to make the determination. Eventually this logic can be removed and we can assume a new db is being created. """ - from sqlalchemy.engine import reflection - from .models import SchemaChanges, SCHEMA_VERSION - inspector = reflection.Inspector.from_engine(engine) indexes = inspector.get_indexes("events") diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 12f4a9065af..b512bfc8204 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -15,6 +15,7 @@ from sqlalchemy import ( distinct, ) 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 @@ -164,8 +165,6 @@ class RecorderRuns(Base): # type: ignore Specify point_in_time if you want to know which existed at that point in time inside the run. """ - from sqlalchemy.orm.session import Session - session = Session.object_session(self) assert session is not None, "RecorderRuns need to be persisted" diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index 81426d65f06..089476245fe 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -2,7 +2,10 @@ from datetime import timedelta import logging +from sqlalchemy.exc import SQLAlchemyError + import homeassistant.util.dt as dt_util +from .models import Events, States from .util import session_scope @@ -11,9 +14,6 @@ _LOGGER = logging.getLogger(__name__) def purge_old_data(instance, purge_days, repack): """Purge events and states older than purge_days ago.""" - from .models import States, Events - from sqlalchemy.exc import SQLAlchemyError - purge_before = dt_util.utcnow() - timedelta(days=purge_days) _LOGGER.debug("Purging events before %s", purge_before) @@ -34,8 +34,8 @@ def purge_old_data(instance, purge_days, repack): _LOGGER.debug("Deleted %s events", deleted_rows) # Execute sqlite vacuum command to free up space on disk - if repack and instance.engine.driver == "pysqlite": - _LOGGER.debug("Vacuuming SQLite to free space") + if repack and instance.engine.driver in ("pysqlite", "postgresql"): + _LOGGER.debug("Vacuuming SQL DB to free space") instance.engine.execute("VACUUM") except SQLAlchemyError as err: diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index 674d687ec14..8cfcafea79d 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -3,6 +3,8 @@ from contextlib import contextmanager import logging import time +from sqlalchemy.exc import OperationalError, SQLAlchemyError + from .const import DATA_INSTANCE _LOGGER = logging.getLogger(__name__) @@ -37,8 +39,6 @@ def session_scope(*, hass=None, session=None): def commit(session, work): """Commit & retry work: Either a model or in a function.""" - import sqlalchemy.exc - for _ in range(0, RETRIES): try: if callable(work): @@ -47,7 +47,7 @@ def commit(session, work): session.add(work) session.commit() return True - except sqlalchemy.exc.OperationalError as err: + except OperationalError as err: _LOGGER.error("Error executing query: %s", err) session.rollback() time.sleep(QUERY_RETRY_WAIT) @@ -59,8 +59,6 @@ def execute(qry): This method also retries a few times in the case of stale connections. """ - from sqlalchemy.exc import SQLAlchemyError - for tryno in range(0, RETRIES): try: timer_start = time.perf_counter() diff --git a/homeassistant/components/rejseplanen/sensor.py b/homeassistant/components/rejseplanen/sensor.py index 61cb319fd11..b7d36010714 100644 --- a/homeassistant/components/rejseplanen/sensor.py +++ b/homeassistant/components/rejseplanen/sensor.py @@ -7,17 +7,18 @@ https://help.rejseplanen.dk/hc/en-us/articles/214174465-Rejseplanen-s-API For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.rejseplanen/ """ +from datetime import datetime, timedelta import logging -from datetime import timedelta, datetime from operator import itemgetter +import rjpl 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__) @@ -166,8 +167,6 @@ class PublicTransportData: def update(self): """Get the latest data from rejseplanen.""" - import rjpl - self.info = [] def intersection(lst1, lst2): diff --git a/homeassistant/components/remember_the_milk/__init__.py b/homeassistant/components/remember_the_milk/__init__.py index c92a246da14..fdfbdfd5cdc 100644 --- a/homeassistant/components/remember_the_milk/__init__.py +++ b/homeassistant/components/remember_the_milk/__init__.py @@ -3,6 +3,7 @@ import json import logging import os +from rtmapi import Rtm, RtmRequestFailedException import voluptuous as vol from homeassistant.const import CONF_API_KEY, CONF_ID, CONF_NAME, CONF_TOKEN, STATE_OK @@ -102,8 +103,6 @@ def _create_instance( def _register_new_account( hass, account_name, api_key, shared_secret, stored_rtm_config, component ): - from rtmapi import Rtm - request_id = None configurator = hass.components.configurator api = Rtm(api_key, shared_secret, "write", None) @@ -240,14 +239,12 @@ class RememberTheMilk(Entity): def __init__(self, name, api_key, shared_secret, token, rtm_config): """Create new instance of Remember The Milk component.""" - import rtmapi - self._name = name self._api_key = api_key self._shared_secret = shared_secret self._token = token self._rtm_config = rtm_config - self._rtm_api = rtmapi.Rtm(api_key, shared_secret, "delete", token) + self._rtm_api = Rtm(api_key, shared_secret, "delete", token) self._token_valid = None self._check_token() _LOGGER.debug("Instance created for account %s", self._name) @@ -277,8 +274,6 @@ class RememberTheMilk(Entity): e.g. "my task #some_tag ^today" will add tag "some_tag" and set the due date to today. """ - import rtmapi - try: task_name = call.data.get(CONF_NAME) hass_id = call.data.get(CONF_ID) @@ -316,7 +311,7 @@ class RememberTheMilk(Entity): self.name, task_name, ) - except rtmapi.RtmRequestFailedException as rtm_exception: + except RtmRequestFailedException as rtm_exception: _LOGGER.error( "Error creating new Remember The Milk task for " "account %s: %s", self._name, @@ -327,8 +322,6 @@ class RememberTheMilk(Entity): def complete_task(self, call): """Complete a task that was previously created by this component.""" - import rtmapi - hass_id = call.data.get(CONF_ID) rtm_id = self._rtm_config.get_rtm_id(self._name, hass_id) if rtm_id is None: @@ -352,7 +345,7 @@ class RememberTheMilk(Entity): _LOGGER.debug( "Completed task with id %s in account %s", hass_id, self._name ) - except rtmapi.RtmRequestFailedException as rtm_exception: + except RtmRequestFailedException as rtm_exception: _LOGGER.error( "Error creating new Remember The Milk task for " "account %s: %s", self._name, diff --git a/homeassistant/components/remember_the_milk/manifest.json b/homeassistant/components/remember_the_milk/manifest.json index 4979fe29e0e..6ec9dc6f8f4 100644 --- a/homeassistant/components/remember_the_milk/manifest.json +++ b/homeassistant/components/remember_the_milk/manifest.json @@ -3,7 +3,7 @@ "name": "Remember the milk", "documentation": "https://www.home-assistant.io/integrations/remember_the_milk", "requirements": [ - "RtmAPI==0.7.0", + "RtmAPI==0.7.2", "httplib2==0.10.3" ], "dependencies": ["configurator"], diff --git a/homeassistant/components/repetier/__init__.py b/homeassistant/components/repetier/__init__.py index 6f72a6b7ddc..12975baca91 100644 --- a/homeassistant/components/repetier/__init__.py +++ b/homeassistant/components/repetier/__init__.py @@ -1,7 +1,8 @@ """Support for Repetier-Server sensors.""" -import logging from datetime import timedelta +import logging +import pyrepetier import voluptuous as vol from homeassistant.const import ( @@ -160,8 +161,6 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): """Set up the Repetier Server component.""" - import pyrepetier - hass.data[REPETIER_API] = {} for repetier in config[DOMAIN]: diff --git a/homeassistant/components/rest/sensor.py b/homeassistant/components/rest/sensor.py index 01d974e7006..41adb855903 100644 --- a/homeassistant/components/rest/sensor.py +++ b/homeassistant/components/rest/sensor.py @@ -16,6 +16,7 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_PAYLOAD, CONF_RESOURCE, + CONF_RESOURCE_TEMPLATE, CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME, CONF_TIMEOUT, @@ -42,7 +43,8 @@ METHODS = ["POST", "GET"] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { - vol.Required(CONF_RESOURCE): cv.url, + vol.Exclusive(CONF_RESOURCE, CONF_RESOURCE): cv.url, + vol.Exclusive(CONF_RESOURCE_TEMPLATE, CONF_RESOURCE): cv.template, vol.Optional(CONF_AUTHENTICATION): vol.In( [HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION] ), @@ -62,11 +64,16 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( } ) +PLATFORM_SCHEMA = vol.All( + cv.has_at_least_one_key(CONF_RESOURCE, CONF_RESOURCE_TEMPLATE), PLATFORM_SCHEMA +) + def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the RESTful sensor.""" name = config.get(CONF_NAME) resource = config.get(CONF_RESOURCE) + resource_template = config.get(CONF_RESOURCE_TEMPLATE) method = config.get(CONF_METHOD) payload = config.get(CONF_PAYLOAD) verify_ssl = config.get(CONF_VERIFY_SSL) @@ -83,6 +90,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if value_template is not None: value_template.hass = hass + if resource_template is not None: + resource_template.hass = hass + resource = resource_template.render() + if username and password: if config.get(CONF_AUTHENTICATION) == HTTP_DIGEST_AUTHENTICATION: auth = HTTPDigestAuth(username, password) @@ -108,6 +119,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): value_template, json_attrs, force_update, + resource_template, ) ], True, @@ -127,6 +139,7 @@ class RestSensor(Entity): value_template, json_attrs, force_update, + resource_template, ): """Initialize the REST sensor.""" self._hass = hass @@ -139,6 +152,7 @@ class RestSensor(Entity): self._json_attrs = json_attrs self._attributes = None self._force_update = force_update + self._resource_template = resource_template @property def name(self): @@ -172,6 +186,9 @@ class RestSensor(Entity): def update(self): """Get the latest data from REST API and update the state.""" + if self._resource_template is not None: + self.rest.set_url(self._resource_template.render()) + self.rest.update() value = self.rest.data @@ -217,6 +234,10 @@ class RestData: self._timeout = timeout self.data = None + def set_url(self, url): + """Set url.""" + self._request.prepare_url(url, None) + def update(self): """Get the latest data from REST service with provided method.""" _LOGGER.debug("Updating from %s", self._request.url) diff --git a/homeassistant/components/rest_command/__init__.py b/homeassistant/components/rest_command/__init__.py index 1607000e8d9..223dc7da7cc 100644 --- a/homeassistant/components/rest_command/__init__.py +++ b/homeassistant/components/rest_command/__init__.py @@ -28,7 +28,7 @@ DEFAULT_TIMEOUT = 10 DEFAULT_METHOD = "get" DEFAULT_VERIFY_SSL = True -SUPPORT_REST_METHODS = ["get", "post", "put", "delete"] +SUPPORT_REST_METHODS = ["get", "patch", "post", "put", "delete"] CONF_CONTENT_TYPE = "content_type" diff --git a/homeassistant/components/rflink/__init__.py b/homeassistant/components/rflink/__init__.py index c218bc271ce..b3e1d2b16b7 100644 --- a/homeassistant/components/rflink/__init__.py +++ b/homeassistant/components/rflink/__init__.py @@ -2,8 +2,10 @@ import asyncio from collections import defaultdict import logging -import async_timeout +import async_timeout +from rflink.protocol import create_rflink_connection +from serial import SerialException import voluptuous as vol from homeassistant.const import ( @@ -11,18 +13,18 @@ from homeassistant.const import ( CONF_COMMAND, CONF_HOST, CONF_PORT, - STATE_ON, EVENT_HOMEASSISTANT_STOP, + STATE_ON, ) 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.entity import Entity from homeassistant.helpers.dispatcher import ( - async_dispatcher_send, async_dispatcher_connect, + async_dispatcher_send, ) +from homeassistant.helpers.entity import Entity from homeassistant.helpers.restore_state import RestoreEntity _LOGGER = logging.getLogger(__name__) @@ -118,9 +120,6 @@ def identify_event_type(event): async def async_setup(hass, config): """Set up the Rflink component.""" - from rflink.protocol import create_rflink_connection - import serial - # Allow entities to register themselves by device_id to be looked up when # new rflink events arrive to be handled hass.data[DATA_ENTITY_LOOKUP] = { @@ -239,7 +238,7 @@ async def async_setup(hass, config): transport, protocol = await connection except ( - serial.serialutil.SerialException, + SerialException, ConnectionRefusedError, TimeoutError, OSError, diff --git a/homeassistant/components/rflink/sensor.py b/homeassistant/components/rflink/sensor.py index 48484621c4d..aa0ef4f9c62 100644 --- a/homeassistant/components/rflink/sensor.py +++ b/homeassistant/components/rflink/sensor.py @@ -1,6 +1,7 @@ """Support for Rflink sensors.""" import logging +from rflink.parser import PACKET_FIELDS, UNITS import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -66,8 +67,6 @@ def lookup_unit_for_sensor_type(sensor_type): Async friendly. """ - from rflink.parser import UNITS, PACKET_FIELDS - field_abbrev = {v: k for k, v in PACKET_FIELDS.items()} return UNITS.get(field_abbrev.get(sensor_type)) diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index 79b3054ecf2..1515ce33c6e 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -1,7 +1,9 @@ """Support for RFXtrx devices.""" +import binascii from collections import OrderedDict import logging +import RFXtrx as rfxtrxmod import voluptuous as vol from homeassistant.const import ( @@ -12,8 +14,8 @@ from homeassistant.const import ( CONF_DEVICES, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, - TEMP_CELSIUS, POWER_WATT, + TEMP_CELSIUS, ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -113,9 +115,6 @@ def setup(hass, config): for subscriber in RECEIVED_EVT_SUBSCRIBERS: subscriber(event) - # Try to load the RFXtrx module. - import RFXtrx as rfxtrxmod - device = config[DOMAIN][ATTR_DEVICE] debug = config[DOMAIN][ATTR_DEBUG] dummy_connection = config[DOMAIN][ATTR_DUMMY] @@ -144,8 +143,6 @@ def setup(hass, config): def get_rfx_object(packetid): """Return the RFXObject with the packetid.""" - import RFXtrx as rfxtrxmod - try: binarypacket = bytearray.fromhex(packetid) except ValueError: @@ -167,7 +164,6 @@ def get_pt2262_deviceid(device_id, nb_data_bits): """Extract and return the address bits from a Lighting4/PT2262 packet.""" if nb_data_bits is None: return - import binascii try: data = bytearray.fromhex(device_id) diff --git a/homeassistant/components/rfxtrx/binary_sensor.py b/homeassistant/components/rfxtrx/binary_sensor.py index 8f1c7e6fa55..6465dc36326 100644 --- a/homeassistant/components/rfxtrx/binary_sensor.py +++ b/homeassistant/components/rfxtrx/binary_sensor.py @@ -1,6 +1,7 @@ """Support for RFXtrx binary sensors.""" import logging +import RFXtrx as rfxtrxmod import voluptuous as vol from homeassistant.components import rfxtrx @@ -54,8 +55,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Binary Sensor platform to RFXtrx.""" - import RFXtrx as rfxtrxmod - sensors = [] for packet_id, entity in config[CONF_DEVICES].items(): diff --git a/homeassistant/components/rfxtrx/cover.py b/homeassistant/components/rfxtrx/cover.py index 3d420981685..7aff22bd124 100644 --- a/homeassistant/components/rfxtrx/cover.py +++ b/homeassistant/components/rfxtrx/cover.py @@ -1,4 +1,5 @@ """Support for RFXtrx covers.""" +import RFXtrx as rfxtrxmod import voluptuous as vol from homeassistant.components import rfxtrx @@ -34,8 +35,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the RFXtrx cover.""" - import RFXtrx as rfxtrxmod - covers = rfxtrx.get_devices_from_config(config, RfxtrxCover) add_entities(covers) diff --git a/homeassistant/components/rfxtrx/light.py b/homeassistant/components/rfxtrx/light.py index d2d2e842c0a..a745a11388a 100644 --- a/homeassistant/components/rfxtrx/light.py +++ b/homeassistant/components/rfxtrx/light.py @@ -1,6 +1,7 @@ """Support for RFXtrx lights.""" import logging +import RFXtrx as rfxtrxmod import voluptuous as vol from homeassistant.components import rfxtrx @@ -45,8 +46,6 @@ SUPPORT_RFXTRX = SUPPORT_BRIGHTNESS def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the RFXtrx platform.""" - import RFXtrx as rfxtrxmod - lights = rfxtrx.get_devices_from_config(config, RfxtrxLight) add_entities(lights) diff --git a/homeassistant/components/rfxtrx/sensor.py b/homeassistant/components/rfxtrx/sensor.py index 5941b00764b..5429943a7a6 100644 --- a/homeassistant/components/rfxtrx/sensor.py +++ b/homeassistant/components/rfxtrx/sensor.py @@ -1,6 +1,7 @@ """Support for RFXtrx sensors.""" import logging +from RFXtrx import SensorEvent import voluptuous as vol from homeassistant.components import rfxtrx @@ -43,8 +44,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the RFXtrx platform.""" - from RFXtrx import SensorEvent - sensors = [] for packet_id, entity_info in config[CONF_DEVICES].items(): event = rfxtrx.get_rfx_object(packet_id) diff --git a/homeassistant/components/rfxtrx/switch.py b/homeassistant/components/rfxtrx/switch.py index bb5d5fe6d43..6d91b261a4f 100644 --- a/homeassistant/components/rfxtrx/switch.py +++ b/homeassistant/components/rfxtrx/switch.py @@ -1,6 +1,7 @@ """Support for RFXtrx switches.""" import logging +import RFXtrx as rfxtrxmod import voluptuous as vol from homeassistant.components import rfxtrx @@ -38,8 +39,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities_callback, discovery_info=None): """Set up the RFXtrx platform.""" - import RFXtrx as rfxtrxmod - # Add switch from config file switches = rfxtrx.get_devices_from_config(config, RfxtrxSwitch) add_entities_callback(switches) diff --git a/homeassistant/components/rmvtransport/manifest.json b/homeassistant/components/rmvtransport/manifest.json index 1f06daf0623..ed33caa1264 100644 --- a/homeassistant/components/rmvtransport/manifest.json +++ b/homeassistant/components/rmvtransport/manifest.json @@ -3,7 +3,7 @@ "name": "Rmvtransport", "documentation": "https://www.home-assistant.io/integrations/rmvtransport", "requirements": [ - "PyRMVtransport==0.1.3" + "PyRMVtransport==0.2.9" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/rmvtransport/sensor.py b/homeassistant/components/rmvtransport/sensor.py index d7d075f48f7..190274518cd 100644 --- a/homeassistant/components/rmvtransport/sensor.py +++ b/homeassistant/components/rmvtransport/sensor.py @@ -3,6 +3,8 @@ import asyncio import logging from datetime import timedelta +from RMVtransport import RMVtransport +from RMVtransport.rmvtransport import RMVtransportApiConnectionError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -157,7 +159,7 @@ class RMVDepartureSensor(Entity): """Return the state attributes.""" try: return { - "next_departures": [val for val in self.data.departures[1:]], + "next_departures": self.data.departures[1:], "direction": self.data.departures[0].get("direction"), "line": self.data.departures[0].get("line"), "minutes": self.data.departures[0].get("minutes"), @@ -208,8 +210,6 @@ class RMVDepartureData: timeout, ): """Initialize the sensor.""" - from RMVtransport import RMVtransport - self.station = None self._station_id = station_id self._destinations = destinations @@ -224,14 +224,12 @@ class RMVDepartureData: @Throttle(SCAN_INTERVAL) async def async_update(self): """Update the connection data.""" - from RMVtransport.rmvtransport import RMVtransportApiConnectionError - try: _data = await self.rmv.get_departures( self._station_id, products=self._products, - directionId=self._direction, - maxJourneys=50, + direction_id=self._direction, + max_journeys=50, ) except RMVtransportApiConnectionError: self.departures = [] diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index d69b0eddb71..12aca141510 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -20,7 +20,7 @@ from homeassistant.const import ( STATE_HOME, STATE_IDLE, STATE_PLAYING, - STATE_OFF, + STATE_STANDBY, ) DEFAULT_PORT = 8060 @@ -98,7 +98,7 @@ class RokuDevice(MediaPlayerDevice): def state(self): """Return the state of the device.""" if self._power_state == "Off": - return STATE_OFF + return STATE_STANDBY if self.current_app is None: return None diff --git a/homeassistant/components/route53/services.yaml b/homeassistant/components/route53/services.yaml index e69de29bb2d..20dbfa77f7a 100644 --- a/homeassistant/components/route53/services.yaml +++ b/homeassistant/components/route53/services.yaml @@ -0,0 +1,2 @@ +update_records: + description: Trigger update of records. \ No newline at end of file diff --git a/homeassistant/components/rpi_gpio/__init__.py b/homeassistant/components/rpi_gpio/__init__.py index 31509614df4..ed7eefbb1fe 100644 --- a/homeassistant/components/rpi_gpio/__init__.py +++ b/homeassistant/components/rpi_gpio/__init__.py @@ -1,6 +1,8 @@ """Support for controlling GPIO pins of a Raspberry Pi.""" import logging +from RPi import GPIO # pylint: disable=import-error + from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP _LOGGER = logging.getLogger(__name__) @@ -10,7 +12,6 @@ DOMAIN = "rpi_gpio" def setup(hass, config): """Set up the Raspberry PI GPIO component.""" - from RPi import GPIO # pylint: disable=import-error def cleanup_gpio(event): """Stuff to do before stopping.""" @@ -27,34 +28,24 @@ def setup(hass, config): def setup_output(port): """Set up a GPIO as output.""" - from RPi import GPIO # pylint: disable=import-error - GPIO.setup(port, GPIO.OUT) def setup_input(port, pull_mode): """Set up a GPIO as input.""" - from RPi import GPIO # pylint: disable=import-error - GPIO.setup(port, GPIO.IN, GPIO.PUD_DOWN if pull_mode == "DOWN" else GPIO.PUD_UP) def write_output(port, value): """Write a value to a GPIO.""" - from RPi import GPIO # pylint: disable=import-error - GPIO.output(port, value) def read_input(port): """Read a value from a GPIO.""" - from RPi import GPIO # pylint: disable=import-error - return GPIO.input(port) def edge_detect(port, event_callback, bounce): """Add detection for RISING and FALLING events.""" - from RPi import GPIO # pylint: disable=import-error - GPIO.add_event_detect(port, GPIO.BOTH, callback=event_callback, bouncetime=bounce) diff --git a/homeassistant/components/rpi_gpio/binary_sensor.py b/homeassistant/components/rpi_gpio/binary_sensor.py index 4acbed9a0fa..3e38da47eed 100644 --- a/homeassistant/components/rpi_gpio/binary_sensor.py +++ b/homeassistant/components/rpi_gpio/binary_sensor.py @@ -4,7 +4,7 @@ import logging import voluptuous as vol from homeassistant.components import rpi_gpio -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/rpi_gpio/cover.py b/homeassistant/components/rpi_gpio/cover.py index 83cc497324d..648171b9738 100644 --- a/homeassistant/components/rpi_gpio/cover.py +++ b/homeassistant/components/rpi_gpio/cover.py @@ -4,9 +4,9 @@ from time import sleep import voluptuous as vol -from homeassistant.components.cover import CoverDevice, PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME from homeassistant.components import rpi_gpio +from homeassistant.components.cover import PLATFORM_SCHEMA, CoverDevice +from homeassistant.const import CONF_NAME import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/rpi_gpio/manifest.json b/homeassistant/components/rpi_gpio/manifest.json index 0bee2baeddf..4d3ea4da010 100644 --- a/homeassistant/components/rpi_gpio/manifest.json +++ b/homeassistant/components/rpi_gpio/manifest.json @@ -3,7 +3,7 @@ "name": "Rpi gpio", "documentation": "https://www.home-assistant.io/integrations/rpi_gpio", "requirements": [ - "RPi.GPIO==0.6.5" + "RPi.GPIO==0.7.0" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/rpi_pfio/__init__.py b/homeassistant/components/rpi_pfio/__init__.py index d51785daf9c..72be34e0f45 100644 --- a/homeassistant/components/rpi_pfio/__init__.py +++ b/homeassistant/components/rpi_pfio/__init__.py @@ -1,6 +1,8 @@ """Support for controlling the PiFace Digital I/O module on a RPi.""" import logging +import pifacedigitalio as PFIO + from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP _LOGGER = logging.getLogger(__name__) @@ -12,8 +14,6 @@ DATA_PFIO_LISTENER = "pfio_listener" def setup(hass, config): """Set up the Raspberry PI PFIO component.""" - import pifacedigitalio as PFIO - pifacedigital = PFIO.PiFaceDigital() hass.data[DATA_PFIO_LISTENER] = PFIO.InputEventListener(chip=pifacedigital) @@ -33,22 +33,16 @@ def setup(hass, config): def write_output(port, value): """Write a value to a PFIO.""" - import pifacedigitalio as PFIO - PFIO.digital_write(port, value) def read_input(port): """Read a value from a PFIO.""" - import pifacedigitalio as PFIO - return PFIO.digital_read(port) def edge_detect(hass, port, event_callback, settle): """Add detection for RISING and FALLING events.""" - import pifacedigitalio as PFIO - hass.data[DATA_PFIO_LISTENER].register( port, PFIO.IODIR_BOTH, event_callback, settle_time=settle ) diff --git a/homeassistant/components/rpi_pfio/binary_sensor.py b/homeassistant/components/rpi_pfio/binary_sensor.py index 44da251732b..89d44a0e8db 100644 --- a/homeassistant/components/rpi_pfio/binary_sensor.py +++ b/homeassistant/components/rpi_pfio/binary_sensor.py @@ -3,8 +3,8 @@ import logging import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice from homeassistant.components import rpi_pfio +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 diff --git a/homeassistant/components/sabnzbd/sensor.py b/homeassistant/components/sabnzbd/sensor.py index 58624c758d9..21ac9eefdb2 100644 --- a/homeassistant/components/sabnzbd/sensor.py +++ b/homeassistant/components/sabnzbd/sensor.py @@ -49,6 +49,7 @@ class SabnzbdSensor(Entity): """Return the state of the sensor.""" return self._state + @property def should_poll(self): """Don't poll. Will be updated by dispatcher signal.""" return False diff --git a/homeassistant/components/saj/manifest.json b/homeassistant/components/saj/manifest.json index e42b37195a4..2dd701e9c7c 100644 --- a/homeassistant/components/saj/manifest.json +++ b/homeassistant/components/saj/manifest.json @@ -3,7 +3,7 @@ "name": "SAJ", "documentation": "https://www.home-assistant.io/integrations/saj", "requirements": [ - "pysaj==0.0.9" + "pysaj==0.0.12" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/saj/sensor.py b/homeassistant/components/saj/sensor.py index fa06b2b9125..5605866908e 100644 --- a/homeassistant/components/saj/sensor.py +++ b/homeassistant/components/saj/sensor.py @@ -9,6 +9,9 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_HOST, + CONF_PASSWORD, + CONF_TYPE, + CONF_USERNAME, DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, ENERGY_KILO_WATT_HOUR, @@ -31,6 +34,8 @@ MAX_INTERVAL = 300 UNIT_OF_MEASUREMENT_HOURS = "h" +INVERTER_TYPES = ["ethernet", "wifi"] + SAJ_UNIT_MAPPINGS = { "W": POWER_WATT, "kWh": ENERGY_KILO_WATT_HOUR, @@ -40,16 +45,24 @@ SAJ_UNIT_MAPPINGS = { "": None, } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_HOST): cv.string}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_TYPE, default=INVERTER_TYPES[0]): vol.In(INVERTER_TYPES), + vol.Inclusive(CONF_USERNAME, "credentials"): cv.string, + vol.Inclusive(CONF_PASSWORD, "credentials"): cv.string, + } +) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up SAJ sensors.""" remove_interval_update = None + wifi = config[CONF_TYPE] == INVERTER_TYPES[1] # Init all sensors - sensor_def = pysaj.Sensors() + sensor_def = pysaj.Sensors(wifi) # Use all sensors by default hass_sensors = [] @@ -57,7 +70,25 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= for sensor in sensor_def: hass_sensors.append(SAJsensor(sensor)) - saj = pysaj.SAJ(config[CONF_HOST]) + kwargs = {} + + if wifi: + kwargs["wifi"] = True + if config.get(CONF_USERNAME) and config.get(CONF_PASSWORD): + kwargs["username"] = config[CONF_USERNAME] + kwargs["password"] = config[CONF_PASSWORD] + + try: + saj = pysaj.SAJ(config[CONF_HOST], **kwargs) + await saj.read(sensor_def) + except pysaj.UnauthorizedException: + _LOGGER.error("Username and/or password is wrong.") + return + except pysaj.UnexpectedResponseException as err: + _LOGGER.error( + "Error in SAJ, please check host/ip address. Original error: %s", err + ) + return async_add_entities(hass_sensors) diff --git a/homeassistant/components/scene/__init__.py b/homeassistant/components/scene/__init__.py index 1f71a24c304..ec2dc3118a9 100644 --- a/homeassistant/components/scene/__init__.py +++ b/homeassistant/components/scene/__init__.py @@ -1,5 +1,4 @@ """Allow users to set and activate scenes.""" -import asyncio import importlib import logging @@ -7,7 +6,6 @@ import voluptuous as vol from homeassistant.core import DOMAIN as HA_DOMAIN from homeassistant.const import CONF_PLATFORM, SERVICE_TURN_ON -from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.state import HASS_DOMAIN @@ -69,20 +67,7 @@ async def async_setup(hass, config): HA_DOMAIN, {"platform": "homeasistant", STATES: []} ) - async def async_handle_scene_service(service): - """Handle calls to the switch services.""" - target_scenes = await component.async_extract_from_service(service) - - tasks = [scene.async_activate() for scene in target_scenes] - if tasks: - await asyncio.wait(tasks) - - hass.services.async_register( - DOMAIN, - SERVICE_TURN_ON, - async_handle_scene_service, - schema=ENTITY_SERVICE_SCHEMA, - ) + component.async_register_entity_service(SERVICE_TURN_ON, {}, "async_activate") return True diff --git a/homeassistant/components/scene/services.yaml b/homeassistant/components/scene/services.yaml index ee255affe44..0f1e7103aaf 100644 --- a/homeassistant/components/scene/services.yaml +++ b/homeassistant/components/scene/services.yaml @@ -5,4 +5,18 @@ turn_on: fields: entity_id: description: Name(s) of scenes to turn on - example: 'scene.romantic' + example: "scene.romantic" + +reload: + description: Reload the scene configuration + +apply: + description: Activate a scene. Takes same data as the entities field from a single scene in the config. + fields: + entities: + description: The entities and the state that they need to be. + example: + light.kitchen: "on" + light.ceiling: + state: "on" + brightness: 80 diff --git a/homeassistant/components/scrape/manifest.json b/homeassistant/components/scrape/manifest.json index 989070900ca..5fdcca372b9 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.0" + "beautifulsoup4==4.8.1" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py index b6a6fdf4896..0bfb7351c88 100644 --- a/homeassistant/components/scrape/sensor.py +++ b/homeassistant/components/scrape/sensor.py @@ -1,6 +1,7 @@ """Support for getting data from websites with scraping.""" import logging +from bs4 import BeautifulSoup import voluptuous as vol from requests.auth import HTTPBasicAuth, HTTPDigestAuth @@ -124,8 +125,6 @@ class ScrapeSensor(Entity): _LOGGER.error("Unable to retrieve data") return - from bs4 import BeautifulSoup - raw_data = BeautifulSoup(self.rest.data, "html.parser") _LOGGER.debug(raw_data) diff --git a/homeassistant/components/season/sensor.py b/homeassistant/components/season/sensor.py index cdd6af57617..46d2291cf81 100644 --- a/homeassistant/components/season/sensor.py +++ b/homeassistant/components/season/sensor.py @@ -2,6 +2,7 @@ import logging from datetime import datetime +import ephem import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -67,7 +68,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): def get_season(date, hemisphere, season_tracking_type): """Calculate the current season.""" - import ephem if hemisphere == "equator": return None diff --git a/homeassistant/components/sensor/.translations/ca.json b/homeassistant/components/sensor/.translations/ca.json index 59db5a62f86..94d95e7ddf8 100644 --- a/homeassistant/components/sensor/.translations/ca.json +++ b/homeassistant/components/sensor/.translations/ca.json @@ -4,6 +4,7 @@ "is_battery_level": "Nivell de bateria de {entity_name}", "is_humidity": "Humitat de {entity_name}", "is_illuminance": "Il\u00b7luminaci\u00f3 de {entity_name}", + "is_power": "Pot\u00e8ncia de {entity_name}", "is_pressure": "Pressi\u00f3 de {entity_name}", "is_signal_strength": "For\u00e7a del senyal de {entity_name}", "is_temperature": "Temperatura de {entity_name}", @@ -14,6 +15,7 @@ "battery_level": "Nivell de bateria de {entity_name}", "humidity": "Humitat de {entity_name}", "illuminance": "Il\u00b7luminaci\u00f3 de {entity_name}", + "power": "Pot\u00e8ncia de {entity_name}", "pressure": "Pressi\u00f3 de {entity_name}", "signal_strength": "For\u00e7a del senyal de {entity_name}", "temperature": "Temperatura de {entity_name}", diff --git a/homeassistant/components/sensor/.translations/de.json b/homeassistant/components/sensor/.translations/de.json index 1f248099df3..bf28653c0ce 100644 --- a/homeassistant/components/sensor/.translations/de.json +++ b/homeassistant/components/sensor/.translations/de.json @@ -1,7 +1,10 @@ { "device_automation": { "condition_type": { + "is_battery_level": "{entity_name} Batteriestand", "is_humidity": "{entity_name} Feuchtigkeit", + "is_illuminance": "{entity_name} Beleuchtungsst\u00e4rke", + "is_power": "{entity_name} Leistung", "is_pressure": "{entity_name} Druck", "is_signal_strength": "{entity_name} Signalst\u00e4rke", "is_temperature": "{entity_name} Temperatur", @@ -11,6 +14,8 @@ "trigger_type": { "battery_level": "{entity_name} Batteriestatus", "humidity": "{entity_name} Feuchtigkeit", + "illuminance": "{entity_name} Beleuchtungsst\u00e4rke", + "power": "{entity_name} Leistung", "pressure": "{entity_name} Druck", "signal_strength": "{entity_name} Signalst\u00e4rke", "temperature": "{entity_name} Temperatur", diff --git a/homeassistant/components/sensor/.translations/en.json b/homeassistant/components/sensor/.translations/en.json index 7bbbe660feb..07411b885b8 100644 --- a/homeassistant/components/sensor/.translations/en.json +++ b/homeassistant/components/sensor/.translations/en.json @@ -1,26 +1,26 @@ { "device_automation": { "condition_type": { - "is_battery_level": "{entity_name} battery level", - "is_humidity": "{entity_name} humidity", - "is_illuminance": "{entity_name} illuminance", - "is_power": "{entity_name} power", - "is_pressure": "{entity_name} pressure", - "is_signal_strength": "{entity_name} signal strength", - "is_temperature": "{entity_name} temperature", - "is_timestamp": "{entity_name} timestamp", - "is_value": "{entity_name} value" + "is_battery_level": "Current {entity_name} battery level", + "is_humidity": "Current {entity_name} humidity", + "is_illuminance": "Current {entity_name} illuminance", + "is_power": "Current {entity_name} power", + "is_pressure": "Current {entity_name} pressure", + "is_signal_strength": "Current {entity_name} signal strength", + "is_temperature": "Current {entity_name} temperature", + "is_timestamp": "Current {entity_name} timestamp", + "is_value": "Current {entity_name} value" }, "trigger_type": { - "battery_level": "{entity_name} battery level", - "humidity": "{entity_name} humidity", - "illuminance": "{entity_name} illuminance", - "power": "{entity_name} power", - "pressure": "{entity_name} pressure", - "signal_strength": "{entity_name} signal strength", - "temperature": "{entity_name} temperature", - "timestamp": "{entity_name} timestamp", - "value": "{entity_name} value" + "battery_level": "{entity_name} battery level changes", + "humidity": "{entity_name} humidity changes", + "illuminance": "{entity_name} illuminance changes", + "power": "{entity_name} power changes", + "pressure": "{entity_name} pressure changes", + "signal_strength": "{entity_name} signal strength changes", + "temperature": "{entity_name} temperature changes", + "timestamp": "{entity_name} timestamp changes", + "value": "{entity_name} value changes" } } } \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/fr.json b/homeassistant/components/sensor/.translations/fr.json index 676a5aa413f..56725a59e21 100644 --- a/homeassistant/components/sensor/.translations/fr.json +++ b/homeassistant/components/sensor/.translations/fr.json @@ -1,24 +1,24 @@ { "device_automation": { "condition_type": { - "is_battery_level": "{entity_name} niveau batterie", - "is_humidity": "{entity_name} humidit\u00e9", - "is_illuminance": "{entity_name} \u00e9clairement", + "is_battery_level": "Le niveau de la batterie de {entity_name}", + "is_humidity": "L'humidit\u00e9 de {entity_name}", + "is_illuminance": "L'\u00e9clairement de {entity_name}", "is_power": "{entity_name} puissance", "is_pressure": "{entity_name} pression", "is_signal_strength": "{entity_name} force du signal", - "is_temperature": "{entity_name} temp\u00e9rature", + "is_temperature": "La temp\u00e9rature de {entity_name}", "is_timestamp": "{entity_name} horodatage", "is_value": "{entity_name} valeur" }, "trigger_type": { - "battery_level": "{entity_name} niveau batterie", - "humidity": "{entity_name} humidit\u00e9", - "illuminance": "{entity_name} \u00e9clairement", + "battery_level": "Le niveau de la batterie de {entity_name}", + "humidity": "L'humidit\u00e9 de {entity_name}", + "illuminance": "L'\u00e9clairement de {entity_name}", "power": "{entity_name} puissance", "pressure": "{entity_name} pression", "signal_strength": "{entity_name} force du signal", - "temperature": "{entity_name} temp\u00e9rature", + "temperature": "La temp\u00e9rature de {entity_name}", "timestamp": "{entity_name} horodatage", "value": "{entity_name} valeur" } diff --git a/homeassistant/components/sensor/.translations/hu.json b/homeassistant/components/sensor/.translations/hu.json new file mode 100644 index 00000000000..78ea3e5e89b --- /dev/null +++ b/homeassistant/components/sensor/.translations/hu.json @@ -0,0 +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" + }, + "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" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/ko.json b/homeassistant/components/sensor/.translations/ko.json new file mode 100644 index 00000000000..d24a4058343 --- /dev/null +++ b/homeassistant/components/sensor/.translations/ko.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "{entity_name} \ubc30\ud130\ub9ac \uc794\ub7c9", + "is_humidity": "{entity_name} \uc2b5\ub3c4", + "is_illuminance": "{entity_name} \uc870\ub3c4", + "is_power": "{entity_name} \uc18c\ube44 \uc804\ub825", + "is_pressure": "{entity_name} \uc555\ub825", + "is_signal_strength": "{entity_name} \uc2e0\ud638 \uac15\ub3c4", + "is_temperature": "{entity_name} \uc628\ub3c4", + "is_timestamp": "{entity_name} \uc2dc\uac01", + "is_value": "{entity_name} \uac12" + }, + "trigger_type": { + "battery_level": "{entity_name} \ubc30\ud130\ub9ac \uc794\ub7c9", + "humidity": "{entity_name} \uc2b5\ub3c4", + "illuminance": "{entity_name} \uc870\ub3c4", + "power": "{entity_name} \uc18c\ube44 \uc804\ub825", + "pressure": "{entity_name} \uc555\ub825", + "signal_strength": "{entity_name} \uc2e0\ud638 \uac15\ub3c4", + "temperature": "{entity_name} \uc628\ub3c4", + "timestamp": "{entity_name} \uc2dc\uac01", + "value": "{entity_name} \uac12" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/moon.hu.json b/homeassistant/components/sensor/.translations/moon.hu.json index 0fcd02a6961..fff9f51f50d 100644 --- a/homeassistant/components/sensor/.translations/moon.hu.json +++ b/homeassistant/components/sensor/.translations/moon.hu.json @@ -4,9 +4,9 @@ "full_moon": "Telihold", "last_quarter": "Utols\u00f3 negyed", "new_moon": "\u00dajhold", - "waning_crescent": "Fogy\u00f3 Hold (sarl\u00f3)", - "waning_gibbous": "Fogy\u00f3 Hold", - "waxing_crescent": "N\u00f6v\u0151 Hold (sarl\u00f3)", - "waxing_gibbous": "N\u00f6v\u0151 Hold" + "waning_crescent": "Fogy\u00f3 holdsarl\u00f3", + "waning_gibbous": "Fogy\u00f3 hold", + "waxing_crescent": "N\u00f6v\u0151 holdsarl\u00f3", + "waxing_gibbous": "N\u00f6v\u0151 hold" } } \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/nl.json b/homeassistant/components/sensor/.translations/nl.json new file mode 100644 index 00000000000..33a7d837d55 --- /dev/null +++ b/homeassistant/components/sensor/.translations/nl.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "Huidige batterijniveau {entity_name}", + "is_humidity": "{entity_name} vochtigheidsgraad", + "is_illuminance": "{entity_name} verlichtingssterkte", + "is_power": "{entity_name}\nvermogen", + "is_pressure": "{entity_name} druk", + "is_signal_strength": "{entity_name} signaalsterkte", + "is_temperature": "{entity_name} temperatuur", + "is_timestamp": "{entity_name} tijdstip", + "is_value": "{entity_name} waarde" + }, + "trigger_type": { + "battery_level": "{entity_name} batterijniveau", + "humidity": "{entity_name} vochtigheidsgraad", + "illuminance": "{entity_name} verlichtingssterkte", + "power": "{entity_name} vermogen", + "pressure": "{entity_name} druk", + "signal_strength": "{entity_name} signaalsterkte", + "temperature": "{entity_name} temperatuur", + "timestamp": "{entity_name} tijdstip", + "value": "{entity_name} waarde" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/no.json b/homeassistant/components/sensor/.translations/no.json index 5f5eeaacd11..6709e4eb28c 100644 --- a/homeassistant/components/sensor/.translations/no.json +++ b/homeassistant/components/sensor/.translations/no.json @@ -4,7 +4,7 @@ "is_battery_level": "{entity_name} batteriniv\u00e5", "is_humidity": "{entity_name} fuktighet", "is_illuminance": "{entity_name} belysningsstyrke", - "is_power": "{entity_name} str\u00f8m", + "is_power": "{entity_name} effekt", "is_pressure": "{entity_name} trykk", "is_signal_strength": "{entity_name} signalstyrke", "is_temperature": "{entity_name} temperatur", diff --git a/homeassistant/components/sensor/.translations/pl.json b/homeassistant/components/sensor/.translations/pl.json index da1dcc1d6fd..68a3a0fecfd 100644 --- a/homeassistant/components/sensor/.translations/pl.json +++ b/homeassistant/components/sensor/.translations/pl.json @@ -3,7 +3,24 @@ "condition_type": { "is_battery_level": "{entity_name} poziom na\u0142adowania baterii", "is_humidity": "{entity_name} wilgotno\u015b\u0107", - "is_temperature": "{entity_name} temperatura" + "is_illuminance": "nat\u0119\u017cenie o\u015bwietlenia {entity_name}", + "is_power": "moc {entity_name}", + "is_pressure": "ci\u015bnienie {entity_name}", + "is_signal_strength": "si\u0142a sygna\u0142u {entity_name}", + "is_temperature": "temperatura {entity_name}", + "is_timestamp": "znacznik czasu {entity_name}", + "is_value": "warto\u015b\u0107 {entity_name}" + }, + "trigger_type": { + "battery_level": "poziom baterii {entity_name}", + "humidity": "wilgotno\u015b\u0107 {entity_name}", + "illuminance": "nat\u0119\u017cenie o\u015bwietlenia {entity_name}", + "power": "moc {entity_name}", + "pressure": "ci\u015bnienie {entity_name}", + "signal_strength": "si\u0142a sygna\u0142u {entity_name}", + "temperature": "temperatura {entity_name}", + "timestamp": "znacznik czasu {entity_name}", + "value": "warto\u015b\u0107 {entity_name}" } } } \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/pt.json b/homeassistant/components/sensor/.translations/pt.json new file mode 100644 index 00000000000..801b22f0c45 --- /dev/null +++ b/homeassistant/components/sensor/.translations/pt.json @@ -0,0 +1,21 @@ +{ + "device_automation": { + "condition_type": { + "is_humidity": "humidade {entity_name}", + "is_power": "pot\u00eancia {entity_name}", + "is_timestamp": "momento temporal de {entity_name}", + "is_value": "valor {entity_name}" + }, + "trigger_type": { + "battery_level": "n\u00edvel da bateria {entity_name}", + "humidity": "humidade {entity_name}", + "illuminance": "ilumin\u00e2ncia {entity_name}", + "power": "pot\u00eancia {entity_name}", + "pressure": "press\u00e3o {entity_name}", + "signal_strength": "for\u00e7a do sinal de {entity_name}", + "temperature": "temperatura de {entity_name}", + "timestamp": "momento temporal de {entity_name}", + "value": "valor {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/ru.json b/homeassistant/components/sensor/.translations/ru.json new file mode 100644 index 00000000000..8c70f41fcb7 --- /dev/null +++ b/homeassistant/components/sensor/.translations/ru.json @@ -0,0 +1,15 @@ +{ + "device_automation": { + "trigger_type": { + "battery_level": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "humidity": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "illuminance": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "power": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "pressure": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "signal_strength": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "temperature": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "timestamp": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "value": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/device_condition.py b/homeassistant/components/sensor/device_condition.py new file mode 100644 index 00000000000..259fb5dbab9 --- /dev/null +++ b/homeassistant/components/sensor/device_condition.py @@ -0,0 +1,170 @@ +"""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, + CONF_ABOVE, + CONF_BELOW, + CONF_ENTITY_ID, + CONF_TYPE, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_POWER, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_SIGNAL_STRENGTH, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_TIMESTAMP, +) +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" + +CONF_IS_BATTERY_LEVEL = "is_battery_level" +CONF_IS_HUMIDITY = "is_humidity" +CONF_IS_ILLUMINANCE = "is_illuminance" +CONF_IS_POWER = "is_power" +CONF_IS_PRESSURE = "is_pressure" +CONF_IS_SIGNAL_STRENGTH = "is_signal_strength" +CONF_IS_TEMPERATURE = "is_temperature" +CONF_IS_TIMESTAMP = "is_timestamp" +CONF_IS_VALUE = "is_value" + +ENTITY_CONDITIONS = { + DEVICE_CLASS_BATTERY: [{CONF_TYPE: CONF_IS_BATTERY_LEVEL}], + DEVICE_CLASS_HUMIDITY: [{CONF_TYPE: CONF_IS_HUMIDITY}], + DEVICE_CLASS_ILLUMINANCE: [{CONF_TYPE: CONF_IS_ILLUMINANCE}], + DEVICE_CLASS_POWER: [{CONF_TYPE: CONF_IS_POWER}], + DEVICE_CLASS_PRESSURE: [{CONF_TYPE: CONF_IS_PRESSURE}], + DEVICE_CLASS_SIGNAL_STRENGTH: [{CONF_TYPE: CONF_IS_SIGNAL_STRENGTH}], + DEVICE_CLASS_TEMPERATURE: [{CONF_TYPE: CONF_IS_TEMPERATURE}], + DEVICE_CLASS_TIMESTAMP: [{CONF_TYPE: CONF_IS_TIMESTAMP}], + DEVICE_CLASS_NONE: [{CONF_TYPE: CONF_IS_VALUE}], +} + +CONDITION_SCHEMA = vol.All( + cv.DEVICE_CONDITION_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In( + [ + CONF_IS_BATTERY_LEVEL, + CONF_IS_HUMIDITY, + CONF_IS_ILLUMINANCE, + CONF_IS_POWER, + CONF_IS_PRESSURE, + CONF_IS_SIGNAL_STRENGTH, + CONF_IS_TEMPERATURE, + CONF_IS_TIMESTAMP, + CONF_IS_VALUE, + ] + ), + vol.Optional(CONF_BELOW): vol.Any(vol.Coerce(float)), + vol.Optional(CONF_ABOVE): vol.Any(vol.Coerce(float)), + } + ), + cv.has_at_least_one_key(CONF_BELOW, CONF_ABOVE), +) + + +async def async_get_conditions( + hass: HomeAssistant, device_id: str +) -> List[Dict[str, str]]: + """List device conditions.""" + conditions: List[Dict[str, str]] = [] + entity_registry = await async_get_registry(hass) + entries = [ + entry + for entry in async_entries_for_device(entity_registry, device_id) + if entry.domain == DOMAIN + ] + + for entry in entries: + device_class = DEVICE_CLASS_NONE + state = hass.states.get(entry.entity_id) + unit_of_measurement = ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) if state else None + ) + + if not state or not unit_of_measurement: + continue + + if ATTR_DEVICE_CLASS in state.attributes: + device_class = state.attributes[ATTR_DEVICE_CLASS] + + templates = ENTITY_CONDITIONS.get( + device_class, ENTITY_CONDITIONS[DEVICE_CLASS_NONE] + ) + + conditions.extend( + ( + { + **template, + "condition": "device", + "device_id": device_id, + "entity_id": entry.entity_id, + "domain": DOMAIN, + } + for template in templates + ) + ) + + return conditions + + +def async_condition_from_config( + config: ConfigType, config_validation: bool +) -> condition.ConditionCheckerType: + """Evaluate state based on configuration.""" + if config_validation: + config = CONDITION_SCHEMA(config) + numeric_state_config = { + condition.CONF_CONDITION: "numeric_state", + condition.CONF_ENTITY_ID: config[CONF_ENTITY_ID], + } + if CONF_ABOVE in config: + numeric_state_config[condition.CONF_ABOVE] = config[CONF_ABOVE] + if CONF_BELOW in config: + numeric_state_config[condition.CONF_BELOW] = config[CONF_BELOW] + + return condition.async_numeric_state_from_config(numeric_state_config) + + +async def async_get_condition_capabilities(hass, config): + """List condition capabilities.""" + state = hass.states.get(config[CONF_ENTITY_ID]) + unit_of_measurement = ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) if state else None + ) + + if not state or not unit_of_measurement: + raise InvalidDeviceAutomationConfig + + return { + "extra_fields": vol.Schema( + { + vol.Optional( + CONF_ABOVE, description={"suffix": unit_of_measurement} + ): vol.Coerce(float), + vol.Optional( + CONF_BELOW, description={"suffix": unit_of_measurement} + ): vol.Coerce(float), + } + ) + } diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index 50fb1dd5c14..73e55340da9 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -3,6 +3,9 @@ import voluptuous as vol import homeassistant.components.automation.numeric_state as numeric_state_automation from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from homeassistant.components.device_automation.exceptions import ( + InvalidDeviceAutomationConfig, +) from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, @@ -72,11 +75,6 @@ TRIGGER_SCHEMA = vol.All( ), vol.Optional(CONF_BELOW): vol.Any(vol.Coerce(float)), vol.Optional(CONF_ABOVE): vol.Any(vol.Coerce(float)), - vol.Optional(CONF_FOR): vol.Any( - vol.All(cv.time_period, cv.positive_timedelta), - cv.template, - cv.template_complex, - ), vol.Optional(CONF_FOR): cv.positive_time_period_dict, } ), @@ -87,14 +85,17 @@ TRIGGER_SCHEMA = vol.All( async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" numeric_state_config = { + numeric_state_automation.CONF_PLATFORM: "numeric_state", numeric_state_automation.CONF_ENTITY_ID: config[CONF_ENTITY_ID], - numeric_state_automation.CONF_ABOVE: config.get(CONF_ABOVE), - numeric_state_automation.CONF_BELOW: config.get(CONF_BELOW), - numeric_state_automation.CONF_FOR: config.get(CONF_FOR), } + if CONF_ABOVE in config: + numeric_state_config[numeric_state_automation.CONF_ABOVE] = config[CONF_ABOVE] + if CONF_BELOW in config: + numeric_state_config[numeric_state_automation.CONF_BELOW] = config[CONF_BELOW] if CONF_FOR in config: numeric_state_config[CONF_FOR] = config[CONF_FOR] + numeric_state_config = numeric_state_automation.TRIGGER_SCHEMA(numeric_state_config) return await numeric_state_automation.async_attach_trigger( hass, numeric_state_config, action, automation_info, platform_type="device" ) @@ -121,7 +122,8 @@ async def async_get_triggers(hass, device_id): if not state or not unit_of_measurement: continue - device_class = state.attributes.get(ATTR_DEVICE_CLASS) + if ATTR_DEVICE_CLASS in state.attributes: + device_class = state.attributes[ATTR_DEVICE_CLASS] templates = ENTITY_TRIGGERS.get( device_class, ENTITY_TRIGGERS[DEVICE_CLASS_NONE] @@ -143,13 +145,25 @@ async def async_get_triggers(hass, device_id): return triggers -async def async_get_trigger_capabilities(hass, trigger): +async def async_get_trigger_capabilities(hass, config): """List trigger capabilities.""" + state = hass.states.get(config[CONF_ENTITY_ID]) + unit_of_measurement = ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) if state else None + ) + + if not state or not unit_of_measurement: + raise InvalidDeviceAutomationConfig + return { "extra_fields": vol.Schema( { - vol.Optional(CONF_ABOVE): vol.Coerce(float), - vol.Optional(CONF_BELOW): vol.Coerce(float), + vol.Optional( + CONF_ABOVE, description={"suffix": unit_of_measurement} + ): vol.Coerce(float), + vol.Optional( + CONF_BELOW, description={"suffix": unit_of_measurement} + ): vol.Coerce(float), vol.Optional(CONF_FOR): cv.positive_time_period_dict, } ) diff --git a/homeassistant/components/sensor/strings.json b/homeassistant/components/sensor/strings.json index 7df239facde..a05f57f4584 100644 --- a/homeassistant/components/sensor/strings.json +++ b/homeassistant/components/sensor/strings.json @@ -1,26 +1,26 @@ { "device_automation": { "condition_type": { - "is_battery_level": "{entity_name} battery level", - "is_humidity": "{entity_name} humidity", - "is_illuminance": "{entity_name} illuminance", - "is_power": "{entity_name} power", - "is_pressure": "{entity_name} pressure", - "is_signal_strength": "{entity_name} signal strength", - "is_temperature": "{entity_name} temperature", - "is_timestamp": "{entity_name} timestamp", - "is_value": "{entity_name} value" + "is_battery_level": "Current {entity_name} battery level", + "is_humidity": "Current {entity_name} humidity", + "is_illuminance": "Current {entity_name} illuminance", + "is_power": "Current {entity_name} power", + "is_pressure": "Current {entity_name} pressure", + "is_signal_strength": "Current {entity_name} signal strength", + "is_temperature": "Current {entity_name} temperature", + "is_timestamp": "Current {entity_name} timestamp", + "is_value": "Current {entity_name} value" }, "trigger_type": { - "battery_level": "{entity_name} battery level", - "humidity": "{entity_name} humidity", - "illuminance": "{entity_name} illuminance", - "power": "{entity_name} power", - "pressure": "{entity_name} pressure", - "signal_strength": "{entity_name} signal strength", - "temperature": "{entity_name} temperature", - "timestamp": "{entity_name} timestamp", - "value": "{entity_name} value" + "battery_level": "{entity_name} battery level changes", + "humidity": "{entity_name} humidity changes", + "illuminance": "{entity_name} illuminance changes", + "power": "{entity_name} power changes", + "pressure": "{entity_name} pressure changes", + "signal_strength": "{entity_name} signal strength changes", + "temperature": "{entity_name} temperature changes", + "timestamp": "{entity_name} timestamp changes", + "value": "{entity_name} value changes" } } } diff --git a/homeassistant/components/serial/sensor.py b/homeassistant/components/serial/sensor.py index 27775b8c702..a08f9522c4b 100644 --- a/homeassistant/components/serial/sensor.py +++ b/homeassistant/components/serial/sensor.py @@ -1,12 +1,13 @@ """Support for reading data from a serial port.""" -import logging import json +import logging +import serial_asyncio 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_VALUE_TEMPLATE, EVENT_HOMEASSISTANT_STOP +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -64,8 +65,6 @@ class SerialSensor(Entity): async def serial_read(self, device, rate, **kwargs): """Read the data from the port.""" - import serial_asyncio - reader, _ = await serial_asyncio.open_serial_connection( url=device, baudrate=rate, **kwargs ) diff --git a/homeassistant/components/sesame/lock.py b/homeassistant/components/sesame/lock.py index f8698ac6bd8..fa12ff7a1b2 100644 --- a/homeassistant/components/sesame/lock.py +++ b/homeassistant/components/sesame/lock.py @@ -1,15 +1,17 @@ """Support for Sesame, by CANDY HOUSE.""" from typing import Callable + +import pysesame2 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_BATTERY_LEVEL, CONF_API_KEY, STATE_LOCKED, STATE_UNLOCKED, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType ATTR_DEVICE_ID = "device_id" @@ -22,8 +24,6 @@ def setup_platform( hass, config: ConfigType, add_entities: Callable[[list], None], discovery_info=None ): """Set up the Sesame platform.""" - import pysesame2 - api_key = config.get(CONF_API_KEY) add_entities( diff --git a/homeassistant/components/seven_segments/image_processing.py b/homeassistant/components/seven_segments/image_processing.py index 4b96cc50ecc..315b5c39fec 100644 --- a/homeassistant/components/seven_segments/image_processing.py +++ b/homeassistant/components/seven_segments/image_processing.py @@ -1,19 +1,21 @@ """Optical character recognition processing of seven segments displays.""" -import logging import io +import logging import os +import subprocess +from PIL import Image import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.core import split_entity_id from homeassistant.components.image_processing import ( - PLATFORM_SCHEMA, - ImageProcessingEntity, - CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME, + CONF_SOURCE, + PLATFORM_SCHEMA, + ImageProcessingEntity, ) +from homeassistant.core import split_entity_id +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -120,9 +122,6 @@ class ImageProcessingSsocr(ImageProcessingEntity): def process_image(self, image): """Process the image.""" - from PIL import Image - import subprocess - stream = io.BytesIO(image) img = Image.open(stream) img.save(self.filepath, "png") diff --git a/homeassistant/components/shiftr/__init__.py b/homeassistant/components/shiftr/__init__.py index 8e698d283cf..1c3cddac256 100644 --- a/homeassistant/components/shiftr/__init__.py +++ b/homeassistant/components/shiftr/__init__.py @@ -1,16 +1,17 @@ """Support for Shiftr.io.""" import logging +import paho.mqtt.client as mqtt import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( CONF_PASSWORD, CONF_USERNAME, - EVENT_STATE_CHANGED, EVENT_HOMEASSISTANT_STOP, + EVENT_STATE_CHANGED, ) from homeassistant.helpers import state as state_helper +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -33,8 +34,6 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): """Initialize the Shiftr.io MQTT consumer.""" - import paho.mqtt.client as mqtt - conf = config[DOMAIN] username = conf.get(CONF_USERNAME) password = conf.get(CONF_PASSWORD) diff --git a/homeassistant/components/shodan/sensor.py b/homeassistant/components/shodan/sensor.py index 7b1360b0b01..d2a6a28fbe4 100644 --- a/homeassistant/components/shodan/sensor.py +++ b/homeassistant/components/shodan/sensor.py @@ -1,12 +1,13 @@ """Sensor for displaying the number of result on Shodan.io.""" -import logging from datetime import timedelta +import logging +import shodan 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_NAME +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -32,8 +33,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Shodan sensor.""" - import shodan - api_key = config.get(CONF_API_KEY) name = config.get(CONF_NAME) query = config.get(CONF_QUERY) diff --git a/homeassistant/components/shopping_list/__init__.py b/homeassistant/components/shopping_list/__init__.py index 3c9cb4391a7..a5e901b8c6e 100644 --- a/homeassistant/components/shopping_list/__init__.py +++ b/homeassistant/components/shopping_list/__init__.py @@ -101,13 +101,6 @@ def async_setup(hass, config): hass.http.register_view(UpdateShoppingListItemView) hass.http.register_view(ClearCompletedItemsView) - hass.components.conversation.async_register( - INTENT_ADD_ITEM, ["Add [the] [a] [an] {item} to my shopping list"] - ) - hass.components.conversation.async_register( - INTENT_LAST_ITEMS, ["What is on my shopping list"] - ) - hass.components.frontend.async_register_built_in_panel( "shopping-list", "shopping_list", "mdi:cart" ) diff --git a/homeassistant/components/simplisafe/.translations/ru.json b/homeassistant/components/simplisafe/.translations/ru.json index e82172f92f8..721ba69d67e 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\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435." }, "step": { "user": { diff --git a/homeassistant/components/sinch/__init__.py b/homeassistant/components/sinch/__init__.py new file mode 100644 index 00000000000..43a5f2b2a5c --- /dev/null +++ b/homeassistant/components/sinch/__init__.py @@ -0,0 +1 @@ +"""Component to integrate with Sinch SMS API.""" diff --git a/homeassistant/components/sinch/manifest.json b/homeassistant/components/sinch/manifest.json new file mode 100644 index 00000000000..a1864428fee --- /dev/null +++ b/homeassistant/components/sinch/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "sinch", + "name": "Sinch", + "documentation": "https://www.home-assistant.io/components/sinch", + "dependencies": [], + "codeowners": [ + "@bendikrb" + ], + "requirements": [ + "clx-sdk-xms==1.0.0" + ] +} \ No newline at end of file diff --git a/homeassistant/components/sinch/notify.py b/homeassistant/components/sinch/notify.py new file mode 100644 index 00000000000..173873c0a6c --- /dev/null +++ b/homeassistant/components/sinch/notify.py @@ -0,0 +1,97 @@ +"""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, +) + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.notify import ( + ATTR_MESSAGE, + ATTR_DATA, + ATTR_TARGET, + PLATFORM_SCHEMA, + BaseNotificationService, +) +from homeassistant.const import CONF_API_KEY, CONF_SENDER + +DOMAIN = "sinch" + +CONF_SERVICE_PLAN_ID = "service_plan_id" +CONF_DEFAULT_RECIPIENTS = "default_recipients" + +ATTR_SENDER = CONF_SENDER + +DEFAULT_SENDER = "Home Assistant" + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_SERVICE_PLAN_ID): cv.string, + vol.Optional(CONF_SENDER, default=DEFAULT_SENDER): cv.string, + vol.Optional(CONF_DEFAULT_RECIPIENTS, default=[]): vol.All( + cv.ensure_list, [cv.string] + ), + } +) + + +def get_service(hass, config, discovery_info=None): + """Get the Sinch notification service.""" + return SinchNotificationService(config) + + +class SinchNotificationService(BaseNotificationService): + """Send Notifications to Sinch SMS recipients.""" + + def __init__(self, config): + """Initialize the service.""" + self.default_recipients = config[CONF_DEFAULT_RECIPIENTS] + self.sender = config[CONF_SENDER] + self.client = Client(config[CONF_SERVICE_PLAN_ID], config[CONF_API_KEY]) + + 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, {}) + + clx_args = {ATTR_MESSAGE: message, ATTR_SENDER: self.sender} + + if ATTR_SENDER in data: + clx_args[ATTR_SENDER] = data[ATTR_SENDER] + + if not targets: + _LOGGER.error("At least 1 target is required") + return + + try: + for target in targets: + result: MtBatchTextSmsResult = self.client.create_text_message( + clx_args[ATTR_SENDER], target, clx_args[ATTR_MESSAGE] + ) + batch_id = result.batch_id + _LOGGER.debug( + 'Successfully sent SMS to "%s" (batch_id: %s)', target, batch_id + ) + except ErrorResponseException as ex: + _LOGGER.error( + "Caught ErrorResponseException. Response code: %d (%s)", + ex.error_code, + ex, + ) + except NotFoundException as ex: + _LOGGER.error("Caught NotFoundException (request URL: %s)", ex.url) + except UnauthorizedException as ex: + _LOGGER.error( + "Caught UnauthorizedException (service plan: %s)", ex.service_plan_id + ) + except UnexpectedResponseException as ex: + _LOGGER.error("Caught UnexpectedResponseException: %s", ex) diff --git a/homeassistant/components/skybeacon/manifest.json b/homeassistant/components/skybeacon/manifest.json index a3cb97cdc2d..7ab42c5da87 100644 --- a/homeassistant/components/skybeacon/manifest.json +++ b/homeassistant/components/skybeacon/manifest.json @@ -3,7 +3,7 @@ "name": "Skybeacon", "documentation": "https://www.home-assistant.io/integrations/skybeacon", "requirements": [ - "pygatt[GATTTOOL]==4.0.1" + "pygatt[GATTTOOL]==4.0.5" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/skybeacon/sensor.py b/homeassistant/components/skybeacon/sensor.py index 1c098409610..cbf394edf47 100644 --- a/homeassistant/components/skybeacon/sensor.py +++ b/homeassistant/components/skybeacon/sensor.py @@ -3,6 +3,9 @@ import logging import threading from uuid import UUID +from pygatt import BLEAddressType +from pygatt.backends import Characteristic, GATTToolBackend +from pygatt.exceptions import BLEError, NotConnectedError, NotificationTimeout import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -132,13 +135,8 @@ class Monitor(threading.Thread): def run(self): """Thread that keeps connection alive.""" - # pylint: disable=import-error - import pygatt - from pygatt.backends import Characteristic - from pygatt.exceptions import BLEError, NotConnectedError, NotificationTimeout - cached_char = Characteristic(BLE_TEMP_UUID, BLE_TEMP_HANDLE) - adapter = pygatt.backends.GATTToolBackend() + adapter = GATTToolBackend() while True: try: _LOGGER.debug("Connecting to %s", self.name) @@ -147,7 +145,7 @@ class Monitor(threading.Thread): # Seems only one connection can be initiated at a time with CONNECT_LOCK: device = adapter.connect( - self.mac, CONNECT_TIMEOUT, pygatt.BLEAddressType.random + self.mac, CONNECT_TIMEOUT, BLEAddressType.random ) if SKIP_HANDLE_LOOKUP: # HACK: inject handle mapping collected offline diff --git a/homeassistant/components/slack/notify.py b/homeassistant/components/slack/notify.py index 1b9895aab76..b645a590c3c 100644 --- a/homeassistant/components/slack/notify.py +++ b/homeassistant/components/slack/notify.py @@ -3,11 +3,10 @@ import logging import requests from requests.auth import HTTPBasicAuth, HTTPDigestAuth +import slacker +from slacker import Slacker import voluptuous as vol -from homeassistant.const import CONF_API_KEY, CONF_ICON, CONF_USERNAME -import homeassistant.helpers.config_validation as cv - from homeassistant.components.notify import ( ATTR_DATA, ATTR_TARGET, @@ -15,6 +14,8 @@ from homeassistant.components.notify import ( PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.const import CONF_API_KEY, CONF_ICON, CONF_USERNAME +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -45,7 +46,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def get_service(hass, config, discovery_info=None): """Get the Slack notification service.""" - import slacker channel = config.get(CONF_CHANNEL) api_key = config.get(CONF_API_KEY) @@ -67,7 +67,6 @@ class SlackNotificationService(BaseNotificationService): def __init__(self, default_channel, api_token, username, icon, is_allowed_path): """Initialize the service.""" - from slacker import Slacker self._default_channel = default_channel self._api_token = api_token @@ -84,7 +83,6 @@ class SlackNotificationService(BaseNotificationService): def send_message(self, message="", **kwargs): """Send a message to a user.""" - import slacker if kwargs.get(ATTR_TARGET) is None: targets = [self._default_channel] diff --git a/homeassistant/components/sma/sensor.py b/homeassistant/components/sma/sensor.py index 56e10b03d2a..ff1c48a141d 100644 --- a/homeassistant/components/sma/sensor.py +++ b/homeassistant/components/sma/sensor.py @@ -3,17 +3,18 @@ import asyncio from datetime import timedelta import logging +import pysma import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, + CONF_PATH, CONF_SCAN_INTERVAL, CONF_SSL, CONF_VERIFY_SSL, EVENT_HOMEASSISTANT_STOP, - CONF_PATH, ) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -35,8 +36,6 @@ GROUPS = ["user", "installer"] def _check_sensor_schema(conf): """Check sensors and attributes are valid.""" try: - import pysma - valid = [s.name for s in pysma.Sensors()] except (ImportError, AttributeError): return conf @@ -87,7 +86,6 @@ PLATFORM_SCHEMA = vol.All( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up SMA WebConnect sensor.""" - import pysma # Check config again during load - dependency available config = _check_sensor_schema(config) diff --git a/homeassistant/components/smappee/__init__.py b/homeassistant/components/smappee/__init__.py index 0da0b29fbc2..ecab09f6ff9 100644 --- a/homeassistant/components/smappee/__init__.py +++ b/homeassistant/components/smappee/__init__.py @@ -1,13 +1,16 @@ """Support for Smappee energy monitor.""" -import logging from datetime import datetime, timedelta +import logging import re -import voluptuous as vol + from requests.exceptions import RequestException -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_HOST -from homeassistant.util import Throttle -from homeassistant.helpers.discovery import load_platform +import smappy +import voluptuous as vol + +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.discovery import load_platform +from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -72,7 +75,6 @@ class Smappee: self, client_id, client_secret, username, password, host, host_password ): """Initialize the data.""" - import smappy self._remote_active = False self._local_active = False diff --git a/homeassistant/components/smarthab/__init__.py b/homeassistant/components/smarthab/__init__.py index 7206bea110b..ef2da4e9a1d 100644 --- a/homeassistant/components/smarthab/__init__.py +++ b/homeassistant/components/smarthab/__init__.py @@ -6,11 +6,12 @@ https://home-assistant.io/integrations/smarthab/ """ import logging +import pysmarthab import voluptuous as vol -from homeassistant.helpers.discovery import load_platform from homeassistant.const import CONF_EMAIL, CONF_PASSWORD import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.discovery import load_platform DOMAIN = "smarthab" DATA_HUB = "hub" @@ -32,7 +33,6 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config) -> bool: """Set up the SmartHab platform.""" - import pysmarthab sh_conf = config.get(DOMAIN) diff --git a/homeassistant/components/smarthab/cover.py b/homeassistant/components/smarthab/cover.py index 3d5b4259aa9..9bcb89b7ab4 100644 --- a/homeassistant/components/smarthab/cover.py +++ b/homeassistant/components/smarthab/cover.py @@ -4,18 +4,21 @@ Support for SmartHab device integration. For more details about this component, please refer to the documentation at https://home-assistant.io/integrations/smarthab/ """ -import logging from datetime import timedelta +import logging + +import pysmarthab from requests.exceptions import Timeout from homeassistant.components.cover import ( - CoverDevice, - SUPPORT_OPEN, - SUPPORT_CLOSE, - SUPPORT_SET_POSITION, ATTR_POSITION, + SUPPORT_CLOSE, + SUPPORT_OPEN, + SUPPORT_SET_POSITION, + CoverDevice, ) -from . import DOMAIN, DATA_HUB + +from . import DATA_HUB, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -24,7 +27,6 @@ SCAN_INTERVAL = timedelta(seconds=60) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the SmartHab roller shutters platform.""" - import pysmarthab hub = hass.data[DOMAIN][DATA_HUB] devices = hub.get_device_list() diff --git a/homeassistant/components/smarthab/light.py b/homeassistant/components/smarthab/light.py index a8a55dea48a..bc6eb31fd04 100644 --- a/homeassistant/components/smarthab/light.py +++ b/homeassistant/components/smarthab/light.py @@ -4,12 +4,15 @@ Support for SmartHab device integration. For more details about this component, please refer to the documentation at https://home-assistant.io/integrations/smarthab/ """ -import logging from datetime import timedelta +import logging + +import pysmarthab from requests.exceptions import Timeout from homeassistant.components.light import Light -from . import DOMAIN, DATA_HUB + +from . import DATA_HUB, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -18,7 +21,6 @@ SCAN_INTERVAL = timedelta(seconds=60) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the SmartHab lights platform.""" - import pysmarthab hub = hass.data[DOMAIN][DATA_HUB] devices = hub.get_device_list() diff --git a/homeassistant/components/smartthings/.translations/nn.json b/homeassistant/components/smartthings/.translations/nn.json new file mode 100644 index 00000000000..929e95dc2ff --- /dev/null +++ b/homeassistant/components/smartthings/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "SmartThings" + } +} \ No newline at end of file diff --git a/homeassistant/components/smartthings/.translations/pl.json b/homeassistant/components/smartthings/.translations/pl.json index 33803994764..849ad174134 100644 --- a/homeassistant/components/smartthings/.translations/pl.json +++ b/homeassistant/components/smartthings/.translations/pl.json @@ -2,7 +2,7 @@ "config": { "error": { "app_not_installed": "Upewnij si\u0119, \u017ce zainstalowa\u0142e\u015b i autoryzowa\u0142e\u015b Home Assistant SmartApp i spr\u00f3buj ponownie.", - "app_setup_error": "Nie mo\u017cna skonfigurowa\u0107 SmartApp. Prosz\u0119 spr\u00f3buj ponownie.", + "app_setup_error": "Nie mo\u017cna skonfigurowa\u0107 SmartApp. Spr\u00f3buj ponownie.", "base_url_not_https": "Parametr `base_url` dla komponentu `http` musi by\u0107 skonfigurowany i rozpoczyna\u0107 si\u0119 od `https://`.", "token_already_setup": "Token zosta\u0142 ju\u017c skonfigurowany.", "token_forbidden": "Token nie ma wymaganych zakres\u00f3w OAuth.", diff --git a/homeassistant/components/smartthings/.translations/ru.json b/homeassistant/components/smartthings/.translations/ru.json index 575c593d5a4..f07586c16e3 100644 --- a/homeassistant/components/smartthings/.translations/ru.json +++ b/homeassistant/components/smartthings/.translations/ru.json @@ -6,7 +6,7 @@ "base_url_not_https": "\u0412 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0435 `http` \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0443\u043a\u0430\u0437\u0430\u043d \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 `base_url`, \u043d\u0430\u0447\u0438\u043d\u0430\u044e\u0449\u0438\u0439\u0441\u044f \u0441 `https://`.", "token_already_setup": "\u0422\u043e\u043a\u0435\u043d \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d.", "token_forbidden": "\u0422\u043e\u043a\u0435\u043d \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d \u0434\u043b\u044f OAuth.", - "token_invalid_format": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 UID / GUID", + "token_invalid_format": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 UID / GUID.", "token_unauthorized": "\u0422\u043e\u043a\u0435\u043d \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d \u0438\u043b\u0438 \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u043d.", "webhook_error": "SmartThings \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u043a\u043e\u043d\u0435\u0447\u043d\u0443\u044e \u0442\u043e\u0447\u043a\u0443, \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u0443\u044e \u0432 `base_url`. \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\u0435\u0439 \u043a \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0443." }, diff --git a/homeassistant/components/smartthings/smartapp.py b/homeassistant/components/smartthings/smartapp.py index d205c1d245c..ecd4da5dcab 100644 --- a/homeassistant/components/smartthings/smartapp.py +++ b/homeassistant/components/smartthings/smartapp.py @@ -22,7 +22,7 @@ from pysmartthings import ( SubscriptionEntity, ) -from homeassistant.components import cloud, webhook +from homeassistant.components import webhook from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import ( @@ -88,7 +88,10 @@ 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.async_active_subscription(hass): + if ( + "cloud" in hass.config.components + and hass.components.cloud.async_active_subscription() + ): return True if hass.data[DOMAIN][CONF_CLOUDHOOK_URL] is not None: return True @@ -102,7 +105,11 @@ 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.async_active_subscription(hass) and cloudhook_url is not None: + if ( + "cloud" in hass.config.components + and 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]) @@ -222,10 +229,11 @@ async def setup_smartapp_endpoint(hass: HomeAssistantType): cloudhook_url = config.get(CONF_CLOUDHOOK_URL) if ( cloudhook_url is None - and cloud.async_active_subscription(hass) + and "cloud" in hass.config.components + and hass.components.cloud.async_active_subscription() and not hass.config_entries.async_entries(DOMAIN) ): - cloudhook_url = await cloud.async_create_cloudhook( + cloudhook_url = await hass.components.cloud.async_create_cloudhook( hass, config[CONF_WEBHOOK_ID] ) config[CONF_CLOUDHOOK_URL] = cloudhook_url @@ -273,8 +281,14 @@ 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.async_is_logged_in(hass): - await cloud.async_delete_cloudhook(hass, hass.data[DOMAIN][CONF_WEBHOOK_ID]) + if ( + cloudhook_url + and "cloud" in hass.config.components + and hass.components.cloud.async_is_logged_in() + ): + await hass.components.cloud.async_delete_cloudhook( + hass, hass.data[DOMAIN][CONF_WEBHOOK_ID] + ) # Remove cloudhook from storage store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) await store.async_save( diff --git a/homeassistant/components/smhi/.translations/ru.json b/homeassistant/components/smhi/.translations/ru.json index 03b17b3ba8b..f3ba34adac3 100644 --- a/homeassistant/components/smhi/.translations/ru.json +++ b/homeassistant/components/smhi/.translations/ru.json @@ -2,7 +2,7 @@ "config": { "error": { "name_exists": "\u042d\u0442\u043e \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f.", - "wrong_location": "\u0422\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u0428\u0432\u0435\u0446\u0438\u0438" + "wrong_location": "\u0422\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u0428\u0432\u0435\u0446\u0438\u0438." }, "step": { "user": { diff --git a/homeassistant/components/smtp/notify.py b/homeassistant/components/smtp/notify.py index 8a96865ab8d..d592f25a61d 100644 --- a/homeassistant/components/smtp/notify.py +++ b/homeassistant/components/smtp/notify.py @@ -136,16 +136,15 @@ class MailNotificationService(BaseNotificationService): server = None try: server = self.connect() - except smtplib.socket.gaierror: + except (smtplib.socket.gaierror, ConnectionRefusedError): _LOGGER.exception( - "SMTP server not found (%s:%s). " - "Please check the IP address or hostname of your SMTP server", + "SMTP server not found or refused connection (%s:%s). " + "Please check the IP address, hostname, and availability of your SMTP server.", self._server, self._port, ) - return False - except (smtplib.SMTPAuthenticationError, ConnectionRefusedError): + except smtplib.SMTPAuthenticationError: _LOGGER.exception( "Login not possible. " "Please check your setting and/or your credentials" diff --git a/homeassistant/components/snapcast/__init__.py b/homeassistant/components/snapcast/__init__.py index 9e41bd8ff38..e6c574b7b2b 100644 --- a/homeassistant/components/snapcast/__init__.py +++ b/homeassistant/components/snapcast/__init__.py @@ -1,6 +1,7 @@ """The snapcast component.""" import asyncio + import voluptuous as vol from homeassistant.const import ATTR_ENTITY_ID diff --git a/homeassistant/components/snapcast/media_player.py b/homeassistant/components/snapcast/media_player.py index 81cd6538578..c3c9138eb89 100644 --- a/homeassistant/components/snapcast/media_player.py +++ b/homeassistant/components/snapcast/media_player.py @@ -2,9 +2,11 @@ import logging import socket +import snapcast.control +from snapcast.control.server import CONTROL_PORT 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_VOLUME_MUTE, @@ -24,12 +26,12 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import ( - DOMAIN, - SERVICE_SNAPSHOT, - SERVICE_RESTORE, - SERVICE_JOIN, - SERVICE_UNJOIN, ATTR_MASTER, + DOMAIN, + SERVICE_JOIN, + SERVICE_RESTORE, + SERVICE_SNAPSHOT, + SERVICE_UNJOIN, ) _LOGGER = logging.getLogger(__name__) @@ -55,8 +57,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Snapcast platform.""" - import snapcast.control - from snapcast.control.server import CONTROL_PORT host = config.get(CONF_HOST) port = config.get(CONF_PORT, CONTROL_PORT) diff --git a/homeassistant/components/snmp/device_tracker.py b/homeassistant/components/snmp/device_tracker.py index a628c426e0f..eafae9537e5 100644 --- a/homeassistant/components/snmp/device_tracker.py +++ b/homeassistant/components/snmp/device_tracker.py @@ -2,6 +2,8 @@ import binascii import logging +from pysnmp.entity import config as cfg +from pysnmp.entity.rfc3413.oneliner import cmdgen import voluptuous as vol from homeassistant.components.device_tracker import ( @@ -45,8 +47,6 @@ class SnmpScanner(DeviceScanner): def __init__(self, config): """Initialize the scanner.""" - from pysnmp.entity.rfc3413.oneliner import cmdgen - from pysnmp.entity import config as cfg self.snmp = cmdgen.CommandGenerator() diff --git a/homeassistant/components/snmp/sensor.py b/homeassistant/components/snmp/sensor.py index 5e6b5ed1f28..b369ec83c58 100644 --- a/homeassistant/components/snmp/sensor.py +++ b/homeassistant/components/snmp/sensor.py @@ -2,6 +2,17 @@ from datetime import timedelta import logging +import pysnmp.hlapi.asyncio as hlapi +from pysnmp.hlapi.asyncio import ( + CommunityData, + ContextData, + ObjectIdentity, + ObjectType, + SnmpEngine, + UdpTransportTarget, + UsmUserData, + getCmd, +) import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -70,16 +81,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the SNMP sensor.""" - from pysnmp.hlapi.asyncio import ( - getCmd, - CommunityData, - SnmpEngine, - UdpTransportTarget, - ContextData, - ObjectType, - ObjectIdentity, - UsmUserData, - ) name = config.get(CONF_NAME) host = config.get(CONF_HOST) @@ -101,7 +102,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= value_template.hass = hass if version == "3": - import pysnmp.hlapi.asyncio as hlapi if not authkey: authproto = "none" @@ -194,7 +194,6 @@ class SnmpData: async def async_update(self): """Get the latest data from the remote SNMP capable host.""" - from pysnmp.hlapi.asyncio import getCmd, ObjectType, ObjectIdentity errindication, errstatus, errindex, restable = await getCmd( *self._request_args, ObjectType(ObjectIdentity(self._baseoid)) diff --git a/homeassistant/components/snmp/switch.py b/homeassistant/components/snmp/switch.py index 95496cb6a45..aac43208a1f 100644 --- a/homeassistant/components/snmp/switch.py +++ b/homeassistant/components/snmp/switch.py @@ -1,6 +1,18 @@ """Support for SNMP enabled switch.""" import logging +import pysnmp.hlapi.asyncio as hlapi +from pysnmp.hlapi.asyncio import ( + CommunityData, + ContextData, + ObjectIdentity, + ObjectType, + SnmpEngine, + UdpTransportTarget, + UsmUserData, + getCmd, + setCmd, +) import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice @@ -136,13 +148,6 @@ class SnmpSwitch(SwitchDevice): command_payload_off, ): """Initialize the switch.""" - from pysnmp.hlapi.asyncio import ( - CommunityData, - ContextData, - SnmpEngine, - UdpTransportTarget, - UsmUserData, - ) self._name = name self._baseoid = baseoid @@ -157,7 +162,6 @@ class SnmpSwitch(SwitchDevice): self._payload_off = payload_off if version == "3": - import pysnmp.hlapi.asyncio as hlapi if not authkey: authproto = "none" @@ -186,20 +190,14 @@ class SnmpSwitch(SwitchDevice): async def async_turn_on(self, **kwargs): """Turn on the switch.""" - from pyasn1.type.univ import Integer - - await self._set(Integer(self._command_payload_on)) + await self._set(self._command_payload_on) async def async_turn_off(self, **kwargs): """Turn off the switch.""" - from pyasn1.type.univ import Integer - - await self._set(Integer(self._command_payload_off)) + await self._set(self._command_payload_off) async def async_update(self): """Update the state.""" - from pysnmp.hlapi.asyncio import getCmd, ObjectType, ObjectIdentity - from pyasn1.type.univ import Integer errindication, errstatus, errindex, restable = await getCmd( *self._request_args, ObjectType(ObjectIdentity(self._baseoid)) @@ -215,9 +213,9 @@ class SnmpSwitch(SwitchDevice): ) else: for resrow in restable: - if resrow[-1] == Integer(self._payload_on): + if resrow[-1] == self._payload_on: self._state = True - elif resrow[-1] == Integer(self._payload_off): + elif resrow[-1] == self._payload_off: self._state = False else: self._state = None @@ -233,7 +231,6 @@ class SnmpSwitch(SwitchDevice): return self._state async def _set(self, value): - from pysnmp.hlapi.asyncio import setCmd, ObjectType, ObjectIdentity await setCmd( *self._request_args, ObjectType(ObjectIdentity(self._commandoid), value) diff --git a/homeassistant/components/socialblade/sensor.py b/homeassistant/components/socialblade/sensor.py index 0acfb63a629..3d53e76a27a 100644 --- a/homeassistant/components/socialblade/sensor.py +++ b/homeassistant/components/socialblade/sensor.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +import socialbladeclient import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -71,7 +72,6 @@ class SocialBladeSensor(Entity): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data from Social Blade.""" - import socialbladeclient try: data = socialbladeclient.get_data(self.channel_id) diff --git a/homeassistant/components/solaredge/.translations/de.json b/homeassistant/components/solaredge/.translations/de.json new file mode 100644 index 00000000000..cbe913e131c --- /dev/null +++ b/homeassistant/components/solaredge/.translations/de.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "site_exists": "Diese site_id ist bereits konfiguriert" + }, + "error": { + "site_exists": "Diese site_id ist bereits konfiguriert" + }, + "step": { + "user": { + "data": { + "api_key": "Der API-Schl\u00fcssel f\u00fcr diese Site", + "name": "Der Name dieser Installation", + "site_id": "Die SolarEdge-Site-ID" + }, + "title": "Definiere die API-Parameter f\u00fcr diese Installation" + } + }, + "title": "SolarEdge" + } +} \ No newline at end of file diff --git a/homeassistant/components/solaredge/.translations/ru.json b/homeassistant/components/solaredge/.translations/ru.json index d8622cdd2c1..e6e7094648d 100644 --- a/homeassistant/components/solaredge/.translations/ru.json +++ b/homeassistant/components/solaredge/.translations/ru.json @@ -9,9 +9,9 @@ "step": { "user": { "data": { - "api_key": "\u041a\u043b\u044e\u0447 API \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0441\u0430\u0439\u0442\u0430", + "api_key": "\u041a\u043b\u044e\u0447 API", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", - "site_id": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0441\u0430\u0439\u0442\u0430 SolarEdge" + "site_id": "site-id" }, "title": "SolarEdge" } diff --git a/homeassistant/components/solaredge_local/sensor.py b/homeassistant/components/solaredge_local/sensor.py index 4fc62e44921..917fb86ddcb 100644 --- a/homeassistant/components/solaredge_local/sensor.py +++ b/homeassistant/components/solaredge_local/sensor.py @@ -2,6 +2,7 @@ import logging from datetime import timedelta import statistics +from copy import deepcopy from requests.exceptions import HTTPError, ConnectTimeout from solaredge_local import SolarEdge @@ -14,6 +15,7 @@ from homeassistant.const import ( POWER_WATT, ENERGY_WATT_HOUR, TEMP_CELSIUS, + TEMP_FAHRENHEIT, ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -22,63 +24,107 @@ from homeassistant.util import Throttle DOMAIN = "solaredge_local" UPDATE_DELAY = timedelta(seconds=10) +INVERTER_MODES = ( + "SHUTTING_DOWN", + "ERROR", + "STANDBY", + "PAIRING", + "POWER_PRODUCTION", + "AC_CHARGING", + "NOT_PAIRED", + "NIGHT_MODE", + "GRID_MONITORING", + "IDLE", +) + # Supported sensor types: -# Key: ['json_key', 'name', unit, icon] +# Key: ['json_key', 'name', unit, icon, attribute name] SENSOR_TYPES = { - "current_power": ["currentPower", "Current Power", POWER_WATT, "mdi:solar-power"], + "current_AC_voltage": ["gridvoltage", "Grid Voltage", "V", "mdi:current-ac", None], + "current_DC_voltage": ["dcvoltage", "DC Voltage", "V", "mdi:current-dc", None], + "current_frequency": [ + "gridfrequency", + "Grid Frequency", + "Hz", + "mdi:current-ac", + None, + ], + "current_power": [ + "currentPower", + "Current Power", + POWER_WATT, + "mdi:solar-power", + None, + ], "energy_this_month": [ "energyThisMonth", - "Energy this month", + "Energy This Month", ENERGY_WATT_HOUR, "mdi:solar-power", + None, ], "energy_this_year": [ "energyThisYear", - "Energy this year", + "Energy This Year", ENERGY_WATT_HOUR, "mdi:solar-power", + None, ], "energy_today": [ "energyToday", - "Energy today", + "Energy Today", ENERGY_WATT_HOUR, "mdi:solar-power", + None, ], "inverter_temperature": [ "invertertemperature", "Inverter Temperature", TEMP_CELSIUS, "mdi:thermometer", + "operating_mode", ], "lifetime_energy": [ "energyTotal", - "Lifetime energy", + "Lifetime Energy", ENERGY_WATT_HOUR, "mdi:solar-power", + None, + ], + "optimizer_connected": [ + "optimizers", + "Optimizers Online", + "optimizers", + "mdi:solar-panel", + "optimizers_connected", ], "optimizer_current": [ "optimizercurrent", - "Avrage Optimizer Current", + "Average Optimizer Current", "A", "mdi:solar-panel", + None, ], "optimizer_power": [ "optimizerpower", - "Avrage Optimizer Power", + "Average Optimizer Power", POWER_WATT, "mdi:solar-panel", + None, ], "optimizer_temperature": [ "optimizertemperature", - "Avrage Optimizer Temperature", + "Average Optimizer Temperature", TEMP_CELSIUS, "mdi:solar-panel", + None, ], "optimizer_voltage": [ "optimizervoltage", - "Avrage Optimizer Voltage", + "Average Optimizer Voltage", "V", "mdi:solar-panel", + None, ], } @@ -112,13 +158,71 @@ def setup_platform(hass, config, add_entities, discovery_info=None): _LOGGER.error("Could not retrieve details from SolarEdge API") return + # Changing inverter temperature unit. + sensors = deepcopy(SENSOR_TYPES) + if status.inverters.primary.temperature.units.farenheit: + sensors["inverter_temperature"] = [ + "invertertemperature", + "Inverter Temperature", + TEMP_FAHRENHEIT, + "mdi:thermometer", + "operating_mode", + None, + ] + + try: + if status.metersList[0]: + sensors["import_current_power"] = [ + "currentPowerimport", + "current import Power", + POWER_WATT, + "mdi:arrow-collapse-down", + None, + ] + sensors["import_meter_reading"] = [ + "totalEnergyimport", + "total import Energy", + ENERGY_WATT_HOUR, + "mdi:counter", + None, + ] + except IndexError: + _LOGGER.debug("Import meter sensors are not created") + + try: + if status.metersList[1]: + sensors["export_current_power"] = [ + "currentPowerexport", + "current export Power", + POWER_WATT, + "mdi:arrow-expand-up", + None, + ] + sensors["export_meter_reading"] = [ + "totalEnergyexport", + "total export Energy", + ENERGY_WATT_HOUR, + "mdi:counter", + None, + ] + except IndexError: + _LOGGER.debug("Export meter sensors are not created") + # Create solaredge data service which will retrieve and update the data. data = SolarEdgeData(hass, api) # Create a new sensor for each sensor type. entities = [] - for sensor_key in SENSOR_TYPES: - sensor = SolarEdgeSensor(platform_name, sensor_key, data) + for sensor_info in sensors.values(): + sensor = SolarEdgeSensor( + platform_name, + data, + sensor_info[0], + sensor_info[1], + sensor_info[2], + sensor_info[3], + sensor_info[4], + ) entities.append(sensor) add_entities(entities, True) @@ -127,30 +231,42 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class SolarEdgeSensor(Entity): """Representation of an SolarEdge Monitoring API sensor.""" - def __init__(self, platform_name, sensor_key, data): + def __init__(self, platform_name, data, json_key, name, unit, icon, attr): """Initialize the sensor.""" - self.platform_name = platform_name - self.sensor_key = sensor_key - self.data = data + self._platform_name = platform_name + self._data = data self._state = None - self._json_key = SENSOR_TYPES[self.sensor_key][0] - self._unit_of_measurement = SENSOR_TYPES[self.sensor_key][2] + self._json_key = json_key + self._name = name + self._unit_of_measurement = unit + self._icon = icon + self._attr = attr @property def name(self): """Return the name.""" - return f"{self.platform_name} ({SENSOR_TYPES[self.sensor_key][1]})" + return f"{self._platform_name} ({self._name})" @property def unit_of_measurement(self): """Return the unit of measurement.""" return self._unit_of_measurement + @property + def device_state_attributes(self): + """Return the state attributes.""" + if self._attr: + try: + return {self._attr: self._data.info[self._json_key]} + except KeyError: + return None + return None + @property def icon(self): """Return the sensor icon.""" - return SENSOR_TYPES[self.sensor_key][3] + return self._icon @property def state(self): @@ -159,8 +275,8 @@ class SolarEdgeSensor(Entity): def update(self): """Get the latest data from the sensor and update the state.""" - self.data.update() - self._state = self.data.data[self._json_key] + self._data.update() + self._state = self._data.data[self._json_key] class SolarEdgeData: @@ -171,6 +287,7 @@ class SolarEdgeData: self.hass = hass self.api = api self.data = {} + self.info = {} @Throttle(UPDATE_DELAY) def update(self): @@ -220,11 +337,33 @@ class SolarEdgeData: self.data["energyThisMonth"] = round(status.energy.thisMonth, 2) self.data["energyToday"] = round(status.energy.today, 2) self.data["currentPower"] = round(status.powerWatt, 2) - self.data[ - "invertertemperature" - ] = status.inverters.primary.temperature.value + self.data["invertertemperature"] = round( + status.inverters.primary.temperature.value, 2 + ) + self.data["dcvoltage"] = round(status.inverters.primary.voltage, 2) + self.data["gridfrequency"] = round(status.frequencyHz, 2) + self.data["gridvoltage"] = round(status.voltage, 2) + self.data["optimizers"] = status.optimizersStatus.online + + self.info["optimizers"] = status.optimizersStatus.total + self.info["invertertemperature"] = INVERTER_MODES[status.status] + + try: + if status.metersList[1]: + self.data["currentPowerimport"] = status.metersList[1].currentPower + self.data["totalEnergyimport"] = status.metersList[1].totalEnergy + except IndexError: + pass + + try: + if status.metersList[0]: + self.data["currentPowerexport"] = status.metersList[0].currentPower + self.data["totalEnergyexport"] = status.metersList[0].totalEnergy + except IndexError: + pass + if maintenance.system.name: - self.data["optimizertemperature"] = statistics.mean(temperature) - self.data["optimizervoltage"] = statistics.mean(voltage) - self.data["optimizercurrent"] = statistics.mean(current) - self.data["optimizerpower"] = power + self.data["optimizertemperature"] = round(statistics.mean(temperature), 2) + self.data["optimizervoltage"] = round(statistics.mean(voltage), 2) + self.data["optimizercurrent"] = round(statistics.mean(current), 2) + self.data["optimizerpower"] = round(power, 2) diff --git a/homeassistant/components/solarlog/.translations/ca.json b/homeassistant/components/solarlog/.translations/ca.json new file mode 100644 index 00000000000..6a041c7ea4f --- /dev/null +++ b/homeassistant/components/solarlog/.translations/ca.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "cannot_connect": "No s'ha pogut connectar, verifica l'adre\u00e7a de l'amfitri\u00f3" + }, + "step": { + "user": { + "data": { + "host": "Nom de l'amfitri\u00f3 o adre\u00e7a IP del dispositiu Solar-Log", + "name": "Prefix utilitzat pels sensors de Solar-Log" + }, + "title": "Configuraci\u00f3 de la connexi\u00f3 amb Solar-Log" + } + }, + "title": "Solar-Log" + } +} \ No newline at end of file diff --git a/homeassistant/components/solarlog/.translations/da.json b/homeassistant/components/solarlog/.translations/da.json new file mode 100644 index 00000000000..a344832c61c --- /dev/null +++ b/homeassistant/components/solarlog/.translations/da.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Enheden er allerede konfigureret" + }, + "error": { + "already_configured": "Enheden er allerede konfigureret", + "cannot_connect": "Kunne ikke oprette forbindelse, verificer v\u00e6rtsadressen" + }, + "step": { + "user": { + "data": { + "host": "V\u00e6rtsnavnet eller ip-adressen p\u00e5 din Solar-Log-enhed", + "name": "Pr\u00e6fikset, der skal bruges til dine Solar-Log sensorer" + }, + "title": "Angiv dit Solar-Log forbindelse" + } + }, + "title": "Solar-Log" + } +} \ No newline at end of file diff --git a/homeassistant/components/solarlog/.translations/en.json b/homeassistant/components/solarlog/.translations/en.json new file mode 100644 index 00000000000..f1396045819 --- /dev/null +++ b/homeassistant/components/solarlog/.translations/en.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "already_configured": "Device is already configured", + "cannot_connect": "Failed to connect, please verify host address" + }, + "step": { + "user": { + "data": { + "host": "The hostname or ip-address of your Solar-Log device", + "name": "The prefix to be used for your Solar-Log sensors" + }, + "title": "Define your Solar-Log connection" + } + }, + "title": "Solar-Log" + } +} \ No newline at end of file diff --git a/homeassistant/components/solarlog/.translations/fr.json b/homeassistant/components/solarlog/.translations/fr.json new file mode 100644 index 00000000000..0f1b4944ed9 --- /dev/null +++ b/homeassistant/components/solarlog/.translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "cannot_connect": "\u00c9chec de la connexion, veuillez v\u00e9rifier l'adresse de l'h\u00f4te." + }, + "step": { + "user": { + "data": { + "host": "Le nom d'h\u00f4te ou l'adresse IP de votre p\u00e9riph\u00e9rique Solar-Log", + "name": "Le pr\u00e9fixe \u00e0 utiliser pour vos capteurs Solar-Log" + }, + "title": "D\u00e9finissez votre connexion Solar-Log" + } + }, + "title": "Solar-Log" + } +} \ No newline at end of file diff --git a/homeassistant/components/solarlog/.translations/nl.json b/homeassistant/components/solarlog/.translations/nl.json new file mode 100644 index 00000000000..3965f71e992 --- /dev/null +++ b/homeassistant/components/solarlog/.translations/nl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "already_configured": "Apparaat is al geconfigureerd", + "cannot_connect": "Verbinding mislukt, controleer het host-adres" + }, + "step": { + "user": { + "data": { + "host": "De hostnaam of het IP-adres van uw Solar-Log apparaat", + "name": "Het voorvoegsel dat moet worden gebruikt voor uw Solar-Log sensoren" + }, + "title": "Definieer uw Solar-Log verbinding" + } + }, + "title": "Solar-Log" + } +} \ No newline at end of file diff --git a/homeassistant/components/solarlog/.translations/no.json b/homeassistant/components/solarlog/.translations/no.json new file mode 100644 index 00000000000..017e886c817 --- /dev/null +++ b/homeassistant/components/solarlog/.translations/no.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "already_configured": "Enheten er allerede konfigurert", + "cannot_connect": "Kunne ikke koble til, vennligst bekreft vertsadresse" + }, + "step": { + "user": { + "data": { + "host": "Vertsnavnet eller ip-adressen til din Solar-Log-enhet", + "name": "Prefikset som skal brukes til dine Solar-Log sensorer" + }, + "title": "Definer din Solar-Log tilkobling" + } + }, + "title": "Solar-Log" + } +} \ No newline at end of file diff --git a/homeassistant/components/solarlog/.translations/ru.json b/homeassistant/components/solarlog/.translations/ru.json new file mode 100644 index 00000000000..7f40935e5a5 --- /dev/null +++ b/homeassistant/components/solarlog/.translations/ru.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "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." + }, + "error": { + "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.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u0445\u043e\u0441\u0442\u0430." + }, + "step": { + "user": { + "data": { + "host": "\u0414\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441", + "name": "\u041f\u0440\u0435\u0444\u0438\u043a\u0441, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0431\u0443\u0434\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0434\u043b\u044f \u0434\u0430\u0442\u0447\u0438\u043a\u043e\u0432 Solar-Log" + }, + "title": "Solar-Log" + } + }, + "title": "Solar-Log" + } +} \ No newline at end of file diff --git a/homeassistant/components/solarlog/__init__.py b/homeassistant/components/solarlog/__init__.py new file mode 100644 index 00000000000..c8035e1f7e6 --- /dev/null +++ b/homeassistant/components/solarlog/__init__.py @@ -0,0 +1,21 @@ +"""Solar-Log integration.""" +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.typing import HomeAssistantType + + +async def async_setup(hass, config): + """Component setup, do nothing.""" + return True + + +async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): + """Set up a config entry for solarlog.""" + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, "sensor") + ) + return True + + +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + return await hass.config_entries.async_forward_entry_unload(entry, "sensor") diff --git a/homeassistant/components/solarlog/config_flow.py b/homeassistant/components/solarlog/config_flow.py new file mode 100644 index 00000000000..5cb2d5deec1 --- /dev/null +++ b/homeassistant/components/solarlog/config_flow.py @@ -0,0 +1,107 @@ +"""Config flow for solarlog integration.""" +import logging +from urllib.parse import ParseResult, urlparse + +from requests.exceptions import HTTPError, Timeout +from sunwatcher.solarlog.solarlog import SolarLog +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_HOST, CONF_NAME +from homeassistant.core import HomeAssistant, callback +from homeassistant.util import slugify + +from .const import DEFAULT_HOST, DEFAULT_NAME, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +@callback +def solarlog_entries(hass: HomeAssistant): + """Return the hosts already configured.""" + return set( + entry.data[CONF_HOST] for entry in hass.config_entries.async_entries(DOMAIN) + ) + + +class SolarLogConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for solarlog.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + def __init__(self) -> None: + """Initialize the config flow.""" + self._errors = {} + + def _host_in_configuration_exists(self, host) -> bool: + """Return True if host exists in configuration.""" + if host in solarlog_entries(self.hass): + return True + return False + + async def _test_connection(self, host): + """Check if we can connect to the Solar-Log device.""" + try: + await self.hass.async_add_executor_job(SolarLog, host) + return True + except (OSError, HTTPError, Timeout): + self._errors[CONF_HOST] = "cannot_connect" + _LOGGER.error( + "Could not connect to Solar-Log device at %s, check host ip address", + host, + ) + return False + + async def async_step_user(self, user_input=None): + """Step when user intializes a integration.""" + self._errors = {} + if user_input is not None: + # set some defaults in case we need to return to the form + name = slugify(user_input.get(CONF_NAME, DEFAULT_NAME)) + host_entry = user_input.get(CONF_HOST, DEFAULT_HOST) + + url = urlparse(host_entry, "http") + netloc = url.netloc or url.path + path = url.path if url.netloc else "" + url = ParseResult("http", netloc, path, *url[3:]) + host = url.geturl() + + if self._host_in_configuration_exists(host): + self._errors[CONF_HOST] = "already_configured" + else: + if await self._test_connection(host): + return self.async_create_entry(title=name, data={CONF_HOST: host}) + else: + user_input = {} + user_input[CONF_NAME] = DEFAULT_NAME + user_input[CONF_HOST] = DEFAULT_HOST + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required( + CONF_NAME, default=user_input.get(CONF_NAME, DEFAULT_NAME) + ): str, + vol.Required( + CONF_HOST, default=user_input.get(CONF_HOST, DEFAULT_HOST) + ): str, + } + ), + errors=self._errors, + ) + + async def async_step_import(self, user_input=None): + """Import a config entry.""" + host_entry = user_input.get(CONF_HOST, DEFAULT_HOST) + + url = urlparse(host_entry, "http") + netloc = url.netloc or url.path + path = url.path if url.netloc else "" + url = ParseResult("http", netloc, path, *url[3:]) + host = url.geturl() + + if self._host_in_configuration_exists(host): + return self.async_abort(reason="already_configured") + return await self.async_step_user(user_input) diff --git a/homeassistant/components/solarlog/const.py b/homeassistant/components/solarlog/const.py new file mode 100644 index 00000000000..67eb8006cec --- /dev/null +++ b/homeassistant/components/solarlog/const.py @@ -0,0 +1,89 @@ +"""Constants for the Solar-Log integration.""" +from datetime import timedelta + +from homeassistant.const import POWER_WATT, ENERGY_KILO_WATT_HOUR + +DOMAIN = "solarlog" + +"""Default config for solarlog.""" +DEFAULT_HOST = "http://solar-log" +DEFAULT_NAME = "solarlog" + +"""Fixed constants.""" +SCAN_INTERVAL = timedelta(seconds=60) + +"""Supported sensor types.""" +SENSOR_TYPES = { + "time": ["TIME", "last update", None, "mdi:calendar-clock"], + "power_ac": ["powerAC", "power AC", POWER_WATT, "mdi:solar-power"], + "power_dc": ["powerDC", "power DC", POWER_WATT, "mdi:solar-power"], + "voltage_ac": ["voltageAC", "voltage AC", "V", "mdi:flash"], + "voltage_dc": ["voltageDC", "voltage DC", "V", "mdi:flash"], + "yield_day": ["yieldDAY", "yield day", ENERGY_KILO_WATT_HOUR, "mdi:solar-power"], + "yield_yesterday": [ + "yieldYESTERDAY", + "yield yesterday", + ENERGY_KILO_WATT_HOUR, + "mdi:solar-power", + ], + "yield_month": [ + "yieldMONTH", + "yield month", + ENERGY_KILO_WATT_HOUR, + "mdi:solar-power", + ], + "yield_year": ["yieldYEAR", "yield year", ENERGY_KILO_WATT_HOUR, "mdi:solar-power"], + "yield_total": [ + "yieldTOTAL", + "yield total", + ENERGY_KILO_WATT_HOUR, + "mdi:solar-power", + ], + "consumption_ac": ["consumptionAC", "consumption AC", POWER_WATT, "mdi:power-plug"], + "consumption_day": [ + "consumptionDAY", + "consumption day", + ENERGY_KILO_WATT_HOUR, + "mdi:power-plug", + ], + "consumption_yesterday": [ + "consumptionYESTERDAY", + "consumption yesterday", + ENERGY_KILO_WATT_HOUR, + "mdi:power-plug", + ], + "consumption_month": [ + "consumptionMONTH", + "consumption month", + ENERGY_KILO_WATT_HOUR, + "mdi:power-plug", + ], + "consumption_year": [ + "consumptionYEAR", + "consumption year", + ENERGY_KILO_WATT_HOUR, + "mdi:power-plug", + ], + "consumption_total": [ + "consumptionTOTAL", + "consumption total", + ENERGY_KILO_WATT_HOUR, + "mdi:power-plug", + ], + "total_power": ["totalPOWER", "total power", "Wp", "mdi:solar-power"], + "alternator_loss": [ + "alternatorLOSS", + "alternator loss", + POWER_WATT, + "mdi:solar-power", + ], + "capacity": ["CAPACITY", "capacity", "%", "mdi:solar-power"], + "efficiency": ["EFFICIENCY", "efficiency", "% W/Wp", "mdi:solar-power"], + "power_available": [ + "powerAVAILABLE", + "power available", + POWER_WATT, + "mdi:solar-power", + ], + "usage": ["USAGE", "usage", None, "mdi:solar-power"], +} diff --git a/homeassistant/components/solarlog/manifest.json b/homeassistant/components/solarlog/manifest.json new file mode 100644 index 00000000000..9331628e027 --- /dev/null +++ b/homeassistant/components/solarlog/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "solarlog", + "name": "Solar-Log", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integration/solarlog", + "dependencies": [], + "codeowners": ["@Ernst79"], + "requirements": ["sunwatcher==0.2.1"] +} diff --git a/homeassistant/components/solarlog/sensor.py b/homeassistant/components/solarlog/sensor.py new file mode 100644 index 00000000000..583529ffe87 --- /dev/null +++ b/homeassistant/components/solarlog/sensor.py @@ -0,0 +1,159 @@ +"""Platform for solarlog sensors.""" +import logging +from urllib.parse import ParseResult, urlparse + +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.const import CONF_HOST, CONF_NAME +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle + +from .const import DOMAIN, DEFAULT_HOST, DEFAULT_NAME, SCAN_INTERVAL, SENSOR_TYPES + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + } +) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Import YAML configuration when available.""" + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=dict(config) + ) + ) + + +async def async_setup_entry(hass, entry, async_add_entities): + """Add solarlog entry.""" + host_entry = entry.data[CONF_HOST] + + url = urlparse(host_entry, "http") + netloc = url.netloc or url.path + path = url.path if url.netloc else "" + url = ParseResult("http", netloc, path, *url[3:]) + host = url.geturl() + + platform_name = entry.title + + try: + api = await hass.async_add_executor_job(SolarLog, host) + _LOGGER.debug("Connected to Solar-Log device, setting up entries") + except (OSError, HTTPError, Timeout): + _LOGGER.error( + "Could not connect to Solar-Log device at %s, check host ip address", host + ) + return + + # Create solarlog data service which will retrieve and update the data. + data = await hass.async_add_executor_job(SolarlogData, hass, api, host) + + # Create a new sensor for each sensor type. + entities = [] + for sensor_key in SENSOR_TYPES: + sensor = SolarlogSensor(platform_name, sensor_key, data) + entities.append(sensor) + + async_add_entities(entities, True) + return True + + +class SolarlogSensor(Entity): + """Representation of a Sensor.""" + + def __init__(self, platform_name, sensor_key, data): + """Initialize the sensor.""" + self.platform_name = platform_name + self.sensor_key = sensor_key + self.data = data + self._state = None + + self._json_key = SENSOR_TYPES[self.sensor_key][0] + self._unit_of_measurement = SENSOR_TYPES[self.sensor_key][2] + + @property + def name(self): + """Return the name of the sensor.""" + return "{} ({})".format(self.platform_name, SENSOR_TYPES[self.sensor_key][1]) + + @property + def unit_of_measurement(self): + """Return the state of the sensor.""" + return self._unit_of_measurement + + @property + def icon(self): + """Return the sensor icon.""" + return SENSOR_TYPES[self.sensor_key][3] + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + def update(self): + """Get the latest data from the sensor and update the state.""" + self.data.update() + self._state = self.data.data[self._json_key] + + +class SolarlogData: + """Get and update the latest data.""" + + def __init__(self, hass, api, host): + """Initialize the data object.""" + self.api = api + self.hass = hass + self.host = host + self.update = Throttle(SCAN_INTERVAL)(self._update) + self.data = {} + + def _update(self): + """Update the data from the SolarLog device.""" + try: + self.api = SolarLog(self.host) + response = self.api.time + _LOGGER.debug( + "Connection to Solarlog successful. Retrieving latest Solarlog update of %s", + response, + ) + except (OSError, Timeout, HTTPError): + _LOGGER.error("Connection error, Could not retrieve data, skipping update") + return + + try: + self.data["TIME"] = self.api.time + self.data["powerAC"] = self.api.power_ac + self.data["powerDC"] = self.api.power_dc + self.data["voltageAC"] = self.api.voltage_ac + self.data["voltageDC"] = self.api.voltage_dc + self.data["yieldDAY"] = self.api.yield_day / 1000 + self.data["yieldYESTERDAY"] = self.api.yield_yesterday / 1000 + self.data["yieldMONTH"] = self.api.yield_month / 1000 + self.data["yieldYEAR"] = self.api.yield_year / 1000 + self.data["yieldTOTAL"] = self.api.yield_total / 1000 + self.data["consumptionAC"] = self.api.consumption_ac + self.data["consumptionDAY"] = self.api.consumption_day / 1000 + self.data["consumptionYESTERDAY"] = self.api.consumption_yesterday / 1000 + self.data["consumptionMONTH"] = self.api.consumption_month / 1000 + self.data["consumptionYEAR"] = self.api.consumption_year / 1000 + self.data["consumptionTOTAL"] = self.api.consumption_total / 1000 + self.data["totalPOWER"] = self.api.total_power + self.data["alternatorLOSS"] = self.api.alternator_loss + self.data["CAPACITY"] = round(self.api.capacity * 100, 0) + self.data["EFFICIENCY"] = round(self.api.efficiency * 100, 0) + self.data["powerAVAILABLE"] = self.api.power_available + self.data["USAGE"] = self.api.usage + _LOGGER.debug("Updated Solarlog overview data: %s", self.data) + except AttributeError: + _LOGGER.error("Missing details data in Solarlog response") diff --git a/homeassistant/components/solarlog/strings.json b/homeassistant/components/solarlog/strings.json new file mode 100644 index 00000000000..5399d5176c9 --- /dev/null +++ b/homeassistant/components/solarlog/strings.json @@ -0,0 +1,21 @@ +{ + "config": { + "title": "Solar-Log", + "step": { + "user": { + "title": "Define your Solar-Log connection", + "data": { + "host": "The hostname or ip-address of your Solar-Log device", + "name": "The prefix to be used for your Solar-Log sensors" + } + } + }, + "error": { + "already_configured": "Device is already configured", + "cannot_connect": "Failed to connect, please verify host address" + }, + "abort": { + "already_configured": "Device is already configured" + } + } +} diff --git a/homeassistant/components/soma/.translations/ca.json b/homeassistant/components/soma/.translations/ca.json index 6bd4737d6fc..18b33d1bc9b 100644 --- a/homeassistant/components/soma/.translations/ca.json +++ b/homeassistant/components/soma/.translations/ca.json @@ -8,6 +8,16 @@ "create_entry": { "default": "Autenticaci\u00f3 exitosa amb Soma." }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3", + "port": "Port" + }, + "description": "Introdueix la informaci\u00f3 de connexi\u00f3 de SOMA Connect.", + "title": "SOMA Connect" + } + }, "title": "Soma" } } \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/da.json b/homeassistant/components/soma/.translations/da.json index a82da0ce24d..557eeab55b1 100644 --- a/homeassistant/components/soma/.translations/da.json +++ b/homeassistant/components/soma/.translations/da.json @@ -8,6 +8,16 @@ "create_entry": { "default": "Godkendt med Soma." }, + "step": { + "user": { + "data": { + "host": "V\u00e6rt", + "port": "Port" + }, + "description": "Indtast forbindelsesindstillinger for din SOMA Connect.", + "title": "SOMA Connect" + } + }, "title": "Soma" } } \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/de.json b/homeassistant/components/soma/.translations/de.json index d93eec8aed7..cb08613c07b 100644 --- a/homeassistant/components/soma/.translations/de.json +++ b/homeassistant/components/soma/.translations/de.json @@ -4,6 +4,20 @@ "already_setup": "Du kannst nur ein einziges Soma-Konto konfigurieren.", "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", "missing_configuration": "Die Soma-Komponente ist nicht konfiguriert. Bitte folgen Sie der Dokumentation." - } + }, + "create_entry": { + "default": "Erfolgreich bei Soma authentifiziert." + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port" + }, + "description": "Bitte gib die Verbindungsinformationen f\u00fcr SOMA Connect ein.", + "title": "SOMA Connect" + } + }, + "title": "Soma" } } \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/en.json b/homeassistant/components/soma/.translations/en.json index 5dea73fcc22..42e09a8762c 100644 --- a/homeassistant/components/soma/.translations/en.json +++ b/homeassistant/components/soma/.translations/en.json @@ -8,6 +8,16 @@ "create_entry": { "default": "Successfully authenticated with Soma." }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port" + }, + "description": "Please enter connection settings of your SOMA Connect.", + "title": "SOMA Connect" + } + }, "title": "Soma" } } \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/es.json b/homeassistant/components/soma/.translations/es.json index 8126b6ea5ae..86922622704 100644 --- a/homeassistant/components/soma/.translations/es.json +++ b/homeassistant/components/soma/.translations/es.json @@ -8,6 +8,16 @@ "create_entry": { "default": "Autenticado con \u00e9xito con Soma." }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Puerto" + }, + "description": "Por favor, introduzca los ajustes de conexi\u00f3n de SOMA Connect.", + "title": "SOMA Connect" + } + }, "title": "Soma" } } \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/fr.json b/homeassistant/components/soma/.translations/fr.json index e990fb98dc2..a758ab0f615 100644 --- a/homeassistant/components/soma/.translations/fr.json +++ b/homeassistant/components/soma/.translations/fr.json @@ -8,6 +8,16 @@ "create_entry": { "default": "Authentifi\u00e9 avec succ\u00e8s avec Soma." }, + "step": { + "user": { + "data": { + "host": "H\u00f4te", + "port": "Port" + }, + "description": "Veuillez entrer les param\u00e8tres de connexion de votre SOMA Connect.", + "title": "SOMA Connect" + } + }, "title": "Soma" } } \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/it.json b/homeassistant/components/soma/.translations/it.json index ce8e950dacc..1398b2a66be 100644 --- a/homeassistant/components/soma/.translations/it.json +++ b/homeassistant/components/soma/.translations/it.json @@ -8,6 +8,16 @@ "create_entry": { "default": "Autenticato con successo con Soma." }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Porta" + }, + "description": "Inserisci le impostazioni di connessione del tuo SOMA Connect.", + "title": "SOMA Connect" + } + }, "title": "Soma" } } \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/ko.json b/homeassistant/components/soma/.translations/ko.json index 53146bebf83..90995ebc9f2 100644 --- a/homeassistant/components/soma/.translations/ko.json +++ b/homeassistant/components/soma/.translations/ko.json @@ -8,6 +8,16 @@ "create_entry": { "default": "Soma \ub85c \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4." }, + "step": { + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "port": "\ud3ec\ud2b8" + }, + "description": "SOMA Connect \uc640\uc758 \uc5f0\uacb0 \uc124\uc815\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "title": "SOMA Connect" + } + }, "title": "Soma" } } \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/lb.json b/homeassistant/components/soma/.translations/lb.json index d8aba082537..93e9a1e66c4 100644 --- a/homeassistant/components/soma/.translations/lb.json +++ b/homeassistant/components/soma/.translations/lb.json @@ -8,6 +8,16 @@ "create_entry": { "default": "Erfollegr\u00e4ich mat Soma authentifiz\u00e9iert." }, + "step": { + "user": { + "data": { + "host": "Apparat", + "port": "Port" + }, + "description": "Gitt Verbindungs Informatioune vun \u00e4rem SOMA Connect an.", + "title": "SOMA Connect" + } + }, "title": "Soma" } } \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/nl.json b/homeassistant/components/soma/.translations/nl.json new file mode 100644 index 00000000000..c1188b0ac63 --- /dev/null +++ b/homeassistant/components/soma/.translations/nl.json @@ -0,0 +1,23 @@ +{ + "config": { + "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." + }, + "create_entry": { + "default": "Succesvol geverifieerd met Soma." + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Poort" + }, + "description": "Voer de verbindingsinstellingen van uw SOMA Connect in.", + "title": "SOMA Connect" + } + }, + "title": "Soma" + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/no.json b/homeassistant/components/soma/.translations/no.json index 1ea53b778ea..b2d80208b83 100644 --- a/homeassistant/components/soma/.translations/no.json +++ b/homeassistant/components/soma/.translations/no.json @@ -1,13 +1,23 @@ { "config": { "abort": { - "already_setup": "Du kan bare konfigurere en Soma-konto.", + "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." }, "create_entry": { "default": "Vellykket autentisering med Somfy." }, + "step": { + "user": { + "data": { + "host": "Vert", + "port": "Port" + }, + "description": "Vennligst skriv tilkoblingsinnstillingene for din SOMA Connect.", + "title": "SOMA Connect" + } + }, "title": "Soma" } } \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/pl.json b/homeassistant/components/soma/.translations/pl.json index 0ed881853b8..4d783f3f0a0 100644 --- a/homeassistant/components/soma/.translations/pl.json +++ b/homeassistant/components/soma/.translations/pl.json @@ -8,6 +8,16 @@ "create_entry": { "default": "Pomy\u015blnie uwierzytelniono z Soma" }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port" + }, + "description": "Wprowad\u017a ustawienia po\u0142\u0105czenia SOMA Connect.", + "title": "SOMA Connect" + } + }, "title": "Soma" } } \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/pt-BR.json b/homeassistant/components/soma/.translations/pt-BR.json new file mode 100644 index 00000000000..da05e3b43ae --- /dev/null +++ b/homeassistant/components/soma/.translations/pt-BR.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Host", + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/pt.json b/homeassistant/components/soma/.translations/pt.json new file mode 100644 index 00000000000..f681da4210f --- /dev/null +++ b/homeassistant/components/soma/.translations/pt.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Servidor", + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/ru.json b/homeassistant/components/soma/.translations/ru.json index 5ab3af0ecf8..f7e6574b113 100644 --- a/homeassistant/components/soma/.translations/ru.json +++ b/homeassistant/components/soma/.translations/ru.json @@ -8,6 +8,16 @@ "create_entry": { "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "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" } } \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/sl.json b/homeassistant/components/soma/.translations/sl.json index 7dd523f366c..b3075208d2c 100644 --- a/homeassistant/components/soma/.translations/sl.json +++ b/homeassistant/components/soma/.translations/sl.json @@ -8,6 +8,16 @@ "create_entry": { "default": "Uspe\u0161no overjen s Soma." }, + "step": { + "user": { + "data": { + "host": "Gostitelj", + "port": "Vrata" + }, + "description": "Prosimo, vnesite nastavitve povezave za va\u0161 SOMA Connect.", + "title": "SOMA Connect" + } + }, "title": "Soma" } } \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/zh-Hant.json b/homeassistant/components/soma/.translations/zh-Hant.json index 3d28389ff91..893abe82ee1 100644 --- a/homeassistant/components/soma/.translations/zh-Hant.json +++ b/homeassistant/components/soma/.translations/zh-Hant.json @@ -8,6 +8,16 @@ "create_entry": { "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Soma \u8a2d\u5099\u3002" }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "port": "\u901a\u8a0a\u57e0" + }, + "description": "\u8acb\u8f38\u5165 SOMA Connect \u9023\u7dda\u8a2d\u5b9a\u3002", + "title": "SOMA Connect" + } + }, "title": "Soma" } } \ No newline at end of file diff --git a/homeassistant/components/soma/__init__.py b/homeassistant/components/soma/__init__.py index 5bf51e743e9..b4daa28b5b2 100644 --- a/homeassistant/components/soma/__init__.py +++ b/homeassistant/components/soma/__init__.py @@ -3,6 +3,7 @@ import logging import voluptuous as vol from api.soma_api import SomaApi +from requests import RequestException import homeassistant.helpers.config_validation as cv from homeassistant import config_entries @@ -75,6 +76,12 @@ class SomaEntity(Entity): self.device = device self.api = api self.current_position = 50 + self.is_available = True + + @property + def available(self): + """Return true if the last API commands returned successfully.""" + return self.is_available @property def unique_id(self): @@ -100,12 +107,19 @@ class SomaEntity(Entity): async def async_update(self): """Update the device with the latest data.""" - response = await self.hass.async_add_executor_job( - self.api.get_shade_state, self.device["mac"] - ) + try: + response = await self.hass.async_add_executor_job( + self.api.get_shade_state, self.device["mac"] + ) + except RequestException: + _LOGGER.error("Connection to SOMA Connect failed") + self.is_available = False + return if response["result"] != "success": _LOGGER.error( "Unable to reach device %s (%s)", self.device["name"], response["msg"] ) + self.is_available = False return self.current_position = 100 - response["position"] + self.is_available = True diff --git a/homeassistant/components/soma/strings.json b/homeassistant/components/soma/strings.json index eac817ce119..aa2f92f0be6 100644 --- a/homeassistant/components/soma/strings.json +++ b/homeassistant/components/soma/strings.json @@ -8,6 +8,16 @@ "create_entry": { "default": "Successfully authenticated with Soma." }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port" + }, + "description": "Please enter connection settings of your SOMA Connect.", + "title": "SOMA Connect" + } + }, "title": "Soma" } } diff --git a/homeassistant/components/somfy/__init__.py b/homeassistant/components/somfy/__init__.py index 2c7c71d7a69..cd5960bf6b1 100644 --- a/homeassistant/components/somfy/__init__.py +++ b/homeassistant/components/somfy/__init__.py @@ -4,21 +4,21 @@ Support for Somfy hubs. 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 -from functools import partial import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant import config_entries +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.const import CONF_TOKEN from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import Throttle +from . import api + API = "api" DEVICES = "devices" @@ -52,19 +52,21 @@ SOMFY_COMPONENTS = ["cover"] async def async_setup(hass, config): """Set up the Somfy component.""" + hass.data[DOMAIN] = {} + if DOMAIN not in config: return True - hass.data[DOMAIN] = {} - - config_flow.register_flow_implementation( - hass, config[DOMAIN][CONF_CLIENT_ID], config[DOMAIN][CONF_CLIENT_SECRET] - ) - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT} - ) + config_flow.SomfyFlowHandler.async_register_implementation( + hass, + config_entry_oauth2_flow.LocalOAuth2Implementation( + hass, + DOMAIN, + config[DOMAIN][CONF_CLIENT_ID], + config[DOMAIN][CONF_CLIENT_SECRET], + "https://accounts.somfy.com/oauth/oauth/v2/auth", + "https://accounts.somfy.com/oauth/oauth/v2/token", + ), ) return True @@ -72,25 +74,18 @@ async def async_setup(hass, config): async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): """Set up Somfy from a config entry.""" - - def token_saver(token): - _LOGGER.debug("Saving updated token") - entry.data[CONF_TOKEN] = token - update_entry = partial( - hass.config_entries.async_update_entry, data={**entry.data} + # Backwards compat + if "auth_implementation" not in entry.data: + hass.config_entries.async_update_entry( + entry, data={**entry.data, "auth_implementation": DOMAIN} ) - hass.add_job(update_entry, entry) - # Force token update. - from pymfy.api.somfy_api import SomfyApi - - hass.data[DOMAIN][API] = SomfyApi( - entry.data["refresh_args"]["client_id"], - entry.data["refresh_args"]["client_secret"], - token=entry.data[CONF_TOKEN], - token_updater=token_saver, + implementation = await config_entry_oauth2_flow.async_get_config_entry_implementation( + hass, entry ) + hass.data[DOMAIN][API] = api.ConfigEntrySomfyApi(hass, entry, implementation) + await update_all_devices(hass) for component in SOMFY_COMPONENTS: @@ -104,16 +99,22 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): """Unload a config entry.""" hass.data[DOMAIN].pop(API, None) + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in SOMFY_COMPONENTS + ] + ) return True class SomfyEntity(Entity): """Representation of a generic Somfy device.""" - def __init__(self, device, api): + def __init__(self, device, somfy_api): """Initialize the Somfy device.""" self.device = device - self.api = api + self.api = somfy_api @property def unique_id(self): diff --git a/homeassistant/components/somfy/api.py b/homeassistant/components/somfy/api.py new file mode 100644 index 00000000000..3e7bcf9deb4 --- /dev/null +++ b/homeassistant/components/somfy/api.py @@ -0,0 +1,55 @@ +"""API for Somfy bound to HASS OAuth.""" +from asyncio import run_coroutine_threadsafe +from functools import partial + +import requests +from pymfy.api import somfy_api + +from homeassistant import core, config_entries +from homeassistant.helpers import config_entry_oauth2_flow + + +class ConfigEntrySomfyApi(somfy_api.AbstractSomfyApi): + """Provide a Somfy API tied into an OAuth2 based config entry.""" + + def __init__( + self, + hass: core.HomeAssistant, + config_entry: config_entries.ConfigEntry, + implementation: config_entry_oauth2_flow.AbstractOAuth2Implementation, + ): + """Initialize the Config Entry Somfy API.""" + self.hass = hass + self.config_entry = config_entry + self.session = config_entry_oauth2_flow.OAuth2Session( + hass, config_entry, implementation + ) + + def get(self, path): + """Fetch a URL from the Somfy API.""" + return run_coroutine_threadsafe( + self._request("get", path), self.hass.loop + ).result() + + def post(self, path, *, json): + """Post data to the Somfy API.""" + return run_coroutine_threadsafe( + self._request("post", path, json=json), self.hass.loop + ).result() + + async def _request(self, method, path, **kwargs): + """Make a request.""" + await self.session.async_ensure_token_valid() + + return await self.hass.async_add_executor_job( + partial( + requests.request, + method, + f"{self.base_url}{path}", + **kwargs, + headers={ + **kwargs.get("headers", {}), + "authorization": f"Bearer {self.config_entry.data['token']['access_token']}", + }, + ) + ) diff --git a/homeassistant/components/somfy/config_flow.py b/homeassistant/components/somfy/config_flow.py index 9f3c58c8ffb..cb180d4e247 100644 --- a/homeassistant/components/somfy/config_flow.py +++ b/homeassistant/components/somfy/config_flow.py @@ -1,141 +1,28 @@ """Config flow for Somfy.""" -import asyncio import logging -import async_timeout - from homeassistant import config_entries -from homeassistant.components.http import HomeAssistantView -from homeassistant.core import callback -from .const import CLIENT_ID, CLIENT_SECRET, DOMAIN - -AUTH_CALLBACK_PATH = "/auth/somfy/callback" -AUTH_CALLBACK_NAME = "auth:somfy:callback" +from homeassistant.helpers import config_entry_oauth2_flow +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -@callback -def register_flow_implementation(hass, client_id, client_secret): - """Register a flow implementation. +@config_entries.HANDLERS.register(DOMAIN) +class SomfyFlowHandler(config_entry_oauth2_flow.AbstractOAuth2FlowHandler): + """Config flow to handle Somfy OAuth2 authentication.""" - client_id: Client id. - client_secret: Client secret. - """ - hass.data[DOMAIN][CLIENT_ID] = client_id - hass.data[DOMAIN][CLIENT_SECRET] = client_secret - - -@config_entries.HANDLERS.register("somfy") -class SomfyFlowHandler(config_entries.ConfigFlow): - """Handle a config flow.""" - - VERSION = 1 + DOMAIN = DOMAIN CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL - def __init__(self): - """Instantiate config flow.""" - self.code = None - - async def async_step_import(self, user_input=None): - """Handle external yaml configuration.""" - if self.hass.config_entries.async_entries(DOMAIN): - return self.async_abort(reason="already_setup") - return await self.async_step_auth() + @property + def logger(self) -> logging.Logger: + """Return logger.""" + return logging.getLogger(__name__) async def async_step_user(self, user_input=None): """Handle a flow start.""" if self.hass.config_entries.async_entries(DOMAIN): return self.async_abort(reason="already_setup") - if DOMAIN not in self.hass.data: - return self.async_abort(reason="missing_configuration") - - return await self.async_step_auth() - - async def async_step_auth(self, user_input=None): - """Create an entry for auth.""" - # Flow has been triggered from Somfy website - if user_input: - return await self.async_step_code(user_input) - - try: - with async_timeout.timeout(10): - url, _ = await self._get_authorization_url() - except asyncio.TimeoutError: - return self.async_abort(reason="authorize_url_timeout") - - return self.async_external_step(step_id="auth", url=url) - - async def _get_authorization_url(self): - """Get Somfy authorization url.""" - from pymfy.api.somfy_api import SomfyApi - - client_id = self.hass.data[DOMAIN][CLIENT_ID] - client_secret = self.hass.data[DOMAIN][CLIENT_SECRET] - redirect_uri = f"{self.hass.config.api.base_url}{AUTH_CALLBACK_PATH}" - api = SomfyApi(client_id, client_secret, redirect_uri) - - self.hass.http.register_view(SomfyAuthCallbackView()) - # Thanks to the state, we can forward the flow id to Somfy that will - # add it in the callback. - return await self.hass.async_add_executor_job( - api.get_authorization_url, self.flow_id - ) - - async def async_step_code(self, code): - """Received code for authentication.""" - self.code = code - return self.async_external_step_done(next_step_id="creation") - - async def async_step_creation(self, user_input=None): - """Create Somfy api and entries.""" - client_id = self.hass.data[DOMAIN][CLIENT_ID] - client_secret = self.hass.data[DOMAIN][CLIENT_SECRET] - code = self.code - from pymfy.api.somfy_api import SomfyApi - - redirect_uri = f"{self.hass.config.api.base_url}{AUTH_CALLBACK_PATH}" - api = SomfyApi(client_id, client_secret, redirect_uri) - token = await self.hass.async_add_executor_job(api.request_token, None, code) - _LOGGER.info("Successfully authenticated Somfy") - return self.async_create_entry( - title="Somfy", - data={ - "token": token, - "refresh_args": { - "client_id": client_id, - "client_secret": client_secret, - }, - }, - ) - - -class SomfyAuthCallbackView(HomeAssistantView): - """Somfy Authorization Callback View.""" - - requires_auth = False - url = AUTH_CALLBACK_PATH - name = AUTH_CALLBACK_NAME - - @staticmethod - async def get(request): - """Receive authorization code.""" - from aiohttp import web_response - - if "code" not in request.query or "state" not in request.query: - return web_response.Response( - text="Missing code or state parameter in " + request.url - ) - - hass = request.app["hass"] - hass.async_create_task( - hass.config_entries.flow.async_configure( - flow_id=request.query["state"], user_input=request.query["code"] - ) - ) - - return web_response.Response( - headers={"content-type": "text/html"}, - text="", - ) + return await super().async_step_user(user_input) diff --git a/homeassistant/components/somfy/const.py b/homeassistant/components/somfy/const.py index 99fafb71bff..8765e37e6d6 100644 --- a/homeassistant/components/somfy/const.py +++ b/homeassistant/components/somfy/const.py @@ -1,5 +1,3 @@ """Define constants for the Somfy component.""" DOMAIN = "somfy" -CLIENT_ID = "client_id" -CLIENT_SECRET = "client_secret" diff --git a/homeassistant/components/somfy/manifest.json b/homeassistant/components/somfy/manifest.json index 83b50684fda..a34023f76ff 100644 --- a/homeassistant/components/somfy/manifest.json +++ b/homeassistant/components/somfy/manifest.json @@ -3,11 +3,7 @@ "name": "Somfy Open API", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/somfy", - "dependencies": [], - "codeowners": [ - "@tetienne" - ], - "requirements": [ - "pymfy==0.5.2" - ] -} \ No newline at end of file + "dependencies": ["http"], + "codeowners": ["@tetienne"], + "requirements": ["pymfy==0.6.0"] +} diff --git a/homeassistant/components/songpal/media_player.py b/homeassistant/components/songpal/media_player.py index 9f5ae9d1ac6..0567cd0ea6a 100644 --- a/homeassistant/components/songpal/media_player.py +++ b/homeassistant/components/songpal/media_player.py @@ -4,6 +4,14 @@ import logging from collections import OrderedDict import voluptuous as vol +from songpal import ( + Device, + SongpalException, + VolumeChange, + ContentChange, + PowerChange, + ConnectChange, +) from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA from homeassistant.components.media_player.const import ( @@ -60,8 +68,6 @@ SET_SOUND_SCHEMA = vol.Schema( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Songpal platform.""" - from songpal import SongpalException - if PLATFORM not in hass.data: hass.data[PLATFORM] = {} @@ -117,8 +123,6 @@ class SongpalDevice(MediaPlayerDevice): def __init__(self, name, endpoint, poll=False): """Init.""" - from songpal import Device - self._name = name self._endpoint = endpoint self._poll = poll @@ -151,7 +155,6 @@ class SongpalDevice(MediaPlayerDevice): async def async_activate_websocket(self): """Activate websocket for listening if wanted.""" _LOGGER.info("Activating websocket connection..") - from songpal import VolumeChange, ContentChange, PowerChange, ConnectChange async def _volume_changed(volume: VolumeChange): _LOGGER.debug("Volume changed: %s", volume) @@ -230,8 +233,6 @@ class SongpalDevice(MediaPlayerDevice): async def async_update(self): """Fetch updates from the device.""" - from songpal import SongpalException - try: volumes = await self.dev.get_volume_information() if not volumes: diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index bd16cfe353a..d2c6210f01c 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -1,16 +1,16 @@ """Support to embed Sonos.""" import asyncio + import voluptuous as vol from homeassistant import config_entries from homeassistant.components.media_player import DOMAIN as MP_DOMAIN -from homeassistant.const import CONF_HOSTS, ATTR_ENTITY_ID, ATTR_TIME +from homeassistant.const import ATTR_ENTITY_ID, ATTR_TIME, CONF_HOSTS from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import DOMAIN - CONF_ADVERTISE_ADDR = "advertise_addr" CONF_INTERFACE_ADDR = "interface_addr" diff --git a/homeassistant/components/sonos/config_flow.py b/homeassistant/components/sonos/config_flow.py index 3ce62f54a2f..42ac32163a4 100644 --- a/homeassistant/components/sonos/config_flow.py +++ b/homeassistant/components/sonos/config_flow.py @@ -1,13 +1,14 @@ """Config flow for SONOS.""" -from homeassistant.helpers import config_entry_flow +import pysonos + from homeassistant import config_entries +from homeassistant.helpers import config_entry_flow + from .const import DOMAIN async def _async_has_devices(hass): """Return if there are devices that can be discovered.""" - import pysonos - return await hass.async_add_executor_job(pysonos.discover) diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index 6d636f36b3f..7b0c041b2a9 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sonos", "requirements": [ - "pysonos==0.0.23" + "pysonos==0.0.24" ], "dependencies": [], "ssdp": { diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 41472413a07..94d252e9fee 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -8,8 +8,8 @@ import urllib import async_timeout import pysonos +from pysonos.exceptions import SoCoException, SoCoUPnPException import pysonos.snapshot -from pysonos.exceptions import SoCoUPnPException, SoCoException from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( @@ -40,11 +40,6 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util.dt import utcnow from . import ( - CONF_ADVERTISE_ADDR, - CONF_HOSTS, - CONF_INTERFACE_ADDR, - DATA_SERVICE_EVENT, - DOMAIN as SONOS_DOMAIN, ATTR_ALARM_ID, ATTR_ENABLED, ATTR_INCLUDE_LINKED_ZONES, @@ -56,6 +51,11 @@ from . import ( ATTR_TIME, ATTR_VOLUME, ATTR_WITH_GROUP, + CONF_ADVERTISE_ADDR, + CONF_HOSTS, + CONF_INTERFACE_ADDR, + DATA_SERVICE_EVENT, + DOMAIN as SONOS_DOMAIN, SERVICE_CLEAR_TIMER, SERVICE_JOIN, SERVICE_PLAY_QUEUE, @@ -1161,10 +1161,9 @@ class SonosEntity(MediaPlayerDevice): @soco_coordinator def set_alarm(self, data): """Set the alarm clock on the player.""" - from pysonos import alarms alarm = None - for one_alarm in alarms.get_alarms(self.soco): + for one_alarm in pysonos.alarms.get_alarms(self.soco): # pylint: disable=protected-access if one_alarm._alarm_id == str(data[ATTR_ALARM_ID]): alarm = one_alarm diff --git a/homeassistant/components/sony_projector/switch.py b/homeassistant/components/sony_projector/switch.py index 43a4b7bc0fe..e68bed34cfa 100644 --- a/homeassistant/components/sony_projector/switch.py +++ b/homeassistant/components/sony_projector/switch.py @@ -1,10 +1,11 @@ """Support for Sony projectors via SDCP network control.""" import logging +import pysdcp import voluptuous as vol -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA -from homeassistant.const import STATE_ON, STATE_OFF, CONF_NAME, CONF_HOST +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice +from homeassistant.const import CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON 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): """Connect to Sony projector using network.""" - import pysdcp host = config[CONF_HOST] name = config[CONF_NAME] diff --git a/homeassistant/components/speedtestdotnet/__init__.py b/homeassistant/components/speedtestdotnet/__init__.py index 029356cb082..afccc71d285 100644 --- a/homeassistant/components/speedtestdotnet/__init__.py +++ b/homeassistant/components/speedtestdotnet/__init__.py @@ -1,15 +1,17 @@ """Support for testing internet speed via Speedtest.net.""" -import logging from datetime import timedelta +import logging +import speedtest import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import CONF_MONITORED_CONDITIONS, 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 + from .const import DATA_UPDATED, DOMAIN, SENSOR_TYPES _LOGGER = logging.getLogger(__name__) @@ -72,7 +74,6 @@ class SpeedtestData: def update(self, now=None): """Get the latest data from speedtest.net.""" - import speedtest _LOGGER.debug("Executing speedtest.net speed test") speed = speedtest.Speedtest() diff --git a/homeassistant/components/spotcrime/sensor.py b/homeassistant/components/spotcrime/sensor.py index fc3a7592af3..2edaa3cf933 100644 --- a/homeassistant/components/spotcrime/sensor.py +++ b/homeassistant/components/spotcrime/sensor.py @@ -1,27 +1,28 @@ """Sensor for Spot Crime.""" -from datetime import timedelta from collections import defaultdict +from datetime import timedelta import logging +import spotcrime import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_API_KEY, - CONF_INCLUDE, - CONF_EXCLUDE, - CONF_NAME, - CONF_LATITUDE, - CONF_LONGITUDE, ATTR_ATTRIBUTION, ATTR_LATITUDE, ATTR_LONGITUDE, + CONF_API_KEY, + CONF_EXCLUDE, + CONF_INCLUDE, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_NAME, CONF_RADIUS, ) +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__) @@ -75,7 +76,6 @@ class SpotCrimeSensor(Entity): self, name, latitude, longitude, radius, include, exclude, api_key, days ): """Initialize the Spot Crime sensor.""" - import spotcrime self._name = name self._include = include diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index 31fdc09af80..236c8b8db89 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -3,11 +3,14 @@ from datetime import timedelta import logging import random +import spotipy +import spotipy.oauth2 import voluptuous as vol from homeassistant.components.http import HomeAssistantView -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_MEDIA_CONTENT_ID, MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, SUPPORT_NEXT_TRACK, @@ -18,7 +21,6 @@ from homeassistant.components.media_player.const import ( SUPPORT_SELECT_SOURCE, SUPPORT_SHUFFLE_SET, SUPPORT_VOLUME_SET, - ATTR_MEDIA_CONTENT_ID, ) from homeassistant.const import CONF_NAME, STATE_IDLE, STATE_PAUSED, STATE_PLAYING from homeassistant.core import callback @@ -97,7 +99,6 @@ def request_configuration(hass, config, add_entities, oauth): def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Spotify platform.""" - import spotipy.oauth2 callback_url = f"{hass.config.api.base_url}{AUTH_CALLBACK_PATH}" cache = config.get(CONF_CACHE_PATH, hass.config.path(DEFAULT_CACHE_PATH)) @@ -181,7 +182,6 @@ class SpotifyMediaPlayer(MediaPlayerDevice): def refresh_spotify_instance(self): """Fetch a new spotify instance.""" - import spotipy token_refreshed = False need_token = self._token_info is None or self._oauth.is_token_expired( diff --git a/homeassistant/components/sql/manifest.json b/homeassistant/components/sql/manifest.json index 41d80ebccf9..fa641adc839 100644 --- a/homeassistant/components/sql/manifest.json +++ b/homeassistant/components/sql/manifest.json @@ -3,10 +3,10 @@ "name": "Sql", "documentation": "https://www.home-assistant.io/integrations/sql", "requirements": [ - "sqlalchemy==1.3.8" + "sqlalchemy==1.3.10" ], "dependencies": [], "codeowners": [ "@dgomes" ] -} +} \ No newline at end of file diff --git a/homeassistant/components/sql/sensor.py b/homeassistant/components/sql/sensor.py index 3b32f2747f1..52899c7da80 100644 --- a/homeassistant/components/sql/sensor.py +++ b/homeassistant/components/sql/sensor.py @@ -1,13 +1,15 @@ """Sensor from an SQL Query.""" -import decimal import datetime +import decimal import logging +import sqlalchemy +from sqlalchemy.orm import scoped_session, sessionmaker import voluptuous as vol +from homeassistant.components.recorder import CONF_DB_URL, DEFAULT_DB_FILE, DEFAULT_URL from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE -from homeassistant.components.recorder import CONF_DB_URL, DEFAULT_URL, DEFAULT_DB_FILE import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -46,20 +48,19 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if not db_url: db_url = DEFAULT_URL.format(hass_config_path=hass.config.path(DEFAULT_DB_FILE)) - import sqlalchemy - from sqlalchemy.orm import sessionmaker, scoped_session - try: engine = sqlalchemy.create_engine(db_url) - sessionmaker = scoped_session(sessionmaker(bind=engine)) + sessmaker = scoped_session(sessionmaker(bind=engine)) # Run a dummy query just to test the db_url - sess = sessionmaker() + sess = sessmaker() sess.execute("SELECT 1;") except sqlalchemy.exc.SQLAlchemyError as err: _LOGGER.error("Couldn't connect using %s DB_URL: %s", db_url, err) return + finally: + sess.close() queries = [] @@ -74,7 +75,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): value_template.hass = hass sensor = SQLSensor( - name, sessionmaker, query_str, column_name, unit, value_template + name, sessmaker, query_str, column_name, unit, value_template ) queries.append(sensor) @@ -120,7 +121,6 @@ class SQLSensor(Entity): def update(self): """Retrieve sensor data from the query.""" - import sqlalchemy try: sess = self.sessionmaker() diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index 6540fca1405..d8574223307 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -2,13 +2,14 @@ import asyncio import json import logging +import socket import urllib.parse import aiohttp import async_timeout 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_MEDIA_ENQUEUE, DOMAIN, @@ -40,6 +41,7 @@ from homeassistant.const import ( ) 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 _LOGGER = logging.getLogger(__name__) @@ -100,7 +102,6 @@ SERVICE_TO_METHOD = { async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the squeezebox platform.""" - import socket known_servers = hass.data.get(KNOWN_SERVERS) if known_servers is None: @@ -126,18 +127,21 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= # Get IP of host, to prevent duplication of same host (different DNS names) try: ipaddr = socket.gethostbyname(host) - except (OSError) as error: + except OSError as error: _LOGGER.error("Could not communicate with %s:%d: %s", host, port, error) - return False + raise PlatformNotReady from error if ipaddr in known_servers: return - known_servers.add(ipaddr) _LOGGER.debug("Creating LMS object for %s", ipaddr) lms = LogitechMediaServer(hass, host, port, username, password) players = await lms.create_players() + if players is None: + raise PlatformNotReady + + known_servers.add(ipaddr) hass.data[DATA_SQUEEZEBOX].extend(players) async_add_entities(players) @@ -194,7 +198,7 @@ class LogitechMediaServer: result = [] data = await self.async_query("players", "status") if data is False: - return result + return None for players in data.get("players_loop", []): player = SqueezeBoxDevice(self, players["playerid"], players["name"]) await player.async_update() diff --git a/homeassistant/components/startca/sensor.py b/homeassistant/components/startca/sensor.py index 1b567c58b45..55ae15cede7 100644 --- a/homeassistant/components/startca/sensor.py +++ b/homeassistant/components/startca/sensor.py @@ -1,10 +1,11 @@ """Support for Start.ca Bandwidth Monitor.""" from datetime import timedelta -from xml.parsers.expat import ExpatError import logging -import async_timeout +from xml.parsers.expat import ExpatError +import async_timeout import voluptuous as vol +import xmltodict from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_API_KEY, CONF_MONITORED_VARIABLES, CONF_NAME @@ -138,8 +139,6 @@ class StartcaData: @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self): """Get the Start.ca bandwidth data from the web service.""" - import xmltodict - _LOGGER.debug("Updating Start.ca usage data") url = "https://www.start.ca/support/usage/api?key=" + self.api_key with async_timeout.timeout(REQUEST_TIMEOUT): diff --git a/homeassistant/components/statsd/__init__.py b/homeassistant/components/statsd/__init__.py index 714de88dd87..79065f7ba53 100644 --- a/homeassistant/components/statsd/__init__.py +++ b/homeassistant/components/statsd/__init__.py @@ -1,11 +1,12 @@ """Support for sending data to StatsD.""" import logging +import statsd import voluptuous as vol from homeassistant.const import CONF_HOST, CONF_PORT, CONF_PREFIX, EVENT_STATE_CHANGED -import homeassistant.helpers.config_validation as cv from homeassistant.helpers import state as state_helper +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -40,7 +41,6 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): """Set up the StatsD component.""" - import statsd conf = config[DOMAIN] host = conf.get(CONF_HOST) diff --git a/homeassistant/components/steam_online/sensor.py b/homeassistant/components/steam_online/sensor.py index 6c9c5ac6079..85e5c49fb2c 100644 --- a/homeassistant/components/steam_online/sensor.py +++ b/homeassistant/components/steam_online/sensor.py @@ -1,15 +1,16 @@ """Sensor for Steam account status.""" -import logging from datetime import timedelta +import logging +import steam import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_API_KEY from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval -from homeassistant.const import CONF_API_KEY -import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -38,13 +39,12 @@ BASE_INTERVAL = timedelta(minutes=1) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Steam platform.""" - import steam as steamod - steamod.api.key.set(config.get(CONF_API_KEY)) + steam.api.key.set(config.get(CONF_API_KEY)) # Initialize steammods app list before creating sensors # to benefit from internal caching of the list. - hass.data[APP_LIST_KEY] = steamod.apps.app_list() - entities = [SteamSensor(account, steamod) for account in config.get(CONF_ACCOUNTS)] + hass.data[APP_LIST_KEY] = steam.apps.app_list() + entities = [SteamSensor(account, steam) for account in config.get(CONF_ACCOUNTS)] if not entities: return add_entities(entities, True) diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 2ae8dd5f714..a83f05820e2 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -4,31 +4,30 @@ 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 +import homeassistant.helpers.config_validation as cv +from homeassistant.loader import bind_hass + +from .const import ( + ATTR_ENDPOINTS, + ATTR_STREAMS, + CONF_DURATION, + CONF_LOOKBACK, + CONF_STREAM_SOURCE, + DOMAIN, + SERVICE_RECORD, +) +from .core import PROVIDERS +from .hls import async_setup_hls + try: import uvloop except ImportError: uvloop = None -from homeassistant.auth.util import generate_secret -import homeassistant.helpers.config_validation as cv -from homeassistant.const import EVENT_HOMEASSISTANT_STOP, CONF_FILENAME -from homeassistant.core import callback -from homeassistant.exceptions import HomeAssistantError -from homeassistant.loader import bind_hass - -from .const import ( - DOMAIN, - ATTR_STREAMS, - ATTR_ENDPOINTS, - CONF_STREAM_SOURCE, - CONF_DURATION, - CONF_LOOKBACK, - SERVICE_RECORD, -) -from .core import PROVIDERS -from .worker import stream_worker -from .hls import async_setup_hls -from .recorder import async_setup_recorder _LOGGER = logging.getLogger(__name__) @@ -104,6 +103,9 @@ 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 + from .recorder import async_setup_recorder + hass.data[DOMAIN] = {} hass.data[DOMAIN][ATTR_ENDPOINTS] = {} hass.data[DOMAIN][ATTR_STREAMS] = {} @@ -181,6 +183,9 @@ class Stream: def start(self): """Start a stream.""" + # Keep import here so that we can import stream integration without installing reqs + from .worker import stream_worker + if self._thread is None or not self._thread.isAlive(): self._thread_quit = threading.Event() self._thread = threading.Thread( diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py index 81335783e1a..9282c2cb855 100644 --- a/homeassistant/components/stream/core.py +++ b/homeassistant/components/stream/core.py @@ -2,17 +2,17 @@ import asyncio from collections import deque import io -from typing import List, Any +from typing import Any, List -import attr from aiohttp import web +import attr -from homeassistant.core import callback from homeassistant.components.http import HomeAssistantView +from homeassistant.core import callback from homeassistant.helpers.event import async_call_later from homeassistant.util.decorator import Registry -from .const import DOMAIN, ATTR_STREAMS +from .const import ATTR_STREAMS, DOMAIN PROVIDERS = Registry() diff --git a/homeassistant/components/stream/hls.py b/homeassistant/components/stream/hls.py index c9e62f53a57..2cd98c0a00f 100644 --- a/homeassistant/components/stream/hls.py +++ b/homeassistant/components/stream/hls.py @@ -5,7 +5,7 @@ from homeassistant.core import callback from homeassistant.util.dt import utcnow from .const import FORMAT_CONTENT_TYPE -from .core import StreamView, StreamOutput, PROVIDERS +from .core import PROVIDERS, StreamOutput, StreamView @callback diff --git a/homeassistant/components/stream/recorder.py b/homeassistant/components/stream/recorder.py index cd25896aff3..1dd90b8b804 100644 --- a/homeassistant/components/stream/recorder.py +++ b/homeassistant/components/stream/recorder.py @@ -1,10 +1,13 @@ """Provide functionality to record stream.""" + import threading from typing import List +import av + from homeassistant.core import callback -from .core import Segment, StreamOutput, PROVIDERS +from .core import PROVIDERS, Segment, StreamOutput @callback @@ -14,8 +17,6 @@ def async_setup_recorder(hass): def recorder_save_worker(file_out: str, segments: List[Segment]): """Handle saving stream.""" - import av - output = av.open(file_out, "w", options={"movflags": "frag_keyframe"}) output_v = None diff --git a/homeassistant/components/stream/services.yaml b/homeassistant/components/stream/services.yaml index e69de29bb2d..c3b25e06348 100644 --- a/homeassistant/components/stream/services.yaml +++ b/homeassistant/components/stream/services.yaml @@ -0,0 +1,15 @@ +record: + description: Make a .mp4 recording from a provided stream. + fields: + stream_source: + description: The input source for the stream. + example: "rtsp://my.stream.feed:554" + filename: + description: The file name string. + example: "/tmp/my_stream.mp4" + duration: + description: "Target recording length (in seconds). Default: 30" + example: 30 + lookback: + description: "Target lookback period (in seconds) to include in addition to duration. Only available if there is currently an active HLS stream for stream_source. Default: 0" + example: 5 \ No newline at end of file diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index e87221304a3..99ffd833eb3 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -3,6 +3,8 @@ from fractions import Fraction import io import logging +import av + from .const import AUDIO_SAMPLE_RATE from .core import Segment, StreamBuffer @@ -11,9 +13,8 @@ _LOGGER = logging.getLogger(__name__) def generate_audio_frame(): """Generate a blank audio frame.""" - from av import AudioFrame - audio_frame = AudioFrame(format="dbl", layout="mono", samples=1024) + audio_frame = av.AudioFrame(format="dbl", layout="mono", samples=1024) # audio_bytes = b''.join(b'\x00\x00\x00\x00\x00\x00\x00\x00' # for i in range(0, 1024)) audio_bytes = b"\x00\x00\x00\x00\x00\x00\x00\x00" * 1024 @@ -25,7 +26,6 @@ def generate_audio_frame(): def create_stream_buffer(stream_output, video_stream, audio_frame): """Create a new StreamBuffer.""" - import av a_packet = None segment = io.BytesIO() @@ -45,7 +45,6 @@ def create_stream_buffer(stream_output, video_stream, audio_frame): def stream_worker(hass, stream, quit_event): """Handle consuming streams.""" - import av container = av.open(stream.source, options=stream.options) try: diff --git a/homeassistant/components/stride/__init__.py b/homeassistant/components/stride/__init__.py deleted file mode 100644 index 461a3ee744f..00000000000 --- a/homeassistant/components/stride/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The stride component.""" diff --git a/homeassistant/components/stride/manifest.json b/homeassistant/components/stride/manifest.json deleted file mode 100644 index 840984ad073..00000000000 --- a/homeassistant/components/stride/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "domain": "stride", - "name": "Stride", - "documentation": "https://www.home-assistant.io/integrations/stride", - "requirements": [ - "pystride==0.1.7" - ], - "dependencies": [], - "codeowners": [] -} diff --git a/homeassistant/components/stride/notify.py b/homeassistant/components/stride/notify.py deleted file mode 100644 index 082d986491a..00000000000 --- a/homeassistant/components/stride/notify.py +++ /dev/null @@ -1,84 +0,0 @@ -"""Stride platform for notify component.""" -import logging - -import voluptuous as vol - -from homeassistant.const import CONF_ROOM, CONF_TOKEN -import homeassistant.helpers.config_validation as cv - -from homeassistant.components.notify import ( - ATTR_DATA, - ATTR_TARGET, - PLATFORM_SCHEMA, - BaseNotificationService, -) - -_LOGGER = logging.getLogger(__name__) - -CONF_PANEL = "panel" -CONF_CLOUDID = "cloudid" - -DEFAULT_PANEL = None - -VALID_PANELS = {"info", "note", "tip", "warning", None} - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_CLOUDID): cv.string, - vol.Required(CONF_ROOM): cv.string, - vol.Required(CONF_TOKEN): cv.string, - vol.Optional(CONF_PANEL, default=DEFAULT_PANEL): vol.In(VALID_PANELS), - } -) - - -def get_service(hass, config, discovery_info=None): - """Get the Stride notification service.""" - return StrideNotificationService( - config[CONF_TOKEN], config[CONF_ROOM], config[CONF_PANEL], config[CONF_CLOUDID] - ) - - -class StrideNotificationService(BaseNotificationService): - """Implement the notification service for Stride.""" - - def __init__(self, token, default_room, default_panel, cloudid): - """Initialize the service.""" - self._token = token - self._default_room = default_room - self._default_panel = default_panel - self._cloudid = cloudid - - from stride import Stride - - self._stride = Stride(self._cloudid, access_token=self._token) - - def send_message(self, message="", **kwargs): - """Send a message.""" - panel = self._default_panel - - if kwargs.get(ATTR_DATA) is not None: - data = kwargs.get(ATTR_DATA) - if (data.get(CONF_PANEL) is not None) and ( - data.get(CONF_PANEL) in VALID_PANELS - ): - panel = data.get(CONF_PANEL) - - message_text = { - "type": "paragraph", - "content": [{"type": "text", "text": message}], - } - panel_text = message_text - if panel is not None: - panel_text = { - "type": "panel", - "attrs": {"panelType": panel}, - "content": [message_text], - } - - message_doc = {"body": {"version": 1, "type": "doc", "content": [panel_text]}} - - targets = kwargs.get(ATTR_TARGET, [self._default_room]) - - for target in targets: - self._stride.message_room(target, message_doc) diff --git a/homeassistant/components/sun/__init__.py b/homeassistant/components/sun/__init__.py index 7d883e273e5..e848449e61e 100644 --- a/homeassistant/components/sun/__init__.py +++ b/homeassistant/components/sun/__init__.py @@ -18,7 +18,7 @@ from homeassistant.helpers.sun import ( from homeassistant.util import dt as dt_util -# mypy: allow-untyped-calls, allow-untyped-defs +# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/supla/__init__.py b/homeassistant/components/supla/__init__.py index 86e763142e6..4293f187f5b 100644 --- a/homeassistant/components/supla/__init__.py +++ b/homeassistant/components/supla/__init__.py @@ -9,8 +9,6 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform from homeassistant.helpers.entity import Entity -REQUIREMENTS = ["pysupla==0.0.3"] - _LOGGER = logging.getLogger(__name__) DOMAIN = "supla" diff --git a/homeassistant/components/switch/.translations/de.json b/homeassistant/components/switch/.translations/de.json new file mode 100644 index 00000000000..5396facadd7 --- /dev/null +++ b/homeassistant/components/switch/.translations/de.json @@ -0,0 +1,19 @@ +{ + "device_automation": { + "action_type": { + "toggle": "{entity_name} umschalten", + "turn_off": "Schalte {entity_name} aus.", + "turn_on": "Schalte {entity_name} ein." + }, + "condition_type": { + "is_off": "{entity_name} ist ausgeschaltet", + "is_on": "{entity_name} ist eingeschaltet", + "turn_off": "{entity_name} ausgeschaltet", + "turn_on": "{entity_name} eingeschaltet" + }, + "trigger_type": { + "turned_off": "{entity_name} ausgeschaltet", + "turned_on": "{entity_name} eingeschaltet" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/lv.json b/homeassistant/components/switch/.translations/lv.json new file mode 100644 index 00000000000..784a9a37afa --- /dev/null +++ b/homeassistant/components/switch/.translations/lv.json @@ -0,0 +1,12 @@ +{ + "device_automation": { + "condition_type": { + "turn_off": "{entity_name} tika izsl\u0113gta", + "turn_on": "{entity_name} tika iesl\u0113gta" + }, + "trigger_type": { + "turned_off": "{entity_name} tika izsl\u0113gta", + "turned_on": "{entity_name} tika iesl\u0113gta" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/pl.json b/homeassistant/components/switch/.translations/pl.json index 09b43f4100d..3d352aa2b58 100644 --- a/homeassistant/components/switch/.translations/pl.json +++ b/homeassistant/components/switch/.translations/pl.json @@ -12,8 +12,8 @@ "turn_on": "prze\u0142\u0105cznik {entity_name} w\u0142\u0105czony" }, "trigger_type": { - "turned_off": "wy\u0142\u0105czenie {entity_name}", - "turned_on": "w\u0142\u0105czenie {entity_name}" + "turned_off": "nast\u0105pi wy\u0142\u0105czenie {entity_name}", + "turned_on": "nast\u0105pi w\u0142\u0105czenie {entity_name}" } } } \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/ru.json b/homeassistant/components/switch/.translations/ru.json index cd5cbc0d6a1..74503eea60b 100644 --- a/homeassistant/components/switch/.translations/ru.json +++ b/homeassistant/components/switch/.translations/ru.json @@ -6,14 +6,14 @@ "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c {entity_name}" }, "condition_type": { - "is_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", - "is_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e", - "turn_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", - "turn_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + "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", + "turn_off": "{entity_name} \u0432 \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "turn_on": "{entity_name} \u0432\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438" }, "trigger_type": { - "turned_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435", - "turned_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435" + "turned_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f", + "turned_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f" } } } \ No newline at end of file diff --git a/homeassistant/components/switch/device_condition.py b/homeassistant/components/switch/device_condition.py index 5825a3ba91a..56f8f6c196e 100644 --- a/homeassistant/components/switch/device_condition.py +++ b/homeassistant/components/switch/device_condition.py @@ -1,5 +1,5 @@ """Provides device conditions for switches.""" -from typing import List +from typing import Dict, List import voluptuous as vol from homeassistant.core import HomeAssistant @@ -21,9 +21,16 @@ def async_condition_from_config( """Evaluate state based on configuration.""" if config_validation: config = CONDITION_SCHEMA(config) - return toggle_entity.async_condition_from_config(config, config_validation) + return toggle_entity.async_condition_from_config(config) -async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[dict]: +async def async_get_conditions( + hass: HomeAssistant, device_id: str +) -> List[Dict[str, str]]: """List device conditions.""" return await toggle_entity.async_get_conditions(hass, device_id, DOMAIN) + + +async def async_get_condition_capabilities(hass: HomeAssistant, config: dict) -> dict: + """List condition capabilities.""" + return await toggle_entity.async_get_condition_capabilities(hass, config) diff --git a/homeassistant/components/switch/device_trigger.py b/homeassistant/components/switch/device_trigger.py index 22a016e49b9..7f0458b3e9f 100644 --- a/homeassistant/components/switch/device_trigger.py +++ b/homeassistant/components/switch/device_trigger.py @@ -32,6 +32,6 @@ async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: return await toggle_entity.async_get_triggers(hass, device_id, DOMAIN) -async def async_get_trigger_capabilities(hass: HomeAssistant, trigger: dict) -> dict: +async def async_get_trigger_capabilities(hass: HomeAssistant, config: dict) -> dict: """List trigger capabilities.""" - return await toggle_entity.async_get_trigger_capabilities(hass, trigger) + return await toggle_entity.async_get_trigger_capabilities(hass, config) diff --git a/homeassistant/components/switch/light.py b/homeassistant/components/switch/light.py index 8f3b5d87f8c..b0abf957991 100644 --- a/homeassistant/components/switch/light.py +++ b/homeassistant/components/switch/light.py @@ -21,7 +21,7 @@ from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.components.light import PLATFORM_SCHEMA, Light -# mypy: allow-untyped-calls, allow-untyped-defs +# 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 new file mode 100644 index 00000000000..7ed1f70cb97 --- /dev/null +++ b/homeassistant/components/switch/reproduce_state.py @@ -0,0 +1,61 @@ +"""Reproduce an Switch state.""" +import asyncio +import logging +from typing import Iterable, Optional + +from homeassistant.const import ( + ATTR_ENTITY_ID, + STATE_ON, + STATE_OFF, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.core import Context, State +from homeassistant.helpers.typing import HomeAssistantType + +from . import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +VALID_STATES = {STATE_ON, STATE_OFF} + + +async def _async_reproduce_state( + hass: HomeAssistantType, state: State, context: Optional[Context] = None +) -> None: + """Reproduce a single state.""" + cur_state = hass.states.get(state.entity_id) + + if cur_state is None: + _LOGGER.warning("Unable to find entity %s", state.entity_id) + return + + if state.state not in VALID_STATES: + _LOGGER.warning( + "Invalid state specified for %s: %s", state.entity_id, state.state + ) + return + + # Return if we are already at the right state. + if cur_state.state == state.state: + return + + service_data = {ATTR_ENTITY_ID: state.entity_id} + + if state.state == STATE_ON: + service = SERVICE_TURN_ON + elif state.state == STATE_OFF: + service = SERVICE_TURN_OFF + + await hass.services.async_call( + DOMAIN, service, service_data, context=context, blocking=True + ) + + +async def async_reproduce_states( + hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None +) -> None: + """Reproduce Switch states.""" + await asyncio.gather( + *(_async_reproduce_state(hass, state, context) for state in states) + ) diff --git a/homeassistant/components/switchmate/switch.py b/homeassistant/components/switchmate/switch.py index 950a8a67930..6abbfd5fae5 100644 --- a/homeassistant/components/switchmate/switch.py +++ b/homeassistant/components/switchmate/switch.py @@ -1,12 +1,14 @@ """Support for Switchmate.""" -import logging from datetime import timedelta +import logging +# pylint: disable=import-error, no-member, no-value-for-parameter +import switchmate 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 _LOGGER = logging.getLogger(__name__) @@ -37,8 +39,6 @@ class SwitchmateEntity(SwitchDevice): def __init__(self, mac, name, flip_on_off) -> None: """Initialize the Switchmate.""" - # pylint: disable=import-error, no-member, no-value-for-parameter - import switchmate self._mac = mac self._name = name diff --git a/homeassistant/components/syncthru/manifest.json b/homeassistant/components/syncthru/manifest.json index 79e6f8e5571..41ac1024a85 100644 --- a/homeassistant/components/syncthru/manifest.json +++ b/homeassistant/components/syncthru/manifest.json @@ -3,7 +3,7 @@ "name": "Syncthru", "documentation": "https://www.home-assistant.io/integrations/syncthru", "requirements": [ - "pysyncthru==0.4.3" + "pysyncthru==0.5.0" ], "dependencies": [], "codeowners": ["@nielstron"] diff --git a/homeassistant/components/synology/camera.py b/homeassistant/components/synology/camera.py index 5594a4b3c9a..8c176f48803 100644 --- a/homeassistant/components/synology/camera.py +++ b/homeassistant/components/synology/camera.py @@ -105,6 +105,7 @@ class SynologyCamera(Camera): """Return true if the device is recording.""" return self._camera.is_recording + @property def should_poll(self): """Update the recording state periodically.""" return True diff --git a/homeassistant/components/synology_srm/device_tracker.py b/homeassistant/components/synology_srm/device_tracker.py index b45b393e332..36306efa93e 100644 --- a/homeassistant/components/synology_srm/device_tracker.py +++ b/homeassistant/components/synology_srm/device_tracker.py @@ -4,9 +4,10 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.synology_srm/ """ import logging + +import synology_srm import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, @@ -14,12 +15,13 @@ from homeassistant.components.device_tracker import ( ) from homeassistant.const import ( CONF_HOST, - CONF_USERNAME, CONF_PASSWORD, CONF_PORT, CONF_SSL, + CONF_USERNAME, CONF_VERIFY_SSL, ) +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -52,7 +54,6 @@ class SynologySrmDeviceScanner(DeviceScanner): def __init__(self, config): """Initialize the scanner.""" - import synology_srm self.client = synology_srm.Client( host=config[CONF_HOST], diff --git a/homeassistant/components/syslog/notify.py b/homeassistant/components/syslog/notify.py index d7696e43bef..67d4882e5c3 100644 --- a/homeassistant/components/syslog/notify.py +++ b/homeassistant/components/syslog/notify.py @@ -1,5 +1,6 @@ """Syslog notification service.""" import logging +import syslog import voluptuous as vol @@ -67,7 +68,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def get_service(hass, config, discovery_info=None): """Get the syslog notification service.""" - import syslog facility = getattr(syslog, SYSLOG_FACILITY[config.get(CONF_FACILITY)]) option = getattr(syslog, SYSLOG_OPTION[config.get(CONF_OPTION)]) @@ -87,7 +87,6 @@ class SyslogNotificationService(BaseNotificationService): def send_message(self, message="", **kwargs): """Send a message to a user.""" - import syslog title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index ad2072baaa5..53c5c104cd1 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -3,6 +3,7 @@ import logging import os import socket +import psutil import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -134,8 +135,6 @@ class SystemMonitorSensor(Entity): def update(self): """Get the latest system information.""" - import psutil - if self.type == "disk_use_percent": self._state = psutil.disk_usage(self.argument).percent elif self.type == "disk_use": @@ -219,8 +218,8 @@ class SystemMonitorSensor(Entity): dt_util.utc_from_timestamp(psutil.boot_time()) ).isoformat() elif self.type == "load_1m": - self._state = os.getloadavg()[0] + self._state = round(os.getloadavg()[0], 2) elif self.type == "load_5m": - self._state = os.getloadavg()[1] + self._state = round(os.getloadavg()[1], 2) elif self.type == "load_15m": - self._state = os.getloadavg()[2] + self._state = round(os.getloadavg()[2], 2) diff --git a/homeassistant/components/tahoma/__init__.py b/homeassistant/components/tahoma/__init__.py index 4400df6db96..6bcc783400c 100644 --- a/homeassistant/components/tahoma/__init__.py +++ b/homeassistant/components/tahoma/__init__.py @@ -42,6 +42,7 @@ TAHOMA_TYPES = { "io:RollerShutterUnoIOComponent": "cover", "io:RollerShutterVeluxIOComponent": "cover", "io:RollerShutterWithLowSpeedManagementIOComponent": "cover", + "io:SomfyBasicContactIOSystemSensor": "sensor", "io:SomfyContactIOSystemSensor": "sensor", "io:VerticalExteriorAwningIOComponent": "cover", "io:WindowOpenerVeluxIOComponent": "cover", diff --git a/homeassistant/components/tahoma/sensor.py b/homeassistant/components/tahoma/sensor.py index 0ed3879cc7a..5279b160d9c 100644 --- a/homeassistant/components/tahoma/sensor.py +++ b/homeassistant/components/tahoma/sensor.py @@ -44,6 +44,8 @@ class TahomaSensor(TahomaDevice, Entity): return None if self.tahoma_device.type == "io:SomfyContactIOSystemSensor": return None + if self.tahoma_device.type == "io:SomfyBasicContactIOSystemSensor": + return None if self.tahoma_device.type == "io:LightIOSystemSensor": return "lx" if self.tahoma_device.type == "Humidity Sensor": @@ -66,6 +68,11 @@ class TahomaSensor(TahomaDevice, Entity): self._available = bool( self.tahoma_device.active_states.get("core:StatusState") == "available" ) + if self.tahoma_device.type == "io:SomfyBasicContactIOSystemSensor": + self.current_value = self.tahoma_device.active_states["core:ContactState"] + self._available = bool( + self.tahoma_device.active_states.get("core:StatusState") == "available" + ) if self.tahoma_device.type == "rtds:RTDSContactSensor": self.current_value = self.tahoma_device.active_states["core:ContactState"] self._available = True diff --git a/homeassistant/components/ted5000/sensor.py b/homeassistant/components/ted5000/sensor.py index ea0963a092e..e0025a050c3 100644 --- a/homeassistant/components/ted5000/sensor.py +++ b/homeassistant/components/ted5000/sensor.py @@ -1,9 +1,10 @@ -"""Support gathering ted500 information.""" -import logging +"""Support gathering ted5000 information.""" from datetime import timedelta +import logging import requests import voluptuous as vol +import xmltodict from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, POWER_WATT @@ -94,7 +95,6 @@ class Ted5000Gateway: @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data from the Ted5000 XML API.""" - import xmltodict try: request = requests.get(self.url, timeout=10) diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index a36f41edf3b..7acf4985def 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -7,6 +7,16 @@ import logging import requests from requests.auth import HTTPBasicAuth, HTTPDigestAuth +from telegram import ( + Bot, + InlineKeyboardButton, + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, +) +from telegram.error import TelegramError +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 @@ -375,8 +385,6 @@ async def async_setup(hass, config): def initialize_bot(p_config): """Initialize telegram bot with proxy support.""" - from telegram import Bot - from telegram.utils.request import Request api_key = p_config.get(CONF_API_KEY) proxy_url = p_config.get(CONF_PROXY_URL) @@ -396,7 +404,6 @@ class TelegramNotificationService: def __init__(self, hass, bot, allowed_chat_ids, parser): """Initialize the service.""" - from telegram.parsemode import ParseMode self.allowed_chat_ids = allowed_chat_ids self._default_user = self.allowed_chat_ids[0] @@ -457,7 +464,6 @@ class TelegramNotificationService: - a string like: `/cmd1, /cmd2, /cmd3` - or a string like: `text_b1:/cmd1, text_b2:/cmd2` """ - from telegram import InlineKeyboardButton buttons = [] if isinstance(row_keyboard, str): @@ -507,8 +513,6 @@ class TelegramNotificationService: params[ATTR_REPLY_TO_MSGID] = data[ATTR_REPLY_TO_MSGID] # Keyboards: if ATTR_KEYBOARD in data: - from telegram import ReplyKeyboardMarkup, ReplyKeyboardRemove - keys = data.get(ATTR_KEYBOARD) keys = keys if isinstance(keys, list) else [keys] if keys: @@ -517,9 +521,8 @@ class TelegramNotificationService: ) else: params[ATTR_REPLYMARKUP] = ReplyKeyboardRemove(True) - elif ATTR_KEYBOARD_INLINE in data: - from telegram import InlineKeyboardMarkup + elif ATTR_KEYBOARD_INLINE in data: keys = data.get(ATTR_KEYBOARD_INLINE) keys = keys if isinstance(keys, list) else [keys] params[ATTR_REPLYMARKUP] = InlineKeyboardMarkup( @@ -529,7 +532,6 @@ class TelegramNotificationService: def _send_msg(self, func_send, msg_error, *args_msg, **kwargs_msg): """Send one message.""" - from telegram.error import TelegramError try: out = func_send(*args_msg, **kwargs_msg) diff --git a/homeassistant/components/telegram_bot/polling.py b/homeassistant/components/telegram_bot/polling.py index 7ca486e33b2..314cb31a373 100644 --- a/homeassistant/components/telegram_bot/polling.py +++ b/homeassistant/components/telegram_bot/polling.py @@ -1,6 +1,10 @@ """Support for Telegram bot using polling.""" import logging +from telegram import Update +from telegram.error import TelegramError, TimedOut, NetworkError, RetryAfter +from telegram.ext import Updater, Handler + from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback @@ -32,8 +36,6 @@ async def async_setup_platform(hass, config): def process_error(bot, update, error): """Telegram bot error handler.""" - from telegram.error import TelegramError, TimedOut, NetworkError, RetryAfter - try: raise error except (TimedOut, NetworkError, RetryAfter): @@ -45,8 +47,6 @@ def process_error(bot, update, error): def message_handler(handler): """Create messages handler.""" - from telegram import Update - from telegram.ext import Handler class MessageHandler(Handler): """Telegram bot message handler.""" @@ -72,7 +72,6 @@ class TelegramPoll(BaseTelegramBotEntity): def __init__(self, bot, hass, allowed_chat_ids): """Initialize the polling instance.""" - from telegram.ext import Updater BaseTelegramBotEntity.__init__(self, hass, allowed_chat_ids) diff --git a/homeassistant/components/telegram_bot/webhooks.py b/homeassistant/components/telegram_bot/webhooks.py index c71510eddd9..16da2e741e4 100644 --- a/homeassistant/components/telegram_bot/webhooks.py +++ b/homeassistant/components/telegram_bot/webhooks.py @@ -2,6 +2,8 @@ import datetime as dt import logging +from telegram.error import TimedOut + from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.const import KEY_REAL_IP from homeassistant.const import ( @@ -26,7 +28,6 @@ REMOVE_HANDLER_URL = "" async def async_setup_platform(hass, config): """Set up the Telegram webhooks platform.""" - import telegram bot = initialize_bot(config) @@ -55,7 +56,7 @@ async def async_setup_platform(hass, config): while retry_num < 3: try: return bot.setWebhook(handler_url, timeout=5) - except telegram.error.TimedOut: + except TimedOut: retry_num += 1 _LOGGER.warning("Timeout trying to set webhook (retry #%d)", retry_num) diff --git a/homeassistant/components/tellduslive/.translations/ru.json b/homeassistant/components/tellduslive/.translations/ru.json index 9d3c97ad902..41dc39146e8 100644 --- a/homeassistant/components/tellduslive/.translations/ru.json +++ b/homeassistant/components/tellduslive/.translations/ru.json @@ -4,15 +4,15 @@ "already_setup": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "authorize_url_fail": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", "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.", - "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430" + "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "error": { - "auth_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438, \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443" + "auth_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438, \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443." }, "step": { "auth": { - "description": "\u0414\u043b\u044f \u0442\u043e\u0433\u043e, \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0430\u043a\u043a\u0430\u0443\u043d\u0442 TelldusLive:\n 1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u043e\u0439 \u043d\u0438\u0436\u0435\n 2. \u0412\u043e\u0439\u0434\u0438\u0442\u0435 \u0432 Telldus Live\n 3. Authorize **{app_name}** (\u043d\u0430\u0436\u043c\u0438\u0442\u0435 **Yes**).\n 4. \u0412\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\n [\u0421\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 TelldusLive]({auth_url})", - "title": "\u0412\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0432\u0445\u043e\u0434 \u0432 TelldusLive" + "description": "\u0414\u043b\u044f \u0442\u043e\u0433\u043e, \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0430\u043a\u043a\u0430\u0443\u043d\u0442 Telldus Live:\n 1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u043e\u0439 \u043d\u0438\u0436\u0435\n 2. \u0412\u043e\u0439\u0434\u0438\u0442\u0435 \u0432 Telldus Live\n 3. Authorize **{app_name}** (\u043d\u0430\u0436\u043c\u0438\u0442\u0435 **Yes**).\n 4. \u0412\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\n [\u0421\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 Telldus Live]({auth_url})", + "title": "Telldus Live" }, "user": { "data": { diff --git a/homeassistant/components/tellstick/__init__.py b/homeassistant/components/tellstick/__init__.py index 526db5e73df..e7f341c90b2 100644 --- a/homeassistant/components/tellstick/__init__.py +++ b/homeassistant/components/tellstick/__init__.py @@ -2,13 +2,22 @@ import logging import threading +from tellcore.constants import ( + TELLSTICK_DIM, + TELLSTICK_TURNOFF, + TELLSTICK_TURNON, + TELLSTICK_UP, +) +from tellcore.library import TelldusError +from tellcore.telldus import AsyncioCallbackDispatcher, TelldusCore +from tellcorenet import TellCoreClient import voluptuous as vol -from homeassistant.helpers import discovery +from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback -from homeassistant.const import EVENT_HOMEASSISTANT_STOP, CONF_HOST, CONF_PORT -from homeassistant.helpers.entity import Entity +from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -73,10 +82,6 @@ def _discover(hass, config, component_name, found_tellcore_devices): def setup(hass, config): """Set up the Tellstick component.""" - from tellcore.constants import TELLSTICK_DIM, TELLSTICK_UP - from tellcore.telldus import AsyncioCallbackDispatcher - from tellcore.telldus import TelldusCore - from tellcorenet import TellCoreClient conf = config.get(DOMAIN, {}) net_host = conf.get(CONF_HOST) @@ -219,7 +224,6 @@ class TellstickDevice(Entity): def _send_repeated_command(self): """Send a tellstick command once and decrease the repeat count.""" - from tellcore.library import TelldusError with TELLSTICK_LOCK: if self._repeats_left > 0: @@ -259,11 +263,6 @@ class TellstickDevice(Entity): def _update_model_from_command(self, tellcore_command, tellcore_data): """Update the model, from a sent tellcore command and data.""" - from tellcore.constants import ( - TELLSTICK_TURNON, - TELLSTICK_TURNOFF, - TELLSTICK_DIM, - ) if tellcore_command not in [TELLSTICK_TURNON, TELLSTICK_TURNOFF, TELLSTICK_DIM]: _LOGGER.debug("Unhandled tellstick command: %d", tellcore_command) @@ -289,12 +288,6 @@ class TellstickDevice(Entity): def _update_from_tellcore(self): """Read the current state of the device from the tellcore library.""" - from tellcore.library import TelldusError - from tellcore.constants import ( - TELLSTICK_TURNON, - TELLSTICK_TURNOFF, - TELLSTICK_DIM, - ) with TELLSTICK_LOCK: try: diff --git a/homeassistant/components/tellstick/sensor.py b/homeassistant/components/tellstick/sensor.py index 98d162d6d81..1a55e67ac43 100644 --- a/homeassistant/components/tellstick/sensor.py +++ b/homeassistant/components/tellstick/sensor.py @@ -1,13 +1,15 @@ """Support for Tellstick sensors.""" -import logging from collections import namedtuple +import logging +from tellcore import telldus +import tellcore.constants as tellcore_constants import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import TEMP_CELSIUS, CONF_ID, CONF_NAME, CONF_PROTOCOL -from homeassistant.helpers.entity import Entity +from homeassistant.const import CONF_ID, CONF_NAME, CONF_PROTOCOL, TEMP_CELSIUS import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -48,8 +50,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Tellstick sensors.""" - from tellcore import telldus - import tellcore.constants as tellcore_constants sensor_value_descriptions = { tellcore_constants.TELLSTICK_TEMPERATURE: DatatypeDescription( diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py index 42790e618d9..606f18e5fe1 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -270,7 +270,7 @@ class TemplateFan(FanEntity): # pylint: disable=arguments-differ async def async_turn_on(self, speed: str = None) -> None: """Turn on the fan.""" - await self._on_script.async_run(context=self._context) + await self._on_script.async_run({ATTR_SPEED: speed}, context=self._context) self._state = STATE_ON if speed is not None: diff --git a/homeassistant/components/tensorflow/image_processing.py b/homeassistant/components/tensorflow/image_processing.py index 65e20f558a7..ea73d52fe4a 100644 --- a/homeassistant/components/tensorflow/image_processing.py +++ b/homeassistant/components/tensorflow/image_processing.py @@ -1,8 +1,11 @@ """Support for performing TensorFlow classification on images.""" +import io import logging import os import sys +from PIL import Image, ImageDraw +import numpy as np import voluptuous as vol from homeassistant.components.image_processing import ( @@ -88,6 +91,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): # Verify that the TensorFlow Object Detection API is pre-installed # pylint: disable=unused-import,unused-variable os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2" + # These imports shouldn't be moved to the top, because they depend on code from the model_dir. + # (The model_dir is created during the manual setup process. See integration docs.) import tensorflow as tf # noqa from object_detection.utils import label_map_util # noqa except ImportError: @@ -236,9 +241,6 @@ class TensorFlowImageProcessor(ImageProcessingEntity): } def _save_image(self, image, matches, paths): - from PIL import Image, ImageDraw - import io - img = Image.open(io.BytesIO(bytearray(image))).convert("RGB") img_width, img_height = img.size draw = ImageDraw.Draw(img) @@ -280,7 +282,6 @@ class TensorFlowImageProcessor(ImageProcessingEntity): def process_image(self, image): """Process the image.""" - import numpy as np try: import cv2 # pylint: disable=import-error @@ -289,9 +290,6 @@ class TensorFlowImageProcessor(ImageProcessingEntity): inp = img[:, :, [2, 1, 0]] # BGR->RGB inp_expanded = inp.reshape(1, inp.shape[0], inp.shape[1], 3) except ImportError: - from PIL import Image - import io - img = Image.open(io.BytesIO(bytearray(image))).convert("RGB") img.thumbnail((460, 460), Image.ANTIALIAS) img_width, img_height = img.size diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index e7d35829ffb..e0a8728b295 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/tensorflow", "requirements": [ "tensorflow==1.13.2", - "numpy==1.17.1", + "numpy==1.17.3", "protobuf==3.6.1" ], "dependencies": [], diff --git a/homeassistant/components/tesla/__init__.py b/homeassistant/components/tesla/__init__.py index 4c90f0784af..a08112d66b3 100644 --- a/homeassistant/components/tesla/__init__.py +++ b/homeassistant/components/tesla/__init__.py @@ -3,6 +3,8 @@ from collections import defaultdict import logging import voluptuous as vol +from teslajsonpy import Controller as teslaAPI, TeslaException + from homeassistant.const import ( ATTR_BATTERY_LEVEL, @@ -52,8 +54,6 @@ TESLA_COMPONENTS = [ def setup(hass, base_config): """Set up of Tesla component.""" - from teslajsonpy import Controller as teslaAPI, TeslaException - config = base_config.get(DOMAIN) email = config.get(CONF_USERNAME) diff --git a/homeassistant/components/tesla/manifest.json b/homeassistant/components/tesla/manifest.json index 4071178c7c3..87d76c16f05 100644 --- a/homeassistant/components/tesla/manifest.json +++ b/homeassistant/components/tesla/manifest.json @@ -2,11 +2,7 @@ "domain": "tesla", "name": "Tesla", "documentation": "https://www.home-assistant.io/integrations/tesla", - "requirements": [ - "teslajsonpy==0.0.25" - ], + "requirements": ["teslajsonpy==0.0.26"], "dependencies": [], - "codeowners": [ - "@zabuldon" - ] + "codeowners": ["@zabuldon"] } diff --git a/homeassistant/components/tesla/switch.py b/homeassistant/components/tesla/switch.py index 19ac76018ff..985194f87b2 100644 --- a/homeassistant/components/tesla/switch.py +++ b/homeassistant/components/tesla/switch.py @@ -11,11 +11,12 @@ _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Tesla switch platform.""" - controller = hass.data[TESLA_DOMAIN]["devices"]["controller"] + 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) @@ -72,10 +73,42 @@ class RangeSwitch(TeslaDevice, SwitchDevice): @property def is_on(self): """Get whether the switch is in on state.""" - return self._state == STATE_ON + return self._state def update(self): """Update the state of the switch.""" _LOGGER.debug("Updating state for: %s", self._name) self.tesla_device.update() - self._state = STATE_ON if self.tesla_device.is_maxrange() else STATE_OFF + self._state = bool(self.tesla_device.is_maxrange()) + + +class UpdateSwitch(TeslaDevice, SwitchDevice): + """Representation of a Tesla update switch.""" + + def __init__(self, tesla_device, controller): + """Initialise of the switch.""" + self._state = None + super().__init__(tesla_device, controller) + self._name = self._name.replace("charger", "update") + self.tesla_id = self.tesla_id.replace("charger", "update") + + def turn_on(self, **kwargs): + """Send the on command.""" + _LOGGER.debug("Enable updates: %s %s", self._name, self.tesla_device.id()) + self.controller.set_updates(self.tesla_device.id(), True) + + def turn_off(self, **kwargs): + """Send the off command.""" + _LOGGER.debug("Disable updates: %s %s", self._name, self.tesla_device.id()) + self.controller.set_updates(self.tesla_device.id(), False) + + @property + def is_on(self): + """Get whether the switch is in on state.""" + return self._state + + def update(self): + """Update the state of the switch.""" + car_id = self.tesla_device.id() + _LOGGER.debug("Updating state for: %s %s", self._name, car_id) + self._state = bool(self.controller.get_updates(car_id)) diff --git a/homeassistant/components/thermoworks_smoke/sensor.py b/homeassistant/components/thermoworks_smoke/sensor.py index 70a16287fcc..d5af021108a 100644 --- a/homeassistant/components/thermoworks_smoke/sensor.py +++ b/homeassistant/components/thermoworks_smoke/sensor.py @@ -9,18 +9,21 @@ https://home-assistant.io/components/sensor.thermoworks_smoke/ import logging from requests import RequestException +from requests.exceptions import HTTPError +from stringcase import camelcase, snakecase +import thermoworks_smoke import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - TEMP_FAHRENHEIT, - CONF_EMAIL, - CONF_PASSWORD, - CONF_MONITORED_CONDITIONS, - CONF_EXCLUDE, ATTR_BATTERY_LEVEL, + CONF_EMAIL, + CONF_EXCLUDE, + CONF_MONITORED_CONDITIONS, + CONF_PASSWORD, + TEMP_FAHRENHEIT, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -65,8 +68,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the thermoworks sensor.""" - import thermoworks_smoke - from requests.exceptions import HTTPError email = config[CONF_EMAIL] password = config[CONF_PASSWORD] @@ -144,7 +145,6 @@ class ThermoworksSmokeSensor(Entity): def update(self): """Get the monitored data from firebase.""" - from stringcase import camelcase, snakecase try: values = self.mgr.data(self.serial) diff --git a/homeassistant/components/thingspeak/__init__.py b/homeassistant/components/thingspeak/__init__.py index 0893b3311bb..1870a317752 100644 --- a/homeassistant/components/thingspeak/__init__.py +++ b/homeassistant/components/thingspeak/__init__.py @@ -2,6 +2,7 @@ import logging from requests.exceptions import RequestException +import thingspeak import voluptuous as vol from homeassistant.const import ( @@ -36,7 +37,6 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): """Set up the Thingspeak environment.""" - import thingspeak conf = config[DOMAIN] api_key = conf.get(CONF_API_KEY) diff --git a/homeassistant/components/tibber/__init__.py b/homeassistant/components/tibber/__init__.py index 0622b70f127..df56989714f 100644 --- a/homeassistant/components/tibber/__init__.py +++ b/homeassistant/components/tibber/__init__.py @@ -3,12 +3,13 @@ import asyncio import logging import aiohttp +import tibber import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.const import EVENT_HOMEASSISTANT_STOP, CONF_ACCESS_TOKEN, CONF_NAME +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME, EVENT_HOMEASSISTANT_STOP from homeassistant.helpers import discovery from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv from homeassistant.util import dt as dt_util DOMAIN = "tibber" @@ -25,8 +26,6 @@ async def async_setup(hass, config): """Set up the Tibber component.""" conf = config.get(DOMAIN) - import tibber - tibber_connection = tibber.Tibber( conf[CONF_ACCESS_TOKEN], websession=async_get_clientsession(hass), diff --git a/homeassistant/components/tikteck/light.py b/homeassistant/components/tikteck/light.py index 013e5276f49..6c623f29f18 100644 --- a/homeassistant/components/tikteck/light.py +++ b/homeassistant/components/tikteck/light.py @@ -1,17 +1,18 @@ """Support for Tikteck lights.""" import logging +import tikteck import voluptuous as vol -from homeassistant.const import CONF_DEVICES, CONF_NAME, CONF_PASSWORD from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_HS_COLOR, + PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, Light, - PLATFORM_SCHEMA, ) +from homeassistant.const import CONF_DEVICES, CONF_NAME, CONF_PASSWORD import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util @@ -48,7 +49,6 @@ class TikteckLight(Light): def __init__(self, device): """Initialize the light.""" - import tikteck self._name = device["name"] self._address = device["address"] diff --git a/homeassistant/components/tile/device_tracker.py b/homeassistant/components/tile/device_tracker.py index e8ed5b06d27..924fa913d30 100644 --- a/homeassistant/components/tile/device_tracker.py +++ b/homeassistant/components/tile/device_tracker.py @@ -43,7 +43,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_scanner(hass, config, async_see, discovery_info=None): """Validate the configuration and return a Tile scanner.""" - from pytile import Client + from pytile import async_login websession = aiohttp_client.async_get_clientsession(hass) @@ -52,14 +52,16 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None): ) config_data = await hass.async_add_job(load_json, config_file) if config_data: - client = Client( + client = await async_login( config[CONF_USERNAME], config[CONF_PASSWORD], websession, client_uuid=config_data["client_uuid"], ) else: - client = Client(config[CONF_USERNAME], config[CONF_PASSWORD], websession) + client = await async_login( + config[CONF_USERNAME], config[CONF_PASSWORD], websession + ) config_data = {"client_uuid": client.client_uuid} await hass.async_add_job(save_json, config_file, config_data) diff --git a/homeassistant/components/tile/manifest.json b/homeassistant/components/tile/manifest.json index 5e40c89369a..0dd0b70ef52 100644 --- a/homeassistant/components/tile/manifest.json +++ b/homeassistant/components/tile/manifest.json @@ -3,7 +3,7 @@ "name": "Tile", "documentation": "https://www.home-assistant.io/integrations/tile", "requirements": [ - "pytile==2.0.6" + "pytile==3.0.0" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/timer/reproduce_state.py b/homeassistant/components/timer/reproduce_state.py new file mode 100644 index 00000000000..c765ed7da9c --- /dev/null +++ b/homeassistant/components/timer/reproduce_state.py @@ -0,0 +1,70 @@ +"""Reproduce an Timer state.""" +import asyncio +import logging +from typing import Iterable, Optional + +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import Context, State +from homeassistant.helpers.typing import HomeAssistantType + +from . import ( + ATTR_DURATION, + DOMAIN, + SERVICE_CANCEL, + SERVICE_PAUSE, + SERVICE_START, + STATUS_ACTIVE, + STATUS_IDLE, + STATUS_PAUSED, +) + +_LOGGER = logging.getLogger(__name__) + +VALID_STATES = {STATUS_IDLE, STATUS_ACTIVE, STATUS_PAUSED} + + +async def _async_reproduce_state( + hass: HomeAssistantType, state: State, context: Optional[Context] = None +) -> None: + """Reproduce a single state.""" + cur_state = hass.states.get(state.entity_id) + + if cur_state is None: + _LOGGER.warning("Unable to find entity %s", state.entity_id) + return + + if state.state not in VALID_STATES: + _LOGGER.warning( + "Invalid state specified for %s: %s", state.entity_id, state.state + ) + return + + # Return if we are already at the right state. + if cur_state.state == state.state and cur_state.attributes.get( + ATTR_DURATION + ) == state.attributes.get(ATTR_DURATION): + return + + service_data = {ATTR_ENTITY_ID: state.entity_id} + + if state.state == STATUS_ACTIVE: + service = SERVICE_START + if ATTR_DURATION in state.attributes: + service_data[ATTR_DURATION] = state.attributes[ATTR_DURATION] + elif state.state == STATUS_PAUSED: + service = SERVICE_PAUSE + elif state.state == STATUS_IDLE: + service = SERVICE_CANCEL + + await hass.services.async_call( + DOMAIN, service, service_data, context=context, blocking=True + ) + + +async def async_reproduce_states( + hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None +) -> None: + """Reproduce Timer states.""" + await asyncio.gather( + *(_async_reproduce_state(hass, state, context) for state in states) + ) diff --git a/homeassistant/components/toon/.translations/ru.json b/homeassistant/components/toon/.translations/ru.json index 0eddbe2a151..58e6f53986c 100644 --- a/homeassistant/components/toon/.translations/ru.json +++ b/homeassistant/components/toon/.translations/ru.json @@ -8,7 +8,7 @@ "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\u0435\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": { diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index 075bffb9f26..7aa261564f3 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -3,20 +3,21 @@ import logging import voluptuous as vol -from homeassistant.const import CONF_HOST from homeassistant import config_entries +from homeassistant.const import CONF_HOST import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType, HomeAssistantType from .common import ( - async_discover_devices, - get_static_devices, ATTR_CONFIG, CONF_DIMMER, CONF_DISCOVERY, CONF_LIGHT, CONF_SWITCH, + CONF_STRIP, SmartDevices, + async_discover_devices, + get_static_devices, ) _LOGGER = logging.getLogger(__name__) @@ -36,6 +37,9 @@ CONFIG_SCHEMA = vol.Schema( vol.Optional(CONF_SWITCH, default=[]): vol.All( cv.ensure_list, [TPLINK_HOST_SCHEMA] ), + vol.Optional(CONF_STRIP, default=[]): vol.All( + cv.ensure_list, [TPLINK_HOST_SCHEMA] + ), vol.Optional(CONF_DIMMER, default=[]): vol.All( cv.ensure_list, [TPLINK_HOST_SCHEMA] ), diff --git a/homeassistant/components/tplink/common.py b/homeassistant/components/tplink/common.py index 90895104170..548edc6822c 100644 --- a/homeassistant/components/tplink/common.py +++ b/homeassistant/components/tplink/common.py @@ -1,10 +1,17 @@ """Common code for tplink.""" import asyncio -import logging from datetime import timedelta +import logging from typing import Any, Callable, List -from pyHS100 import SmartBulb, SmartDevice, SmartPlug, SmartDeviceException +from pyHS100 import ( + Discover, + SmartBulb, + SmartDevice, + SmartDeviceException, + SmartPlug, + SmartStrip, +) from homeassistant.helpers.typing import HomeAssistantType @@ -15,6 +22,7 @@ ATTR_CONFIG = "config" CONF_DIMMER = "dimmer" CONF_DISCOVERY = "discovery" CONF_LIGHT = "light" +CONF_STRIP = "strip" CONF_SWITCH = "switch" @@ -49,7 +57,6 @@ class SmartDevices: async def async_get_discoverable_devices(hass): """Return if there are devices that can be discovered.""" - from pyHS100 import Discover def discover(): devs = Discover.discover() @@ -75,7 +82,10 @@ async def async_discover_devices( if existing_devices.has_device_with_host(dev.host): continue - if isinstance(dev, SmartPlug): + if isinstance(dev, SmartStrip): + for plug in dev.plugs.values(): + switches.append(plug) + elif isinstance(dev, SmartPlug): try: if dev.is_dimmable: # Dimmers act as lights lights.append(dev) @@ -100,7 +110,7 @@ def get_static_devices(config_data) -> SmartDevices: lights = [] switches = [] - for type_ in [CONF_LIGHT, CONF_SWITCH, CONF_DIMMER]: + for type_ in [CONF_LIGHT, CONF_SWITCH, CONF_STRIP, CONF_DIMMER]: for entry in config_data[type_]: host = entry["host"] @@ -108,6 +118,9 @@ def get_static_devices(config_data) -> SmartDevices: lights.append(SmartBulb(host)) elif type_ == CONF_SWITCH: switches.append(SmartPlug(host)) + elif type_ == CONF_STRIP: + for plug in SmartStrip(host).plugs.values(): + switches.append(plug) # Dimmers need to be defined as smart plugs to work correctly. elif type_ == CONF_DIMMER: lights.append(SmartPlug(host)) diff --git a/homeassistant/components/tplink/config_flow.py b/homeassistant/components/tplink/config_flow.py index c4888ecee96..40583294bfd 100644 --- a/homeassistant/components/tplink/config_flow.py +++ b/homeassistant/components/tplink/config_flow.py @@ -1,9 +1,9 @@ """Config flow for TP-Link.""" -from homeassistant.helpers import config_entry_flow from homeassistant import config_entries -from .const import DOMAIN -from .common import async_get_discoverable_devices +from homeassistant.helpers import config_entry_flow +from .common import async_get_discoverable_devices +from .const import DOMAIN config_entry_flow.register_discovery_flow( DOMAIN, diff --git a/homeassistant/components/tplink/device_tracker.py b/homeassistant/components/tplink/device_tracker.py deleted file mode 100644 index e7f87074cb4..00000000000 --- a/homeassistant/components/tplink/device_tracker.py +++ /dev/null @@ -1,508 +0,0 @@ -"""Support for TP-Link routers.""" -import base64 -from datetime import datetime -import hashlib -import logging -import re - -from aiohttp.hdrs import ( - ACCEPT, - COOKIE, - PRAGMA, - REFERER, - CONNECTION, - KEEP_ALIVE, - USER_AGENT, - CONTENT_TYPE, - CACHE_CONTROL, - ACCEPT_ENCODING, - ACCEPT_LANGUAGE, -) -import requests -import voluptuous as vol - -from homeassistant.components.device_tracker import ( - DOMAIN, - PLATFORM_SCHEMA, - DeviceScanner, -) -from homeassistant.const import ( - CONF_HOST, - CONF_PASSWORD, - CONF_USERNAME, - HTTP_HEADER_X_REQUESTED_WITH, -) -import homeassistant.helpers.config_validation as cv - -_LOGGER = logging.getLogger(__name__) - -HTTP_HEADER_NO_CACHE = "no-cache" - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_USERNAME): cv.string, - } -) - - -def get_scanner(hass, config): - """ - Validate the configuration and return a TP-Link scanner. - - The default way of integrating devices is to use a pypi - - package, The TplinkDeviceScanner has been refactored - - to depend on a pypi package, the other implementations - - should be gradually migrated in the pypi package - - """ - _LOGGER.warning( - "TP-Link device tracker is unmaintained and will be " - "removed in the future releases if no maintainer is " - "found. If you have interest in this integration, " - "feel free to create a pull request to move this code " - "to a new 'tplink_router' integration and refactoring " - "the device-specific parts to the tplink library" - ) - for cls in [ - TplinkDeviceScanner, - Tplink5DeviceScanner, - Tplink4DeviceScanner, - Tplink3DeviceScanner, - Tplink2DeviceScanner, - Tplink1DeviceScanner, - ]: - scanner = cls(config[DOMAIN]) - if scanner.success_init: - return scanner - - return None - - -class TplinkDeviceScanner(DeviceScanner): - """Queries the router for connected devices.""" - - def __init__(self, config): - """Initialize the scanner.""" - from tplink.tplink import TpLinkClient - - host = config[CONF_HOST] - password = config[CONF_PASSWORD] - username = config[CONF_USERNAME] - - self.success_init = False - try: - self.tplink_client = TpLinkClient(password, host=host, username=username) - - self.last_results = {} - - self.success_init = self._update_info() - except requests.exceptions.RequestException: - _LOGGER.debug("RequestException in %s", __class__.__name__) - - def scan_devices(self): - """Scan for new devices and return a list with found device IDs.""" - self._update_info() - return self.last_results.keys() - - def get_device_name(self, device): - """Get the name of the device.""" - return self.last_results.get(device) - - def _update_info(self): - """Ensure the information from the TP-Link router is up to date. - - Return boolean if scanning successful. - """ - _LOGGER.info("Loading wireless clients...") - result = self.tplink_client.get_connected_devices() - - if result: - self.last_results = result - return True - - return False - - -class Tplink1DeviceScanner(DeviceScanner): - """This class queries a wireless router running TP-Link firmware.""" - - def __init__(self, config): - """Initialize the scanner.""" - host = config[CONF_HOST] - username, password = config[CONF_USERNAME], config[CONF_PASSWORD] - - self.parse_macs = re.compile( - "[0-9A-F]{2}-[0-9A-F]{2}-[0-9A-F]{2}-" - + "[0-9A-F]{2}-[0-9A-F]{2}-[0-9A-F]{2}" - ) - - self.host = host - self.username = username - self.password = password - - self.last_results = {} - self.success_init = False - try: - self.success_init = self._update_info() - except requests.exceptions.RequestException: - _LOGGER.debug("RequestException in %s", __class__.__name__) - - def scan_devices(self): - """Scan for new devices and return a list with found device IDs.""" - self._update_info() - return self.last_results - - def get_device_name(self, device): - """Get firmware doesn't save the name of the wireless device.""" - return None - - def _update_info(self): - """Ensure the information from the TP-Link router is up to date. - - Return boolean if scanning successful. - """ - _LOGGER.info("Loading wireless clients...") - - url = f"http://{self.host}/userRpm/WlanStationRpm.htm" - referer = f"http://{self.host}" - page = requests.get( - url, - auth=(self.username, self.password), - headers={REFERER: referer}, - timeout=4, - ) - - result = self.parse_macs.findall(page.text) - - if result: - self.last_results = [mac.replace("-", ":") for mac in result] - return True - - return False - - -class Tplink2DeviceScanner(Tplink1DeviceScanner): - """This class queries a router with newer version of TP-Link firmware.""" - - def scan_devices(self): - """Scan for new devices and return a list with found device IDs.""" - self._update_info() - return self.last_results.keys() - - def get_device_name(self, device): - """Get firmware doesn't save the name of the wireless device.""" - return self.last_results.get(device) - - def _update_info(self): - """Ensure the information from the TP-Link router is up to date. - - Return boolean if scanning successful. - """ - _LOGGER.info("Loading wireless clients...") - - url = f"http://{self.host}/data/map_access_wireless_client_grid.json" - referer = f"http://{self.host}" - - # Router uses Authorization cookie instead of header - # Let's create the cookie - username_password = f"{self.username}:{self.password}" - b64_encoded_username_password = base64.b64encode( - username_password.encode("ascii") - ).decode("ascii") - cookie = f"Authorization=Basic {b64_encoded_username_password}" - - response = requests.post( - url, headers={REFERER: referer, COOKIE: cookie}, timeout=4 - ) - - try: - result = response.json().get("data") - except ValueError: - _LOGGER.error( - "Router didn't respond with JSON. " "Check if credentials are correct." - ) - return False - - if result: - self.last_results = { - device["mac_addr"].replace("-", ":"): device["name"] - for device in result - } - return True - - return False - - -class Tplink3DeviceScanner(Tplink1DeviceScanner): - """This class queries the Archer C9 router with version 150811 or high.""" - - def __init__(self, config): - """Initialize the scanner.""" - self.stok = "" - self.sysauth = "" - super().__init__(config) - - def scan_devices(self): - """Scan for new devices and return a list with found device IDs.""" - self._update_info() - self._log_out() - return self.last_results.keys() - - def get_device_name(self, device): - """Get the firmware doesn't save the name of the wireless device. - - We are forced to use the MAC address as name here. - """ - return self.last_results.get(device) - - def _get_auth_tokens(self): - """Retrieve auth tokens from the router.""" - _LOGGER.info("Retrieving auth tokens...") - - url = f"http://{self.host}/cgi-bin/luci/;stok=/login?form=login" - referer = f"http://{self.host}/webpages/login.html" - - # If possible implement RSA encryption of password here. - response = requests.post( - url, - params={ - "operation": "login", - "username": self.username, - "password": self.password, - }, - headers={REFERER: referer}, - timeout=4, - ) - - try: - self.stok = response.json().get("data").get("stok") - _LOGGER.info(self.stok) - regex_result = re.search("sysauth=(.*);", response.headers["set-cookie"]) - self.sysauth = regex_result.group(1) - _LOGGER.info(self.sysauth) - return True - except (ValueError, KeyError): - _LOGGER.error("Couldn't fetch auth tokens! Response was: %s", response.text) - return False - - def _update_info(self): - """Ensure the information from the TP-Link router is up to date. - - Return boolean if scanning successful. - """ - if (self.stok == "") or (self.sysauth == ""): - self._get_auth_tokens() - - _LOGGER.info("Loading wireless clients...") - - url = ( - "http://{}/cgi-bin/luci/;stok={}/admin/wireless?" "form=statistics" - ).format(self.host, self.stok) - referer = f"http://{self.host}/webpages/index.html" - - response = requests.post( - url, - params={"operation": "load"}, - headers={REFERER: referer}, - cookies={"sysauth": self.sysauth}, - timeout=5, - ) - - try: - json_response = response.json() - - if json_response.get("success"): - result = response.json().get("data") - else: - if json_response.get("errorcode") == "timeout": - _LOGGER.info("Token timed out. Relogging on next scan") - self.stok = "" - self.sysauth = "" - return False - _LOGGER.error("An unknown error happened while fetching data") - return False - except ValueError: - _LOGGER.error( - "Router didn't respond with JSON. " "Check if credentials are correct" - ) - return False - - if result: - self.last_results = { - device["mac"].replace("-", ":"): device["mac"] for device in result - } - return True - - return False - - def _log_out(self): - _LOGGER.info("Logging out of router admin interface...") - - url = ("http://{}/cgi-bin/luci/;stok={}/admin/system?" "form=logout").format( - self.host, self.stok - ) - referer = f"http://{self.host}/webpages/index.html" - - requests.post( - url, - params={"operation": "write"}, - headers={REFERER: referer}, - cookies={"sysauth": self.sysauth}, - ) - self.stok = "" - self.sysauth = "" - - -class Tplink4DeviceScanner(Tplink1DeviceScanner): - """This class queries an Archer C7 router with TP-Link firmware 150427.""" - - def __init__(self, config): - """Initialize the scanner.""" - self.credentials = "" - self.token = "" - super().__init__(config) - - def scan_devices(self): - """Scan for new devices and return a list with found device IDs.""" - self._update_info() - return self.last_results - - def get_device_name(self, device): - """Get the name of the wireless device.""" - return None - - def _get_auth_tokens(self): - """Retrieve auth tokens from the router.""" - _LOGGER.info("Retrieving auth tokens...") - url = f"http://{self.host}/userRpm/LoginRpm.htm?Save=Save" - - # Generate md5 hash of password. The C7 appears to use the first 15 - # characters of the password only, so we truncate to remove additional - # characters from being hashed. - password = hashlib.md5(self.password.encode("utf")[:15]).hexdigest() - credentials = f"{self.username}:{password}".encode("utf") - - # Encode the credentials to be sent as a cookie. - self.credentials = base64.b64encode(credentials).decode("utf") - - # Create the authorization cookie. - cookie = f"Authorization=Basic {self.credentials}" - - response = requests.get(url, headers={COOKIE: cookie}) - - try: - result = re.search( - r"window.parent.location.href = " - r'"https?:\/\/.*\/(.*)\/userRpm\/Index.htm";', - response.text, - ) - if not result: - return False - self.token = result.group(1) - return True - except ValueError: - _LOGGER.error("Couldn't fetch auth tokens") - return False - - def _update_info(self): - """Ensure the information from the TP-Link router is up to date. - - Return boolean if scanning successful. - """ - if (self.credentials == "") or (self.token == ""): - self._get_auth_tokens() - - _LOGGER.info("Loading wireless clients...") - - mac_results = [] - - # Check both the 2.4GHz and 5GHz client list URLs - for clients_url in ("WlanStationRpm.htm", "WlanStationRpm_5g.htm"): - url = f"http://{self.host}/{self.token}/userRpm/{clients_url}" - referer = f"http://{self.host}" - cookie = f"Authorization=Basic {self.credentials}" - - page = requests.get(url, headers={COOKIE: cookie, REFERER: referer}) - mac_results.extend(self.parse_macs.findall(page.text)) - - if not mac_results: - return False - - self.last_results = [mac.replace("-", ":") for mac in mac_results] - return True - - -class Tplink5DeviceScanner(Tplink1DeviceScanner): - """This class queries a TP-Link EAP-225 AP with newer TP-Link FW.""" - - def scan_devices(self): - """Scan for new devices and return a list with found MAC IDs.""" - self._update_info() - return self.last_results.keys() - - def get_device_name(self, device): - """Get firmware doesn't save the name of the wireless device.""" - return None - - def _update_info(self): - """Ensure the information from the TP-Link AP is up to date. - - Return boolean if scanning successful. - """ - _LOGGER.info("Loading wireless clients...") - - base_url = f"http://{self.host}" - - header = { - USER_AGENT: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12;" - " rv:53.0) Gecko/20100101 Firefox/53.0", - ACCEPT: "application/json, text/javascript, */*; q=0.01", - ACCEPT_LANGUAGE: "Accept-Language: en-US,en;q=0.5", - ACCEPT_ENCODING: "gzip, deflate", - CONTENT_TYPE: "application/x-www-form-urlencoded; charset=UTF-8", - HTTP_HEADER_X_REQUESTED_WITH: "XMLHttpRequest", - REFERER: f"http://{self.host}/", - CONNECTION: KEEP_ALIVE, - PRAGMA: HTTP_HEADER_NO_CACHE, - CACHE_CONTROL: HTTP_HEADER_NO_CACHE, - } - - password_md5 = hashlib.md5(self.password.encode("utf")).hexdigest().upper() - - # Create a session to handle cookie easier - session = requests.session() - session.get(base_url, headers=header) - - login_data = {"username": self.username, "password": password_md5} - session.post(base_url, login_data, headers=header) - - # A timestamp is required to be sent as get parameter - timestamp = int(datetime.now().timestamp() * 1e3) - - client_list_url = f"{base_url}/data/monitor.client.client.json" - - get_params = {"operation": "load", "_": timestamp} - - response = session.get(client_list_url, headers=header, params=get_params) - session.close() - try: - list_of_devices = response.json() - except ValueError: - _LOGGER.error( - "AP didn't respond with JSON. " "Check if credentials are correct" - ) - return False - - if list_of_devices: - self.last_results = { - device["MAC"].replace("-", ":"): device["DeviceName"] - for device in list_of_devices["data"] - } - return True - - return False diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json index f299e02e2d3..c2a2197c844 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -4,8 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tplink", "requirements": [ - "pyHS100==0.3.5", - "tplink==0.2.1" + "pyHS100==0.3.5" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/tplink/switch.py b/homeassistant/components/tplink/switch.py index ebeac984515..791d358c509 100644 --- a/homeassistant/components/tplink/switch.py +++ b/homeassistant/components/tplink/switch.py @@ -69,11 +69,12 @@ class SmartPlugSwitch(SwitchDevice): self._mac = None self._alias = None self._model = None + self._device_id = None @property def unique_id(self): """Return a unique ID.""" - return self._mac + return self._device_id @property def name(self): @@ -120,10 +121,26 @@ class SmartPlugSwitch(SwitchDevice): if not self._sysinfo: self._sysinfo = self.smartplug.sys_info self._mac = self.smartplug.mac - self._alias = self.smartplug.alias self._model = self.smartplug.model + if self.smartplug.context is None: + 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._device_id = self.smartplug.context - self._state = self.smartplug.state == self.smartplug.SWITCH_STATE_ON + 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 if self.smartplug.has_emeter: emeter_readings = self.smartplug.get_emeter_realtime() diff --git a/homeassistant/components/tplink_lte/__init__.py b/homeassistant/components/tplink_lte/__init__.py index 215dd5c94b2..e495a14a38c 100644 --- a/homeassistant/components/tplink_lte/__init__.py +++ b/homeassistant/components/tplink_lte/__init__.py @@ -4,6 +4,7 @@ import logging import aiohttp import attr +import tp_connected import voluptuous as vol from homeassistant.const import ( @@ -106,7 +107,6 @@ async def async_setup(hass, config): async def _setup_lte(hass, lte_config, delay=0): """Set up a TP-Link LTE modem.""" - import tp_connected host = lte_config[CONF_HOST] password = lte_config[CONF_PASSWORD] @@ -145,7 +145,6 @@ async def _login(hass, modem_data, password): async def _retry_login(hass, modem_data, password): """Sleep and retry setup.""" - import tp_connected _LOGGER.warning("Could not connect to %s. Will keep trying.", modem_data.host) diff --git a/homeassistant/components/tplink_lte/notify.py b/homeassistant/components/tplink_lte/notify.py index e677b42a511..478b3e998c0 100644 --- a/homeassistant/components/tplink_lte/notify.py +++ b/homeassistant/components/tplink_lte/notify.py @@ -2,6 +2,7 @@ import logging import attr +import tp_connected from homeassistant.components.notify import ATTR_TARGET, BaseNotificationService from homeassistant.const import CONF_RECIPIENT @@ -27,7 +28,6 @@ class TplinkNotifyService(BaseNotificationService): async def async_send_message(self, message="", **kwargs): """Send a message to a user.""" - import tp_connected modem_data = self.hass.data[DATA_KEY].get_modem_data(self.config) if not modem_data: diff --git a/homeassistant/components/tradfri/.translations/ru.json b/homeassistant/components/tradfri/.translations/ru.json index 99844dc91ca..2e3dc8331be 100644 --- a/homeassistant/components/tradfri/.translations/ru.json +++ b/homeassistant/components/tradfri/.translations/ru.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\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 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u043d\u0430\u0447\u0430\u0442\u0430." }, "error": { - "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0448\u043b\u044e\u0437\u0443", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0448\u043b\u044e\u0437\u0443.", "invalid_key": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0441 \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u043c \u043a\u043b\u044e\u0447\u043e\u043c. \u0415\u0441\u043b\u0438 \u044d\u0442\u043e \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u043f\u0435\u0440\u0435\u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0448\u043b\u044e\u0437.", "timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043a\u043e\u0434\u0430." }, diff --git a/homeassistant/components/tradfri/__init__.py b/homeassistant/components/tradfri/__init__.py index bca91134bed..bdfabb4b00a 100644 --- a/homeassistant/components/tradfri/__init__.py +++ b/homeassistant/components/tradfri/__init__.py @@ -2,13 +2,25 @@ import logging import voluptuous as vol +from pytradfri import Gateway, RequestError +from pytradfri.api.aiocoap_api import APIFactory +import homeassistant.helpers.config_validation as cv from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_STOP -import homeassistant.helpers.config_validation as cv from homeassistant.util.json import load_json - +from . import config_flow # noqa pylint_disable=unused-import 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, @@ -16,18 +28,8 @@ from .const import ( CONF_GATEWAY_ID, ) -from . import config_flow # noqa pylint_disable=unused-import - _LOGGER = logging.getLogger(__name__) - -DOMAIN = "tradfri" -CONFIG_FILE = ".tradfri_psk.conf" -KEY_GATEWAY = "tradfri_gateway" -KEY_API = "tradfri_api" -CONF_ALLOW_TRADFRI_GROUPS = "allow_tradfri_groups" -DEFAULT_ALLOW_TRADFRI_GROUPS = False - CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( @@ -91,8 +93,6 @@ async def async_setup(hass, config): async def async_setup_entry(hass, entry): """Create a gateway.""" # host, identity, key, allow_tradfri_groups - from pytradfri import Gateway, RequestError # pylint: disable=import-error - from pytradfri.api.aiocoap_api import APIFactory factory = APIFactory( entry.data[CONF_HOST], @@ -124,24 +124,16 @@ async def async_setup_entry(hass, entry): config_entry_id=entry.entry_id, connections=set(), identifiers={(DOMAIN, entry.data[CONF_GATEWAY_ID])}, - manufacturer="IKEA", - name="Gateway", + manufacturer=ATTR_TRADFRI_MANUFACTURER, + name=ATTR_TRADFRI_GATEWAY, # They just have 1 gateway model. Type is not exposed yet. - model="E1526", + model=ATTR_TRADFRI_GATEWAY_MODEL, sw_version=gateway_info.firmware_version, ) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "cover") - ) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "light") - ) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "sensor") - ) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "switch") - ) + for device in TRADFRI_DEVICE_TYPES: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, device) + ) return True diff --git a/homeassistant/components/tradfri/base_class.py b/homeassistant/components/tradfri/base_class.py new file mode 100644 index 00000000000..ba90fe05d1e --- /dev/null +++ b/homeassistant/components/tradfri/base_class.py @@ -0,0 +1,113 @@ +"""Base class for IKEA TRADFRI.""" +import logging + +from pytradfri.error import PytradfriError + +from homeassistant.core import callback +from homeassistant.helpers.entity import Entity +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class TradfriBaseClass(Entity): + """Base class for IKEA TRADFRI. + + All devices and groups should ultimately inherit from this class. + """ + + def __init__(self, device, api, gateway_id): + """Initialize a device.""" + self._api = api + self._device = None + self._device_control = None + self._device_data = None + self._gateway_id = gateway_id + self._name = None + self._unique_id = None + + self._refresh(device) + + @callback + def _async_start_observe(self, exc=None): + """Start observation of device.""" + if exc: + self.async_schedule_update_ha_state() + _LOGGER.warning("Observation failed for %s", self._name, exc_info=exc) + + try: + cmd = self._device.observe( + callback=self._observe_update, + err_callback=self._async_start_observe, + duration=0, + ) + self.hass.async_create_task(self._api(cmd)) + except PytradfriError as err: + _LOGGER.warning("Observation failed, trying again", exc_info=err) + self._async_start_observe() + + async def async_added_to_hass(self): + """Start thread when added to hass.""" + self._async_start_observe() + + @property + def name(self): + """Return the display name of this device.""" + return self._name + + @property + def should_poll(self): + """No polling needed for tradfri device.""" + return False + + @property + def unique_id(self): + """Return unique ID for device.""" + return self._unique_id + + @callback + def _observe_update(self, device): + """Receive new state data for this device.""" + self._refresh(device) + self.async_schedule_update_ha_state() + + def _refresh(self, device): + """Refresh the device data.""" + self._device = device + self._name = device.name + + +class TradfriBaseDevice(TradfriBaseClass): + """Base class for a TRADFRI device. + + All devices should inherit from this class. + """ + + def __init__(self, device, api, gateway_id): + """Initialize a device.""" + super().__init__(device, api, gateway_id) + self._available = True + + @property + def available(self): + """Return True if entity is available.""" + return self._available + + @property + def device_info(self): + """Return the device info.""" + info = self._device.device_info + + return { + "identifiers": {(DOMAIN, self._device.id)}, + "manufacturer": info.manufacturer, + "model": info.model_number, + "name": self._name, + "sw_version": info.firmware_version, + "via_device": (DOMAIN, self._gateway_id), + } + + def _refresh(self, device): + """Refresh the device data.""" + super()._refresh(device) + self._available = device.reachable diff --git a/homeassistant/components/tradfri/config_flow.py b/homeassistant/components/tradfri/config_flow.py index 6266766f394..bdb195cf53f 100644 --- a/homeassistant/components/tradfri/config_flow.py +++ b/homeassistant/components/tradfri/config_flow.py @@ -7,18 +7,15 @@ import async_timeout import voluptuous as vol from homeassistant import config_entries - from .const import ( CONF_IMPORT_GROUPS, CONF_IDENTITY, CONF_HOST, CONF_KEY, CONF_GATEWAY_ID, + KEY_SECURITY_CODE, ) -KEY_SECURITY_CODE = "security_code" -KEY_IMPORT_GROUPS = "import_groups" - class AuthError(Exception): """Exception if authentication occurs.""" @@ -83,7 +80,7 @@ class FlowHandler(config_entries.ConfigFlow): """Handle zeroconf discovery.""" host = user_input["host"] - # pylint: disable=unsupported-assignment-operation + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context["host"] = host if any(host == flow["context"]["host"] for flow in self._async_in_progress()): diff --git a/homeassistant/components/tradfri/const.py b/homeassistant/components/tradfri/const.py index d37b5d99f9f..038f0e91c76 100644 --- a/homeassistant/components/tradfri/const.py +++ b/homeassistant/components/tradfri/const.py @@ -1,7 +1,25 @@ """Consts used by Tradfri.""" +from homeassistant.components.light import SUPPORT_TRANSITION, SUPPORT_BRIGHTNESS from homeassistant.const import CONF_HOST # noqa pylint: disable=unused-import -CONF_IMPORT_GROUPS = "import_groups" +ATTR_DIMMER = "dimmer" +ATTR_HUE = "hue" +ATTR_SAT = "saturation" +ATTR_TRADFRI_GATEWAY = "Gateway" +ATTR_TRADFRI_GATEWAY_MODEL = "E1526" +ATTR_TRADFRI_MANUFACTURER = "IKEA of Sweden" +ATTR_TRANSITION_TIME = "transition_time" +CONF_ALLOW_TRADFRI_GROUPS = "allow_tradfri_groups" CONF_IDENTITY = "identity" -CONF_KEY = "key" +CONF_IMPORT_GROUPS = "import_groups" CONF_GATEWAY_ID = "gateway_id" +CONF_KEY = "key" +CONFIG_FILE = ".tradfri_psk.conf" +DEFAULT_ALLOW_TRADFRI_GROUPS = False +DOMAIN = "tradfri" +KEY_API = "tradfri_api" +KEY_GATEWAY = "tradfri_gateway" +KEY_SECURITY_CODE = "security_code" +SUPPORTED_GROUP_FEATURES = SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION +SUPPORTED_LIGHT_FEATURES = SUPPORT_TRANSITION +TRADFRI_DEVICE_TYPES = ["cover", "light", "sensor", "switch"] diff --git a/homeassistant/components/tradfri/cover.py b/homeassistant/components/tradfri/cover.py index 3dea978044f..9b831dce0ec 100644 --- a/homeassistant/components/tradfri/cover.py +++ b/homeassistant/components/tradfri/cover.py @@ -1,7 +1,4 @@ """Support for IKEA Tradfri covers.""" -import logging - -from pytradfri.error import PytradfriError from homeassistant.components.cover import ( CoverDevice, @@ -10,11 +7,8 @@ from homeassistant.components.cover import ( SUPPORT_CLOSE, SUPPORT_SET_POSITION, ) -from homeassistant.core import callback -from . import DOMAIN as TRADFRI_DOMAIN, KEY_API, KEY_GATEWAY -from .const import CONF_GATEWAY_ID - -_LOGGER = logging.getLogger(__name__) +from .base_class import TradfriBaseDevice +from .const import KEY_GATEWAY, KEY_API, CONF_GATEWAY_ID async def async_setup_entry(hass, config_entry, async_add_entities): @@ -30,120 +24,51 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(TradfriCover(cover, api, gateway_id) for cover in covers) -class TradfriCover(CoverDevice): +class TradfriCover(TradfriBaseDevice, CoverDevice): """The platform class required by Home Assistant.""" - def __init__(self, cover, api, gateway_id): + def __init__(self, device, api, gateway_id): """Initialize a cover.""" - self._api = api - self._unique_id = f"{gateway_id}-{cover.id}" - self._cover = None - self._cover_control = None - self._cover_data = None - self._name = None - self._available = True - self._gateway_id = gateway_id + super().__init__(device, api, gateway_id) + self._unique_id = f"{gateway_id}-{device.id}" - self._refresh(cover) + self._refresh(device) @property def supported_features(self): """Flag supported features.""" return SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION - @property - def unique_id(self): - """Return unique ID for cover.""" - return self._unique_id - - @property - def device_info(self): - """Return the device info.""" - info = self._cover.device_info - - return { - "identifiers": {(TRADFRI_DOMAIN, self._cover.id)}, - "name": self._name, - "manufacturer": info.manufacturer, - "model": info.model_number, - "sw_version": info.firmware_version, - "via_device": (TRADFRI_DOMAIN, self._gateway_id), - } - - async def async_added_to_hass(self): - """Start thread when added to hass.""" - self._async_start_observe() - - @property - def available(self): - """Return True if entity is available.""" - return self._available - - @property - def should_poll(self): - """No polling needed for tradfri cover.""" - return False - - @property - def name(self): - """Return the display name of this cover.""" - return self._name - @property def current_cover_position(self): """Return current position of cover. None is unknown, 0 is closed, 100 is fully open. """ - return 100 - self._cover_data.current_cover_position + return 100 - self._device_data.current_cover_position async def async_set_cover_position(self, **kwargs): """Move the cover to a specific position.""" - await self._api(self._cover_control.set_state(100 - kwargs[ATTR_POSITION])) + await self._api(self._device_control.set_state(100 - kwargs[ATTR_POSITION])) async def async_open_cover(self, **kwargs): """Open the cover.""" - await self._api(self._cover_control.set_state(0)) + await self._api(self._device_control.set_state(0)) async def async_close_cover(self, **kwargs): """Close cover.""" - await self._api(self._cover_control.set_state(100)) + await self._api(self._device_control.set_state(100)) @property def is_closed(self): """Return if the cover is closed or not.""" return self.current_cover_position == 0 - @callback - def _async_start_observe(self, exc=None): - """Start observation of cover.""" - if exc: - self._available = False - self.async_schedule_update_ha_state() - _LOGGER.warning("Observation failed for %s", self._name, exc_info=exc) - try: - cmd = self._cover.observe( - callback=self._observe_update, - err_callback=self._async_start_observe, - duration=0, - ) - self.hass.async_create_task(self._api(cmd)) - except PytradfriError as err: - _LOGGER.warning("Observation failed, trying again", exc_info=err) - self._async_start_observe() - - def _refresh(self, cover): + def _refresh(self, device): """Refresh the cover data.""" - self._cover = cover + super()._refresh(device) + self._device = device # Caching of BlindControl and cover object - self._available = cover.reachable - self._cover_control = cover.blind_control - self._cover_data = cover.blind_control.blinds[0] - self._name = cover.name - - @callback - def _observe_update(self, tradfri_device): - """Receive new state data for this cover.""" - self._refresh(tradfri_device) - self.async_schedule_update_ha_state() + self._device_control = device.blind_control + self._device_data = device.blind_control.blinds[0] diff --git a/homeassistant/components/tradfri/light.py b/homeassistant/components/tradfri/light.py index 615899a98c8..9ee3c5d6a8c 100644 --- a/homeassistant/components/tradfri/light.py +++ b/homeassistant/components/tradfri/light.py @@ -1,36 +1,33 @@ """Support for IKEA Tradfri lights.""" import logging -from pytradfri.error import PytradfriError - import homeassistant.util.color as color_util from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION, - PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA, + Light, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, - SUPPORT_TRANSITION, - Light, ) -from homeassistant.core import callback -from . import DOMAIN as TRADFRI_DOMAIN, KEY_API, KEY_GATEWAY -from .const import CONF_GATEWAY_ID, CONF_IMPORT_GROUPS +from .base_class import TradfriBaseDevice, TradfriBaseClass +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, +) _LOGGER = logging.getLogger(__name__) -ATTR_DIMMER = "dimmer" -ATTR_HUE = "hue" -ATTR_SAT = "saturation" -ATTR_TRANSITION_TIME = "transition_time" -PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA -TRADFRI_LIGHT_MANAGER = "Tradfri Light Manager" -SUPPORTED_FEATURES = SUPPORT_TRANSITION -SUPPORTED_GROUP_FEATURES = SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION - async def async_setup_entry(hass, config_entry, async_add_entities): """Load Tradfri lights based on a config entry.""" @@ -51,50 +48,47 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(TradfriGroup(group, api, gateway_id) for group in groups) -class TradfriGroup(Light): - """The platform class required by hass.""" +class TradfriGroup(TradfriBaseClass, Light): + """The platform class for light groups required by hass.""" - def __init__(self, group, api, gateway_id): + def __init__(self, device, api, gateway_id): """Initialize a Group.""" - self._api = api - self._unique_id = f"group-{gateway_id}-{group.id}" - self._group = group - self._name = group.name + super().__init__(device, api, gateway_id) - self._refresh(group) + self._unique_id = f"group-{gateway_id}-{device.id}" - async def async_added_to_hass(self): - """Start thread when added to hass.""" - self._async_start_observe() + self._refresh(device) @property - def unique_id(self): - """Return unique ID for this group.""" - return self._unique_id + def should_poll(self): + """Poll needed for tradfri groups.""" + return True + + async def async_update(self): + """Fetch new state data for the group. + + This method is required for groups to update properly. + """ + await self._api(self._device.update()) @property def supported_features(self): """Flag supported features.""" return SUPPORTED_GROUP_FEATURES - @property - def name(self): - """Return the display name of this group.""" - return self._name - @property def is_on(self): """Return true if group lights are on.""" - return self._group.state + return self._device.state @property def brightness(self): """Return the brightness of the group lights.""" - return self._group.dimmer + return self._device.dimmer async def async_turn_off(self, **kwargs): """Instruct the group lights to turn off.""" - await self._api(self._group.set_state(0)) + await self._api(self._device.set_state(0)) async def async_turn_on(self, **kwargs): """Instruct the group lights to turn on, or dim.""" @@ -106,136 +100,69 @@ class TradfriGroup(Light): if kwargs[ATTR_BRIGHTNESS] == 255: kwargs[ATTR_BRIGHTNESS] = 254 - await self._api(self._group.set_dimmer(kwargs[ATTR_BRIGHTNESS], **keys)) + await self._api(self._device.set_dimmer(kwargs[ATTR_BRIGHTNESS], **keys)) else: - await self._api(self._group.set_state(1)) - - @callback - def _async_start_observe(self, exc=None): - """Start observation of light.""" - if exc: - _LOGGER.warning("Observation failed for %s", self._name, exc_info=exc) - - try: - cmd = self._group.observe( - callback=self._observe_update, - err_callback=self._async_start_observe, - duration=0, - ) - self.hass.async_create_task(self._api(cmd)) - except PytradfriError as err: - _LOGGER.warning("Observation failed, trying again", exc_info=err) - self._async_start_observe() - - def _refresh(self, group): - """Refresh the light data.""" - self._group = group - self._name = group.name - - @callback - def _observe_update(self, tradfri_device): - """Receive new state data for this light.""" - self._refresh(tradfri_device) - self.async_schedule_update_ha_state() - - async def async_update(self): - """Fetch new state data for the group.""" - await self._api(self._group.update()) + await self._api(self._device.set_state(1)) -class TradfriLight(Light): +class TradfriLight(TradfriBaseDevice, Light): """The platform class required by Home Assistant.""" - def __init__(self, light, api, gateway_id): + def __init__(self, device, api, gateway_id): """Initialize a Light.""" - self._api = api - self._unique_id = f"light-{gateway_id}-{light.id}" - self._light = None - self._light_control = None - self._light_data = None - self._name = None + super().__init__(device, api, gateway_id) + self._unique_id = f"light-{gateway_id}-{device.id}" self._hs_color = None - self._features = SUPPORTED_FEATURES - self._available = True - self._gateway_id = gateway_id - self._refresh(light) + # Calculate supported features + _features = SUPPORTED_LIGHT_FEATURES + if device.light_control.can_set_dimmer: + _features |= SUPPORT_BRIGHTNESS + if device.light_control.can_set_color: + _features |= SUPPORT_COLOR + if device.light_control.can_set_temp: + _features |= SUPPORT_COLOR_TEMP + self._features = _features - @property - def unique_id(self): - """Return unique ID for light.""" - return self._unique_id - - @property - def device_info(self): - """Return the device info.""" - info = self._light.device_info - - return { - "identifiers": {(TRADFRI_DOMAIN, self._light.id)}, - "name": self._name, - "manufacturer": info.manufacturer, - "model": info.model_number, - "sw_version": info.firmware_version, - "via_device": (TRADFRI_DOMAIN, self._gateway_id), - } + self._refresh(device) @property def min_mireds(self): """Return the coldest color_temp that this light supports.""" - return self._light_control.min_mireds + return self._device_control.min_mireds @property def max_mireds(self): """Return the warmest color_temp that this light supports.""" - return self._light_control.max_mireds - - async def async_added_to_hass(self): - """Start thread when added to hass.""" - self._async_start_observe() - - @property - def available(self): - """Return True if entity is available.""" - return self._available - - @property - def should_poll(self): - """No polling needed for tradfri light.""" - return False + return self._device_control.max_mireds @property def supported_features(self): """Flag supported features.""" return self._features - @property - def name(self): - """Return the display name of this light.""" - return self._name - @property def is_on(self): """Return true if light is on.""" - return self._light_data.state + return self._device_data.state @property def brightness(self): """Return the brightness of the light.""" - return self._light_data.dimmer + return self._device_data.dimmer @property def color_temp(self): """Return the color temp value in mireds.""" - return self._light_data.color_temp + return self._device_data.color_temp @property def hs_color(self): """HS color of the light.""" - if self._light_control.can_set_color: - hsbxy = self._light_data.hsb_xy_color - hue = hsbxy[0] / (self._light_control.max_hue / 360) - sat = hsbxy[1] / (self._light_control.max_saturation / 100) + if self._device_control.can_set_color: + hsbxy = self._device_data.hsb_xy_color + hue = hsbxy[0] / (self._device_control.max_hue / 360) + sat = hsbxy[1] / (self._device_control.max_saturation / 100) if hue is not None and sat is not None: return hue, sat @@ -248,9 +175,9 @@ class TradfriLight(Light): transition_time = int(kwargs[ATTR_TRANSITION]) * 10 dimmer_data = {ATTR_DIMMER: 0, ATTR_TRANSITION_TIME: transition_time} - await self._api(self._light_control.set_dimmer(**dimmer_data)) + await self._api(self._device_control.set_dimmer(**dimmer_data)) else: - await self._api(self._light_control.set_state(False)) + await self._api(self._device_control.set_state(False)) async def async_turn_on(self, **kwargs): """Instruct the light to turn on.""" @@ -267,32 +194,32 @@ class TradfriLight(Light): ATTR_DIMMER: brightness, ATTR_TRANSITION_TIME: transition_time, } - dimmer_command = self._light_control.set_dimmer(**dimmer_data) + dimmer_command = self._device_control.set_dimmer(**dimmer_data) transition_time = None else: - dimmer_command = self._light_control.set_state(True) + dimmer_command = self._device_control.set_state(True) color_command = None - if ATTR_HS_COLOR in kwargs and self._light_control.can_set_color: - hue = int(kwargs[ATTR_HS_COLOR][0] * (self._light_control.max_hue / 360)) + if ATTR_HS_COLOR in kwargs and self._device_control.can_set_color: + hue = int(kwargs[ATTR_HS_COLOR][0] * (self._device_control.max_hue / 360)) sat = int( - kwargs[ATTR_HS_COLOR][1] * (self._light_control.max_saturation / 100) + kwargs[ATTR_HS_COLOR][1] * (self._device_control.max_saturation / 100) ) color_data = { ATTR_HUE: hue, ATTR_SAT: sat, ATTR_TRANSITION_TIME: transition_time, } - color_command = self._light_control.set_hsb(**color_data) + color_command = self._device_control.set_hsb(**color_data) transition_time = None temp_command = None if ATTR_COLOR_TEMP in kwargs and ( - self._light_control.can_set_temp or self._light_control.can_set_color + self._device_control.can_set_temp or self._device_control.can_set_color ): temp = kwargs[ATTR_COLOR_TEMP] # White Spectrum bulb - if self._light_control.can_set_temp: + if self._device_control.can_set_temp: if temp > self.max_mireds: temp = self.max_mireds elif temp < self.min_mireds: @@ -301,21 +228,21 @@ class TradfriLight(Light): ATTR_COLOR_TEMP: temp, ATTR_TRANSITION_TIME: transition_time, } - temp_command = self._light_control.set_color_temp(**temp_data) + temp_command = self._device_control.set_color_temp(**temp_data) transition_time = None # Color bulb (CWS) # color_temp needs to be set with hue/saturation - elif self._light_control.can_set_color: + elif self._device_control.can_set_color: temp_k = color_util.color_temperature_mired_to_kelvin(temp) hs_color = color_util.color_temperature_to_hs(temp_k) - hue = int(hs_color[0] * (self._light_control.max_hue / 360)) - sat = int(hs_color[1] * (self._light_control.max_saturation / 100)) + hue = int(hs_color[0] * (self._device_control.max_hue / 360)) + sat = int(hs_color[1] * (self._device_control.max_saturation / 100)) color_data = { ATTR_HUE: hue, ATTR_SAT: sat, ATTR_TRANSITION_TIME: transition_time, } - color_command = self._light_control.set_hsb(**color_data) + color_command = self._device_control.set_hsb(**color_data) transition_time = None # HSB can always be set, but color temp + brightness is bulb dependant @@ -325,7 +252,7 @@ class TradfriLight(Light): else: command = color_command - if self._light_control.can_combine_commands: + if self._device_control.can_combine_commands: await self._api(command + temp_command) else: if temp_command is not None: @@ -333,46 +260,10 @@ class TradfriLight(Light): if command is not None: await self._api(command) - @callback - def _async_start_observe(self, exc=None): - """Start observation of light.""" - - if exc: - self._available = False - self.async_schedule_update_ha_state() - _LOGGER.warning("Observation failed for %s", self._name, exc_info=exc) - - try: - cmd = self._light.observe( - callback=self._observe_update, - err_callback=self._async_start_observe, - duration=0, - ) - self.hass.async_create_task(self._api(cmd)) - except PytradfriError as err: - _LOGGER.warning("Observation failed, trying again", exc_info=err) - self._async_start_observe() - - def _refresh(self, light): + def _refresh(self, device): """Refresh the light data.""" - self._light = light + super()._refresh(device) # Caching of LightControl and light object - self._available = light.reachable - self._light_control = light.light_control - self._light_data = light.light_control.lights[0] - self._name = light.name - self._features = SUPPORTED_FEATURES - - if light.light_control.can_set_dimmer: - self._features |= SUPPORT_BRIGHTNESS - if light.light_control.can_set_color: - self._features |= SUPPORT_COLOR - if light.light_control.can_set_temp: - self._features |= SUPPORT_COLOR_TEMP - - @callback - def _observe_update(self, tradfri_device): - """Receive new state data for this light.""" - self._refresh(tradfri_device) - self.async_schedule_update_ha_state() + self._device_control = device.light_control + self._device_data = device.light_control.lights[0] diff --git a/homeassistant/components/tradfri/sensor.py b/homeassistant/components/tradfri/sensor.py index 4877dbbb541..68a2c10291b 100644 --- a/homeassistant/components/tradfri/sensor.py +++ b/homeassistant/components/tradfri/sensor.py @@ -1,20 +1,13 @@ """Support for IKEA Tradfri sensors.""" -import logging -from datetime import timedelta -from pytradfri.error import PytradfriError - -from homeassistant.core import callback -from homeassistant.helpers.entity import Entity -from . import KEY_API, KEY_GATEWAY - -_LOGGER = logging.getLogger(__name__) - -SCAN_INTERVAL = timedelta(minutes=5) +from homeassistant.const import DEVICE_CLASS_BATTERY +from .base_class import TradfriBaseDevice +from .const import KEY_GATEWAY, KEY_API, CONF_GATEWAY_ID async def async_setup_entry(hass, config_entry, async_add_entities): """Set up a Tradfri config entry.""" + gateway_id = config_entry.data[CONF_GATEWAY_ID] api = hass.data[KEY_API][config_entry.entry_id] gateway = hass.data[KEY_GATEWAY][config_entry.entry_id] @@ -23,84 +16,33 @@ async def async_setup_entry(hass, config_entry, async_add_entities): devices = ( dev for dev in all_devices - if not dev.has_light_control and not dev.has_socket_control + if not dev.has_light_control + and not dev.has_socket_control + and not dev.has_blind_control ) - async_add_entities(TradfriDevice(device, api) for device in devices) + if devices: + async_add_entities(TradfriSensor(device, api, gateway_id) for device in devices) -class TradfriDevice(Entity): +class TradfriSensor(TradfriBaseDevice): """The platform class required by Home Assistant.""" - def __init__(self, device, api): + def __init__(self, device, api, gateway_id): """Initialize the device.""" - self._api = api - self._device = None - self._name = None - - self._refresh(device) - - async def async_added_to_hass(self): - """Start thread when added to hass.""" - self._async_start_observe() + super().__init__(device, api, gateway_id) + self._unique_id = f"{gateway_id}-{device.id}" @property - def should_poll(self): - """No polling needed for tradfri.""" - return False - - @property - def name(self): - """Return the display name of this device.""" - return self._name - - @property - def unit_of_measurement(self): - """Return the unit_of_measurement of the device.""" - return "%" - - @property - def device_state_attributes(self): + def device_class(self): """Return the devices' state attributes.""" - info = self._device.device_info - attrs = { - "manufacturer": info.manufacturer, - "model_number": info.model_number, - "serial": info.serial, - "firmware_version": info.firmware_version, - "power_source": info.power_source_str, - "battery_level": info.battery_level, - } - return attrs + return DEVICE_CLASS_BATTERY @property def state(self): """Return the current state of the device.""" return self._device.device_info.battery_level - @callback - def _async_start_observe(self, exc=None): - """Start observation of light.""" - if exc: - _LOGGER.warning("Observation failed for %s", self._name, exc_info=exc) - - try: - cmd = self._device.observe( - callback=self._observe_update, - err_callback=self._async_start_observe, - duration=0, - ) - self.hass.async_create_task(self._api(cmd)) - except PytradfriError as err: - _LOGGER.warning("Observation failed, trying again", exc_info=err) - self._async_start_observe() - - def _refresh(self, device): - """Refresh the device data.""" - self._device = device - self._name = device.name - - def _observe_update(self, tradfri_device): - """Receive new state data for this device.""" - self._refresh(tradfri_device) - - self.hass.async_create_task(self.async_update_ha_state()) + @property + def unit_of_measurement(self): + """Return the unit_of_measurement of the device.""" + return "%" diff --git a/homeassistant/components/tradfri/switch.py b/homeassistant/components/tradfri/switch.py index 545c1ad93ce..e1c549a1805 100644 --- a/homeassistant/components/tradfri/switch.py +++ b/homeassistant/components/tradfri/switch.py @@ -1,16 +1,7 @@ """Support for IKEA Tradfri switches.""" -import logging - -from pytradfri.error import PytradfriError - from homeassistant.components.switch import SwitchDevice -from homeassistant.core import callback -from . import DOMAIN as TRADFRI_DOMAIN, KEY_API, KEY_GATEWAY -from .const import CONF_GATEWAY_ID - -_LOGGER = logging.getLogger(__name__) - -TRADFRI_SWITCH_MANAGER = "Tradfri Switch Manager" +from .base_class import TradfriBaseDevice +from .const import KEY_GATEWAY, KEY_API, CONF_GATEWAY_ID async def async_setup_entry(hass, config_entry, async_add_entities): @@ -28,104 +19,31 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -class TradfriSwitch(SwitchDevice): +class TradfriSwitch(TradfriBaseDevice, SwitchDevice): """The platform class required by Home Assistant.""" - def __init__(self, switch, api, gateway_id): + def __init__(self, device, api, gateway_id): """Initialize a switch.""" - self._api = api - self._unique_id = f"{gateway_id}-{switch.id}" - self._switch = None - self._socket_control = None - self._switch_data = None - self._name = None - self._available = True - self._gateway_id = gateway_id + super().__init__(device, api, gateway_id) + self._unique_id = f"{gateway_id}-{device.id}" - self._refresh(switch) + def _refresh(self, device): + """Refresh the switch data.""" + super()._refresh(device) - @property - def unique_id(self): - """Return unique ID for switch.""" - return self._unique_id - - @property - def device_info(self): - """Return the device info.""" - info = self._switch.device_info - - return { - "identifiers": {(TRADFRI_DOMAIN, self._switch.id)}, - "name": self._name, - "manufacturer": info.manufacturer, - "model": info.model_number, - "sw_version": info.firmware_version, - "via_device": (TRADFRI_DOMAIN, self._gateway_id), - } - - async def async_added_to_hass(self): - """Start thread when added to hass.""" - self._async_start_observe() - - @property - def available(self): - """Return True if entity is available.""" - return self._available - - @property - def should_poll(self): - """No polling needed for tradfri switch.""" - return False - - @property - def name(self): - """Return the display name of this switch.""" - return self._name + # Caching of switch control and switch object + self._device_control = device.socket_control + self._device_data = device.socket_control.sockets[0] @property def is_on(self): """Return true if switch is on.""" - return self._switch_data.state + return self._device_data.state async def async_turn_off(self, **kwargs): """Instruct the switch to turn off.""" - await self._api(self._socket_control.set_state(False)) + await self._api(self._device_control.set_state(False)) async def async_turn_on(self, **kwargs): """Instruct the switch to turn on.""" - await self._api(self._socket_control.set_state(True)) - - @callback - def _async_start_observe(self, exc=None): - """Start observation of switch.""" - if exc: - self._available = False - self.async_schedule_update_ha_state() - _LOGGER.warning("Observation failed for %s", self._name, exc_info=exc) - - try: - cmd = self._switch.observe( - callback=self._observe_update, - err_callback=self._async_start_observe, - duration=0, - ) - self.hass.async_create_task(self._api(cmd)) - except PytradfriError as err: - _LOGGER.warning("Observation failed, trying again", exc_info=err) - self._async_start_observe() - - def _refresh(self, switch): - """Refresh the switch data.""" - self._switch = switch - - # Caching of switchControl and switch object - self._available = switch.reachable - self._socket_control = switch.socket_control - self._switch_data = switch.socket_control.sockets[0] - self._name = switch.name - - @callback - def _observe_update(self, tradfri_device): - """Receive new state data for this switch.""" - self._refresh(tradfri_device) - self.async_schedule_update_ha_state() + await self._api(self._device_control.set_state(True)) diff --git a/homeassistant/components/transmission/.translations/de.json b/homeassistant/components/transmission/.translations/de.json index ed0342b9430..1a2fa4a48c0 100644 --- a/homeassistant/components/transmission/.translations/de.json +++ b/homeassistant/components/transmission/.translations/de.json @@ -21,16 +21,19 @@ "password": "Passwort", "port": "Port", "username": "Benutzername" - } + }, + "title": "Transmission-Client einrichten" } - } + }, + "title": "Transmission" }, "options": { "step": { "init": { "data": { "scan_interval": "Aktualisierungsfrequenz" - } + }, + "description": "Konfigurieren von Optionen f\u00fcr Transmission" } } } diff --git a/homeassistant/components/transmission/.translations/en.json b/homeassistant/components/transmission/.translations/en.json index 67461d1a3e8..aa8b99a4914 100644 --- a/homeassistant/components/transmission/.translations/en.json +++ b/homeassistant/components/transmission/.translations/en.json @@ -1,10 +1,12 @@ { "config": { "abort": { + "already_configured": "Host is already configured.", "one_instance_allowed": "Only a single instance is necessary." }, "error": { "cannot_connect": "Unable to Connect to host", + "name_exists": "Name already exists", "wrong_credentials": "Wrong username or password" }, "step": { @@ -33,7 +35,8 @@ "data": { "scan_interval": "Update frequency" }, - "description": "Configure options for Transmission" + "description": "Configure options for Transmission", + "title": "Configure options for Transmission" } } } diff --git a/homeassistant/components/transmission/.translations/ko.json b/homeassistant/components/transmission/.translations/ko.json new file mode 100644 index 00000000000..a9b1b369f90 --- /dev/null +++ b/homeassistant/components/transmission/.translations/ko.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "\ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \ud544\uc694\ud569\ub2c8\ub2e4." + }, + "error": { + "cannot_connect": "\ud638\uc2a4\ud2b8\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", + "wrong_credentials": "\uc0ac\uc6a9\uc790 \uc774\ub984 \ub610\ub294 \ube44\ubc00\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "step": { + "options": { + "data": { + "scan_interval": "\uc5c5\ub370\uc774\ud2b8 \ube48\ub3c4" + }, + "title": "\uc635\uc158 \uc124\uc815" + }, + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "name": "\uc774\ub984", + "password": "\ube44\ubc00\ubc88\ud638", + "port": "\ud3ec\ud2b8", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + }, + "title": "Transmission \ud074\ub77c\uc774\uc5b8\ud2b8 \uc124\uc815" + } + }, + "title": "Transmission" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\uc5c5\ub370\uc774\ud2b8 \ube48\ub3c4" + }, + "description": "Transmission \uc635\uc158 \uc124\uc815" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/nl.json b/homeassistant/components/transmission/.translations/nl.json new file mode 100644 index 00000000000..fdf3db99ed0 --- /dev/null +++ b/homeassistant/components/transmission/.translations/nl.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "Slechts \u00e9\u00e9n instantie is nodig." + }, + "error": { + "cannot_connect": "Kan geen verbinding maken met host", + "wrong_credentials": "verkeerde gebruikersnaam of wachtwoord" + }, + "step": { + "options": { + "data": { + "scan_interval": "Update frequentie" + }, + "title": "Configureer opties" + }, + "user": { + "data": { + "host": "Host", + "name": "Naam", + "password": "Wachtwoord", + "port": "Poort", + "username": "Gebruikersnaam" + }, + "title": "Verzendclient instellen" + } + }, + "title": "Transmission" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Update frequentie" + }, + "description": "Configureer opties voor Transmission" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/no.json b/homeassistant/components/transmission/.translations/no.json index f6ddce2a4a7..94044e692d9 100644 --- a/homeassistant/components/transmission/.translations/no.json +++ b/homeassistant/components/transmission/.translations/no.json @@ -22,7 +22,7 @@ "port": "Port", "username": "Brukernavn" }, - "title": "Oppsett av klient for Transmission" + "title": "Oppsett av Transmission-klient" } }, "title": "Transmission" diff --git a/homeassistant/components/transmission/.translations/pt.json b/homeassistant/components/transmission/.translations/pt.json new file mode 100644 index 00000000000..f681da4210f --- /dev/null +++ b/homeassistant/components/transmission/.translations/pt.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Servidor", + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/ru.json b/homeassistant/components/transmission/.translations/ru.json index e7a438cae11..23f1ceaaa94 100644 --- a/homeassistant/components/transmission/.translations/ru.json +++ b/homeassistant/components/transmission/.translations/ru.json @@ -4,8 +4,8 @@ "one_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": { - "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0445\u043e\u0441\u0442\u0443", - "wrong_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c" + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0445\u043e\u0441\u0442\u0443.", + "wrong_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c." }, "step": { "options": { diff --git a/homeassistant/components/transmission/__init__.py b/homeassistant/components/transmission/__init__.py index e6ddd87bdf5..6cfd6bf640a 100644 --- a/homeassistant/components/transmission/__init__.py +++ b/homeassistant/components/transmission/__init__.py @@ -19,11 +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_TRANSMISSION, - DATA_UPDATED, DEFAULT_NAME, DEFAULT_PORT, DEFAULT_SCAN_INTERVAL, @@ -37,74 +36,77 @@ _LOGGER = logging.getLogger(__name__) SERVICE_ADD_TORRENT_SCHEMA = vol.Schema({vol.Required(ATTR_TORRENT): cv.string}) +TRANS_SCHEMA = vol.All( + vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PASSWORD): cv.string, + vol.Optional(CONF_USERNAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional( + CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL + ): cv.time_period, + } + ) +) + CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_PASSWORD): cv.string, - vol.Optional(CONF_USERNAME): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional( - CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL - ): cv.time_period, - } - ) - }, - extra=vol.ALLOW_EXTRA, + {DOMAIN: vol.All(cv.ensure_list, [TRANS_SCHEMA])}, extra=vol.ALLOW_EXTRA ) async def async_setup(hass, config): """Import the Transmission Component from config.""" - if not hass.config_entries.async_entries(DOMAIN) and DOMAIN in config: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=config[DOMAIN] + if DOMAIN in config: + for entry in config[DOMAIN]: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=entry + ) ) - ) return True async def async_setup_entry(hass, config_entry): """Set up the Transmission Component.""" - if DOMAIN not in hass.data: - hass.data[DOMAIN] = {} - - if not config_entry.options: - await async_populate_options(hass, config_entry) - client = TransmissionClient(hass, config_entry) - client_id = config_entry.entry_id - hass.data[DOMAIN][client_id] = client + hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = client + if not await client.async_setup(): return False return True -async def async_unload_entry(hass, entry): +async def async_unload_entry(hass, config_entry): """Unload Transmission Entry from config_entry.""" - hass.services.async_remove(DOMAIN, SERVICE_ADD_TORRENT) - if hass.data[DOMAIN][entry.entry_id].unsub_timer: - hass.data[DOMAIN][entry.entry_id].unsub_timer() + client = hass.data[DOMAIN][config_entry.entry_id] + hass.services.async_remove(DOMAIN, client.service_name) + if client.unsub_timer: + client.unsub_timer() for component in "sensor", "switch": - await hass.config_entries.async_forward_entry_unload(entry, component) + await hass.config_entries.async_forward_entry_unload(config_entry, component) - del hass.data[DOMAIN] + hass.data[DOMAIN].pop(config_entry.entry_id) return True -async def get_api(hass, host, port, username=None, password=None): +async def get_api(hass, entry): """Get Transmission client.""" + host = entry[CONF_HOST] + port = entry[CONF_PORT] + username = entry.get(CONF_USERNAME) + password = entry.get(CONF_PASSWORD) + try: api = await hass.async_add_executor_job( transmissionrpc.Client, host, port, username, password ) + _LOGGER.debug("Successfully connected to %s", host) return api except TransmissionError as error: @@ -112,20 +114,13 @@ async def get_api(hass, host, port, username=None, password=None): _LOGGER.error("Credentials for Transmission client are not valid") raise AuthenticationError if "111: Connection refused" in str(error): - _LOGGER.error("Connecting to the Transmission client failed") + _LOGGER.error("Connecting to the Transmission client %s failed", host) raise CannotConnect _LOGGER.error(error) raise UnknownError -async def async_populate_options(hass, config_entry): - """Populate default options for Transmission Client.""" - options = {CONF_SCAN_INTERVAL: config_entry.data["options"][CONF_SCAN_INTERVAL]} - - hass.config_entries.async_update_entry(config_entry, options=options) - - class TransmissionClient: """Transmission Client Object.""" @@ -133,33 +128,35 @@ class TransmissionClient: """Initialize the Transmission RPC API.""" self.hass = hass self.config_entry = config_entry - self.scan_interval = self.config_entry.options[CONF_SCAN_INTERVAL] - self.tm_data = 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.""" + return self._tm_data + async def async_setup(self): """Set up the Transmission client.""" - config = { - CONF_HOST: self.config_entry.data[CONF_HOST], - CONF_PORT: self.config_entry.data[CONF_PORT], - CONF_USERNAME: self.config_entry.data.get(CONF_USERNAME), - CONF_PASSWORD: self.config_entry.data.get(CONF_PASSWORD), - } try: - api = await get_api(self.hass, **config) + api = await get_api(self.hass, self.config_entry.data) except CannotConnect: raise ConfigEntryNotReady except (AuthenticationError, UnknownError): return False - self.tm_data = self.hass.data[DOMAIN][DATA_TRANSMISSION] = TransmissionData( - self.hass, self.config_entry, api - ) + self._tm_data = TransmissionData(self.hass, self.config_entry, 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.set_scan_interval(self.scan_interval) + 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"]: self.hass.async_create_task( @@ -181,19 +178,31 @@ class TransmissionClient: ) self.hass.services.async_register( - DOMAIN, SERVICE_ADD_TORRENT, add_torrent, schema=SERVICE_ADD_TORRENT_SCHEMA + DOMAIN, self.service_name, add_torrent, schema=SERVICE_ADD_TORRENT_SCHEMA ) self.config_entry.add_update_listener(self.async_options_updated) return True + def add_options(self): + """Add options for entry.""" + if not self.config_entry.options: + scan_interval = self.config_entry.data.pop( + CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL + ) + options = {CONF_SCAN_INTERVAL: scan_interval} + + self.hass.config_entries.async_update_entry( + self.config_entry, options=options + ) + def set_scan_interval(self, scan_interval): """Update scan interval.""" - def refresh(event_time): + async def refresh(event_time): """Get the latest data from Transmission.""" - self.tm_data.update() + self._tm_data.update() if self.unsub_timer is not None: self.unsub_timer() @@ -215,6 +224,7 @@ class TransmissionData: def __init__(self, hass, config, api): """Initialize the Transmission RPC API.""" self.hass = hass + self.config = config self.data = None self.torrents = None self.session = None @@ -223,6 +233,16 @@ class TransmissionData: self.completed_torrents = [] self.started_torrents = [] + @property + def host(self): + """Return the host name.""" + 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 update(self): """Get the latest data from Transmission instance.""" try: @@ -232,14 +252,13 @@ class TransmissionData: self.check_completed_torrent() self.check_started_torrent() - _LOGGER.debug("Torrent Data Updated") + _LOGGER.debug("Torrent Data for %s Updated", self.host) self.available = True except TransmissionError: self.available = False - _LOGGER.error("Unable to connect to Transmission client") - - dispatcher_send(self.hass, DATA_UPDATED) + _LOGGER.error("Unable to connect to Transmission client %s", self.host) + dispatcher_send(self.hass, self.signal_options_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 99376f4b6e0..d7b9efb15d8 100644 --- a/homeassistant/components/transmission/config_flow.py +++ b/homeassistant/components/transmission/config_flow.py @@ -29,32 +29,32 @@ class TransmissionFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Get the options flow for this handler.""" return TransmissionOptionsFlowHandler(config_entry) - def __init__(self): - """Initialize the Transmission flow.""" - self.config = {} - self.errors = {} - async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" - if self.hass.config_entries.async_entries(DOMAIN): - return self.async_abort(reason="one_instance_allowed") + errors = {} if user_input is not None: - self.config[CONF_NAME] = user_input.pop(CONF_NAME) + for entry in self.hass.config_entries.async_entries(DOMAIN): + if entry.data[CONF_HOST] == user_input[CONF_HOST]: + return self.async_abort(reason="already_configured") + if entry.data[CONF_NAME] == user_input[CONF_NAME]: + errors[CONF_NAME] = "name_exists" + break + try: - await get_api(self.hass, **user_input) - self.config.update(user_input) - if "options" not in self.config: - self.config["options"] = {CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL} - return self.async_create_entry( - title=self.config[CONF_NAME], data=self.config - ) + await get_api(self.hass, user_input) + except AuthenticationError: - self.errors[CONF_USERNAME] = "wrong_credentials" - self.errors[CONF_PASSWORD] = "wrong_credentials" + errors[CONF_USERNAME] = "wrong_credentials" + errors[CONF_PASSWORD] = "wrong_credentials" except (CannotConnect, UnknownError): - self.errors["base"] = "cannot_connect" + errors["base"] = "cannot_connect" + + if not errors: + return self.async_create_entry( + title=user_input[CONF_NAME], data=user_input + ) return self.async_show_form( step_id="user", @@ -67,15 +67,12 @@ class TransmissionFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): vol.Required(CONF_PORT, default=DEFAULT_PORT): int, } ), - errors=self.errors, + errors=errors, ) async def async_step_import(self, import_config): """Import from Transmission client config.""" - self.config["options"] = { - CONF_SCAN_INTERVAL: import_config.pop(CONF_SCAN_INTERVAL).seconds - } - + import_config[CONF_SCAN_INTERVAL] = import_config[CONF_SCAN_INTERVAL].seconds return await self.async_step_user(user_input=import_config) @@ -95,8 +92,7 @@ class TransmissionOptionsFlowHandler(config_entries.OptionsFlow): vol.Optional( CONF_SCAN_INTERVAL, default=self.config_entry.options.get( - CONF_SCAN_INTERVAL, - self.config_entry.data["options"][CONF_SCAN_INTERVAL], + CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL ), ): int } diff --git a/homeassistant/components/transmission/const.py b/homeassistant/components/transmission/const.py index e4a8b1490c2..472bb32a391 100644 --- a/homeassistant/components/transmission/const.py +++ b/homeassistant/components/transmission/const.py @@ -21,4 +21,3 @@ ATTR_TORRENT = "torrent" SERVICE_ADD_TORRENT = "add_torrent" DATA_UPDATED = "transmission_data_updated" -DATA_TRANSMISSION = "data_transmission" diff --git a/homeassistant/components/transmission/sensor.py b/homeassistant/components/transmission/sensor.py index 30dfa4a3cbe..d9fd2b51144 100644 --- a/homeassistant/components/transmission/sensor.py +++ b/homeassistant/components/transmission/sensor.py @@ -6,7 +6,7 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from .const import DATA_TRANSMISSION, DATA_UPDATED, DOMAIN, SENSOR_TYPES +from .const import DOMAIN, SENSOR_TYPES _LOGGER = logging.getLogger(__name__) @@ -19,7 +19,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Transmission sensors.""" - transmission_api = hass.data[DOMAIN][DATA_TRANSMISSION] + tm_client = hass.data[DOMAIN][config_entry.entry_id] name = config_entry.data[CONF_NAME] dev = [] @@ -27,7 +27,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): dev.append( TransmissionSensor( sensor_type, - transmission_api, + tm_client, name, SENSOR_TYPES[sensor_type][0], SENSOR_TYPES[sensor_type][1], @@ -41,17 +41,12 @@ class TransmissionSensor(Entity): """Representation of a Transmission sensor.""" def __init__( - self, - sensor_type, - transmission_api, - client_name, - sensor_name, - unit_of_measurement, + self, sensor_type, tm_client, client_name, sensor_name, unit_of_measurement ): """Initialize the sensor.""" self._name = sensor_name self._state = None - self._transmission_api = transmission_api + self._tm_client = tm_client self._unit_of_measurement = unit_of_measurement self._data = None self.client_name = client_name @@ -62,6 +57,11 @@ class TransmissionSensor(Entity): """Return the name of the sensor.""" return f"{self.client_name} {self._name}" + @property + def unique_id(self): + """Return the unique id of the entity.""" + return f"{self._tm_client.api.host}-{self.name}" + @property def state(self): """Return the state of the sensor.""" @@ -80,12 +80,14 @@ class TransmissionSensor(Entity): @property def available(self): """Could the device be accessed during the last update call.""" - return self._transmission_api.available + return self._tm_client.api.available async def async_added_to_hass(self): """Handle entity which will be added.""" async_dispatcher_connect( - self.hass, DATA_UPDATED, self._schedule_immediate_update + self.hass, + self._tm_client.api.signal_options_update, + self._schedule_immediate_update, ) @callback @@ -94,12 +96,12 @@ class TransmissionSensor(Entity): def update(self): """Get the latest data from Transmission and updates the state.""" - self._data = self._transmission_api.data + self._data = self._tm_client.api.data if self.type == "completed_torrents": - self._state = self._transmission_api.get_completed_torrent_count() + self._state = self._tm_client.api.get_completed_torrent_count() elif self.type == "started_torrents": - self._state = self._transmission_api.get_started_torrent_count() + self._state = self._tm_client.api.get_started_torrent_count() if self.type == "current_status": if self._data: diff --git a/homeassistant/components/transmission/strings.json b/homeassistant/components/transmission/strings.json index 7160cd109c4..45c16be36e2 100644 --- a/homeassistant/components/transmission/strings.json +++ b/homeassistant/components/transmission/strings.json @@ -7,30 +7,25 @@ "data": { "name": "Name", "host": "Host", - "username": "User name", + "username": "Username", "password": "Password", "port": "Port" } - }, - "options": { - "title": "Configure Options", - "data": { - "scan_interval": "Update frequency" - } } }, "error": { + "name_exists": "Name already exists", "wrong_credentials": "Wrong username or password", "cannot_connect": "Unable to Connect to host" }, "abort": { - "one_instance_allowed": "Only a single instance is necessary." + "already_configured": "Host is already configured." } }, "options": { "step": { "init": { - "description": "Configure options for Transmission", + "title": "Configure options for Transmission", "data": { "scan_interval": "Update frequency" } diff --git a/homeassistant/components/transmission/switch.py b/homeassistant/components/transmission/switch.py index 0bb43f715ac..4b93b3f06e2 100644 --- a/homeassistant/components/transmission/switch.py +++ b/homeassistant/components/transmission/switch.py @@ -6,7 +6,7 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import ToggleEntity -from .const import DATA_TRANSMISSION, DATA_UPDATED, DOMAIN, SWITCH_TYPES +from .const import DOMAIN, SWITCH_TYPES _LOGGING = logging.getLogger(__name__) @@ -19,12 +19,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Transmission switch.""" - transmission_api = hass.data[DOMAIN][DATA_TRANSMISSION] + tm_client = hass.data[DOMAIN][config_entry.entry_id] name = config_entry.data[CONF_NAME] dev = [] for switch_type, switch_name in SWITCH_TYPES.items(): - dev.append(TransmissionSwitch(switch_type, switch_name, transmission_api, name)) + dev.append(TransmissionSwitch(switch_type, switch_name, tm_client, name)) async_add_entities(dev, True) @@ -32,12 +32,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class TransmissionSwitch(ToggleEntity): """Representation of a Transmission switch.""" - def __init__(self, switch_type, switch_name, transmission_api, name): + def __init__(self, switch_type, switch_name, tm_client, name): """Initialize the Transmission switch.""" self._name = switch_name self.client_name = name self.type = switch_type - self._transmission_api = transmission_api + self._tm_client = tm_client self._state = STATE_OFF self._data = None @@ -46,6 +46,11 @@ class TransmissionSwitch(ToggleEntity): """Return the name of the switch.""" return f"{self.client_name} {self._name}" + @property + def unique_id(self): + """Return the unique id of the entity.""" + return f"{self._tm_client.api.host}-{self.name}" + @property def state(self): """Return the state of the device.""" @@ -64,32 +69,34 @@ class TransmissionSwitch(ToggleEntity): @property def available(self): """Could the device be accessed during the last update call.""" - return self._transmission_api.available + return self._tm_client.api.available def turn_on(self, **kwargs): """Turn the device on.""" if self.type == "on_off": _LOGGING.debug("Starting all torrents") - self._transmission_api.start_torrents() + self._tm_client.api.start_torrents() elif self.type == "turtle_mode": _LOGGING.debug("Turning Turtle Mode of Transmission on") - self._transmission_api.set_alt_speed_enabled(True) - self._transmission_api.update() + self._tm_client.api.set_alt_speed_enabled(True) + self._tm_client.api.update() def turn_off(self, **kwargs): """Turn the device off.""" if self.type == "on_off": _LOGGING.debug("Stoping all torrents") - self._transmission_api.stop_torrents() + self._tm_client.api.stop_torrents() if self.type == "turtle_mode": _LOGGING.debug("Turning Turtle Mode of Transmission off") - self._transmission_api.set_alt_speed_enabled(False) - self._transmission_api.update() + self._tm_client.api.set_alt_speed_enabled(False) + self._tm_client.api.update() async def async_added_to_hass(self): """Handle entity which will be added.""" async_dispatcher_connect( - self.hass, DATA_UPDATED, self._schedule_immediate_update + self.hass, + self._tm_client.api.signal_options_update, + self._schedule_immediate_update, ) @callback @@ -100,12 +107,12 @@ class TransmissionSwitch(ToggleEntity): """Get the latest data from Transmission and updates the state.""" active = None if self.type == "on_off": - self._data = self._transmission_api.data + self._data = self._tm_client.api.data if self._data: active = self._data.activeTorrentCount > 0 elif self.type == "turtle_mode": - active = self._transmission_api.get_alt_speed_enabled() + active = self._tm_client.api.get_alt_speed_enabled() if active is None: return diff --git a/homeassistant/components/transport_nsw/sensor.py b/homeassistant/components/transport_nsw/sensor.py index 9d0610c139e..79df41ac489 100644 --- a/homeassistant/components/transport_nsw/sensor.py +++ b/homeassistant/components/transport_nsw/sensor.py @@ -3,6 +3,7 @@ from datetime import timedelta import logging import voluptuous as vol +from TransportNSW import TransportNSW import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -120,8 +121,6 @@ class PublicTransportData: def __init__(self, stop_id, route, destination, api_key): """Initialize the data object.""" - import TransportNSW - self._stop_id = stop_id self._route = route self._destination = destination @@ -134,7 +133,7 @@ class PublicTransportData: ATTR_DESTINATION: "n/a", ATTR_MODE: None, } - self.tnsw = TransportNSW.TransportNSW() + self.tnsw = TransportNSW() def update(self): """Get the next leave time.""" diff --git a/homeassistant/components/trend/binary_sensor.py b/homeassistant/components/trend/binary_sensor.py index 9154f891cc1..7c4a2dc4067 100644 --- a/homeassistant/components/trend/binary_sensor.py +++ b/homeassistant/components/trend/binary_sensor.py @@ -3,6 +3,7 @@ from collections import deque import logging import math +import numpy as np import voluptuous as vol from homeassistant.components.binary_sensor import ( @@ -17,9 +18,9 @@ from homeassistant.const import ( CONF_DEVICE_CLASS, CONF_ENTITY_ID, CONF_FRIENDLY_NAME, - STATE_UNKNOWN, - STATE_UNAVAILABLE, CONF_SENSORS, + STATE_UNAVAILABLE, + STATE_UNKNOWN, ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv @@ -207,8 +208,6 @@ class SensorTrend(BinarySensorDevice): This need run inside executor. """ - import numpy as np - timestamps = np.array([t for t, _ in self.samples]) values = np.array([s for _, s in self.samples]) coeffs = np.polyfit(timestamps, values, 1) diff --git a/homeassistant/components/trend/manifest.json b/homeassistant/components/trend/manifest.json index 1432d2d21a0..cf9333be7c3 100644 --- a/homeassistant/components/trend/manifest.json +++ b/homeassistant/components/trend/manifest.json @@ -3,7 +3,7 @@ "name": "Trend", "documentation": "https://www.home-assistant.io/integrations/trend", "requirements": [ - "numpy==1.17.1" + "numpy==1.17.3" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index 3e7900502d6..2ce0e18bee5 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -11,17 +11,18 @@ import re from typing import Optional from aiohttp import web +import mutagen import voluptuous as vol from homeassistant.components.http import HomeAssistantView from homeassistant.components.media_player.const import ( ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, + DOMAIN as DOMAIN_MP, MEDIA_TYPE_MUSIC, SERVICE_PLAY_MEDIA, ) -from homeassistant.components.media_player.const import DOMAIN as DOMAIN_MP -from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, CONF_PLATFORM +from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM, ENTITY_MATCH_ALL from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_per_platform @@ -29,7 +30,6 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import HomeAssistantType from homeassistant.setup import async_prepare_setup_platform - # mypy: allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) @@ -433,7 +433,6 @@ class SpeechManager: Async friendly. """ - import mutagen data_bytes = io.BytesIO(data) data_bytes.name = filename diff --git a/homeassistant/components/twilio/.translations/ca.json b/homeassistant/components/twilio/.translations/ca.json index 324ab0dd69a..bad78e51a36 100644 --- a/homeassistant/components/twilio/.translations/ca.json +++ b/homeassistant/components/twilio/.translations/ca.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia." }, "create_entry": { - "default": "Per enviar esdeveniments a Home Assistant, haur\u00e0s de configurar [Webhooks amb Twilio]({twilio_url}).\n\nCompleta la seg\u00fcent informaci\u00f3 : \n\n- URL: `{webhook_url}` \n- M\u00e8tode: POST \n- Tipus de contingut: application/x-www-form-urlencoded\n\nConsulta la [documentaci\u00f3]({docs_url}) sobre com configurar els automatismes per gestionar dades entrants." + "default": "Per enviar esdeveniments a Home Assistant, haur\u00e0s de configurar [Webhooks amb Twilio]({twilio_url}).\n\nCompleta la seg\u00fcent informaci\u00f3 : \n\n- URL: `{webhook_url}` \n- M\u00e8tode: POST \n- Tipus de contingut: application/x-www-form-urlencoded\n\nConsulta la [documentaci\u00f3]({docs_url}) sobre com configurar les automatitzacions per gestionar dades entrants." }, "step": { "user": { diff --git a/homeassistant/components/twilio/.translations/ru.json b/homeassistant/components/twilio/.translations/ru.json index b8d6f11f7ef..1c4e0653496 100644 --- a/homeassistant/components/twilio/.translations/ru.json +++ b/homeassistant/components/twilio/.translations/ru.json @@ -10,7 +10,7 @@ "step": { "user": { "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 Twilio?", - "title": "Twilio Webhook" + "title": "Twilio" } }, "title": "Twilio" diff --git a/homeassistant/components/twilio/__init__.py b/homeassistant/components/twilio/__init__.py index ea5629e7cab..15c6697b2f7 100644 --- a/homeassistant/components/twilio/__init__.py +++ b/homeassistant/components/twilio/__init__.py @@ -1,9 +1,12 @@ """Support for Twilio.""" +from twilio.rest import Client +from twilio.twiml import TwiML import voluptuous as vol -import homeassistant.helpers.config_validation as cv 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 CONF_ACCOUNT_SID = "account_sid" @@ -28,8 +31,6 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass, config): """Set up the Twilio component.""" - from twilio.rest import Client - if DOMAIN not in config: return True @@ -42,8 +43,6 @@ async def async_setup(hass, config): async def handle_webhook(hass, webhook_id, request): """Handle incoming webhook from Twilio for inbound messages and calls.""" - from twilio.twiml import TwiML - data = dict(await request.post()) data["webhook_id"] = webhook_id hass.bus.async_fire(RECEIVED_DATA, dict(data)) diff --git a/homeassistant/components/twilio/config_flow.py b/homeassistant/components/twilio/config_flow.py index dad8e0bf496..1539c1ffadc 100644 --- a/homeassistant/components/twilio/config_flow.py +++ b/homeassistant/components/twilio/config_flow.py @@ -3,7 +3,6 @@ from homeassistant.helpers import config_entry_flow from .const import DOMAIN - config_entry_flow.register_webhook_flow( DOMAIN, "Twilio Webhook", diff --git a/homeassistant/components/twilio/manifest.json b/homeassistant/components/twilio/manifest.json index 23fac51a347..8f4ed125fb6 100644 --- a/homeassistant/components/twilio/manifest.json +++ b/homeassistant/components/twilio/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/twilio", "requirements": [ - "twilio==6.19.1" + "twilio==6.32.0" ], "dependencies": [ "webhook" diff --git a/homeassistant/components/twilio_call/notify.py b/homeassistant/components/twilio_call/notify.py index 0672a3d3b9e..82705091814 100644 --- a/homeassistant/components/twilio_call/notify.py +++ b/homeassistant/components/twilio_call/notify.py @@ -4,14 +4,13 @@ import urllib import voluptuous as vol -from homeassistant.components.twilio import DATA_TWILIO -import homeassistant.helpers.config_validation as cv - from homeassistant.components.notify import ( ATTR_TARGET, PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.components.twilio import DATA_TWILIO +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/twilio_sms/notify.py b/homeassistant/components/twilio_sms/notify.py index bd873f13468..da5e0e754b9 100644 --- a/homeassistant/components/twilio_sms/notify.py +++ b/homeassistant/components/twilio_sms/notify.py @@ -3,15 +3,14 @@ import logging import voluptuous as vol -from homeassistant.components.twilio import DATA_TWILIO -import homeassistant.helpers.config_validation as cv - from homeassistant.components.notify import ( + ATTR_DATA, ATTR_TARGET, PLATFORM_SCHEMA, BaseNotificationService, - ATTR_DATA, ) +from homeassistant.components.twilio import DATA_TWILIO +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -26,6 +25,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( r"^\+?[1-9]\d{1,14}$|" r"^(?=.{1,11}$)[a-zA-Z0-9\s]*" r"[a-zA-Z][a-zA-Z0-9\s]*$" + r"^(?:[a-zA-Z]+)\:?\+?[1-9]\d{1,14}$|" ), ) } diff --git a/homeassistant/components/unifi/.translations/fr.json b/homeassistant/components/unifi/.translations/fr.json index c40b7822073..0a100be0a11 100644 --- a/homeassistant/components/unifi/.translations/fr.json +++ b/homeassistant/components/unifi/.translations/fr.json @@ -33,6 +33,12 @@ "track_wired_clients": "Inclure les clients du r\u00e9seau filaire" } }, + "init": { + "data": { + "one": "Vide", + "other": "Vide" + } + }, "statistics_sensors": { "data": { "allow_bandwidth_sensors": "Cr\u00e9er des capteurs d'utilisation de la bande passante pour les clients r\u00e9seau" diff --git a/homeassistant/components/unifi/.translations/ko.json b/homeassistant/components/unifi/.translations/ko.json index 1fff9887906..295430b7284 100644 --- a/homeassistant/components/unifi/.translations/ko.json +++ b/homeassistant/components/unifi/.translations/ko.json @@ -32,6 +32,11 @@ "track_devices": "\ub124\ud2b8\uc6cc\ud06c \uae30\uae30 \ucd94\uc801 (Ubiquiti \uae30\uae30)", "track_wired_clients": "\uc720\uc120 \ub124\ud2b8\uc6cc\ud06c \ud074\ub77c\uc774\uc5b8\ud2b8 \ud3ec\ud568" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "\ub124\ud2b8\uc6cc\ud06c \ud074\ub77c\uc774\uc5b8\ud2b8\ub97c \uc704\ud55c \ub300\uc5ed\ud3ed \uc0ac\uc6a9\ub7c9 \uc13c\uc11c \uc0dd\uc131\ud558\uae30" + } } } } diff --git a/homeassistant/components/unifi/.translations/nl.json b/homeassistant/components/unifi/.translations/nl.json index 518f0066534..36e21728f1d 100644 --- a/homeassistant/components/unifi/.translations/nl.json +++ b/homeassistant/components/unifi/.translations/nl.json @@ -38,6 +38,11 @@ "one": "Leeg", "other": "Leeg" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Maak bandbreedtegebruiksensoren voor netwerkclients" + } } } } diff --git a/homeassistant/components/unifi/.translations/pt-BR.json b/homeassistant/components/unifi/.translations/pt-BR.json index 113eaa000fe..a57bb33ee7a 100644 --- a/homeassistant/components/unifi/.translations/pt-BR.json +++ b/homeassistant/components/unifi/.translations/pt-BR.json @@ -32,6 +32,12 @@ "track_devices": "Rastrear dispositivos de rede (dispositivos Ubiquiti)", "track_wired_clients": "Incluir clientes de rede com fio" } + }, + "init": { + "data": { + "one": "um", + "other": "uns" + } } } } diff --git a/homeassistant/components/unifi/.translations/pt.json b/homeassistant/components/unifi/.translations/pt.json index 6730a3d258e..c602a58660b 100644 --- a/homeassistant/components/unifi/.translations/pt.json +++ b/homeassistant/components/unifi/.translations/pt.json @@ -22,5 +22,28 @@ } }, "title": "Controlador UniFi" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "detection_time": "Tempo em segundos desde a \u00faltima vez que foi visto at\u00e9 ser considerado afastado", + "track_clients": "Acompanhar clientes da rede", + "track_devices": "Acompanhar dispositivos de rede (dispositivos Ubiquiti)", + "track_wired_clients": "Incluir clientes da rede cablada" + } + }, + "init": { + "data": { + "one": "Vazio", + "other": "Vazios" + } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Criar sensores de uso de largura de banda para clientes da rede" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/ru.json b/homeassistant/components/unifi/.translations/ru.json index d7451bd81a0..dbb6efd8343 100644 --- a/homeassistant/components/unifi/.translations/ru.json +++ b/homeassistant/components/unifi/.translations/ru.json @@ -2,11 +2,11 @@ "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.", - "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" + "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", - "service_unavailable": "\u0421\u043b\u0443\u0436\u0431\u0430 \u043d\u0435 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430" + "faulty_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\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": { "user": { diff --git a/homeassistant/components/unifi/__init__.py b/homeassistant/components/unifi/__init__.py index 5b43289e403..4f3edf9ce79 100644 --- a/homeassistant/components/unifi/__init__.py +++ b/homeassistant/components/unifi/__init__.py @@ -77,12 +77,13 @@ async def async_setup_entry(hass, config_entry): hass.data[DOMAIN] = {} controller = UniFiController(hass, config_entry) - controller_id = get_controller_id_from_config_entry(config_entry) - hass.data[DOMAIN][controller_id] = controller if not await controller.async_setup(): return False + controller_id = get_controller_id_from_config_entry(config_entry) + hass.data[DOMAIN][controller_id] = controller + if controller.mac is None: return True diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index fdb75d09194..01b97a78366 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -12,6 +12,7 @@ from homeassistant.const import ( ) from .const import ( + CONF_ALLOW_BANDWIDTH_SENSORS, CONF_CONTROLLER, CONF_DETECTION_TIME, CONF_SITE_ID, @@ -19,6 +20,7 @@ from .const import ( CONF_TRACK_DEVICES, CONF_TRACK_WIRED_CLIENTS, CONTROLLER_ID, + DEFAULT_ALLOW_BANDWIDTH_SENSORS, DEFAULT_TRACK_CLIENTS, DEFAULT_TRACK_DEVICES, DEFAULT_TRACK_WIRED_CLIENTS, @@ -148,12 +150,6 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.desc = next(iter(self.sites.values()))["desc"] return await self.async_step_site(user_input={}) - if self.desc is not None: - for site in self.sites.values(): - if self.desc == site["name"]: - self.desc = site["desc"] - return await self.async_step_site(user_input={}) - sites = [] for site in self.sites.values(): sites.append(site["desc"]) @@ -171,6 +167,7 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow): def __init__(self, config_entry): """Initialize UniFi options flow.""" self.config_entry = config_entry + self.options = dict(config_entry.options) async def async_step_init(self, user_input=None): """Manage the UniFi options.""" @@ -179,7 +176,8 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow): async def async_step_device_tracker(self, user_input=None): """Manage the device tracker options.""" if user_input is not None: - return self.async_create_entry(title="", data=user_input) + self.options.update(user_input) + return await self.async_step_statistics_sensors() return self.async_show_form( step_id="device_tracker", @@ -212,3 +210,28 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow): } ), ) + + async def async_step_statistics_sensors(self, user_input=None): + """Manage the statistics sensors options.""" + if user_input is not None: + self.options.update(user_input) + return await self._update_options() + + return self.async_show_form( + step_id="statistics_sensors", + data_schema=vol.Schema( + { + vol.Optional( + CONF_ALLOW_BANDWIDTH_SENSORS, + default=self.config_entry.options.get( + CONF_ALLOW_BANDWIDTH_SENSORS, + DEFAULT_ALLOW_BANDWIDTH_SENSORS, + ), + ): bool + } + ), + ) + + async def _update_options(self): + """Update config entry options.""" + return self.async_create_entry(title="", data=self.options) diff --git a/homeassistant/components/unifi/const.py b/homeassistant/components/unifi/const.py index eac14735074..d82b7b49d45 100644 --- a/homeassistant/components/unifi/const.py +++ b/homeassistant/components/unifi/const.py @@ -12,6 +12,7 @@ CONF_SITE_ID = "site" UNIFI_CONFIG = "unifi_config" UNIFI_WIRELESS_CLIENTS = "unifi_wireless_clients" +CONF_ALLOW_BANDWIDTH_SENSORS = "allow_bandwidth_sensors" CONF_BLOCK_CLIENT = "block_client" CONF_DETECTION_TIME = "detection_time" CONF_TRACK_CLIENTS = "track_clients" @@ -23,6 +24,7 @@ CONF_DONT_TRACK_CLIENTS = "dont_track_clients" CONF_DONT_TRACK_DEVICES = "dont_track_devices" CONF_DONT_TRACK_WIRED_CLIENTS = "dont_track_wired_clients" +DEFAULT_ALLOW_BANDWIDTH_SENSORS = False DEFAULT_BLOCK_CLIENTS = [] DEFAULT_TRACK_CLIENTS = True DEFAULT_TRACK_DEVICES = True diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index ffea98b9050..3deb2e9040a 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -15,6 +15,7 @@ from homeassistant.helpers import aiohttp_client from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import ( + CONF_ALLOW_BANDWIDTH_SENSORS, CONF_BLOCK_CLIENT, CONF_CONTROLLER, CONF_DETECTION_TIME, @@ -27,6 +28,7 @@ from .const import ( CONF_SITE_ID, CONF_SSID_FILTER, CONTROLLER_ID, + DEFAULT_ALLOW_BANDWIDTH_SENSORS, DEFAULT_BLOCK_CLIENTS, DEFAULT_TRACK_CLIENTS, DEFAULT_TRACK_DEVICES, @@ -40,6 +42,8 @@ from .const import ( ) from .errors import AuthenticationRequired, CannotConnect +SUPPORTED_PLATFORMS = ["device_tracker", "sensor", "switch"] + class UniFiController: """Manages a single UniFi Controller.""" @@ -53,6 +57,7 @@ class UniFiController: self.progress = None self.wireless_clients = None + self.listeners = [] self._site_name = None self._site_role = None @@ -76,6 +81,13 @@ class UniFiController: """Return the site user role of this controller.""" return self._site_role + @property + def option_allow_bandwidth_sensors(self): + """Config entry option to allow bandwidth sensors.""" + return self.config_entry.options.get( + CONF_ALLOW_BANDWIDTH_SENSORS, DEFAULT_ALLOW_BANDWIDTH_SENSORS + ) + @property def option_block_clients(self): """Config entry option with list of clients to control network access.""" @@ -225,7 +237,7 @@ class UniFiController: self.config_entry.add_update_listener(self.async_options_updated) - for platform in ["device_tracker", "switch"]: + for platform in SUPPORTED_PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup( self.config_entry, platform @@ -247,13 +259,14 @@ class UniFiController: def import_configuration(self): """Import configuration to config entry options.""" - unifi_config = {} + import_config = {} + for config in self.hass.data[UNIFI_CONFIG]: if ( self.host == config[CONF_HOST] and self.site_name == config[CONF_SITE_ID] ): - unifi_config = config + import_config = config break old_options = dict(self.config_entry.options) @@ -267,16 +280,17 @@ class UniFiController: (CONF_DETECTION_TIME, CONF_DETECTION_TIME), (CONF_SSID_FILTER, CONF_SSID_FILTER), ): - if config in unifi_config: - if config == option and unifi_config[ + if config in import_config: + print(config) + if config == option and import_config[ config ] != self.config_entry.options.get(option): - new_options[option] = unifi_config[config] + new_options[option] = import_config[config] elif config != option and ( option not in self.config_entry.options - or unifi_config[config] == self.config_entry.options.get(option) + or import_config[config] == self.config_entry.options.get(option) ): - new_options[option] = not unifi_config[config] + new_options[option] = not import_config[config] if new_options: options = {**old_options, **new_options} @@ -290,15 +304,15 @@ class UniFiController: Will cancel any scheduled setup retry and will unload the config entry. """ - # If the authentication was wrong. - if self.api is None: - return True - - for platform in ["device_tracker", "switch"]: + for platform in SUPPORTED_PLATFORMS: await self.hass.config_entries.async_forward_entry_unload( self.config_entry, platform ) + for unsub_dispatcher in self.listeners: + unsub_dispatcher() + self.listeners = [] + return True diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index 48b19d7bada..b92211c4eae 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -67,7 +67,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Update the values of the controller.""" update_items(controller, async_add_entities, tracked) - async_dispatcher_connect(hass, controller.signal_update, update_controller) + controller.listeners.append( + async_dispatcher_connect(hass, controller.signal_update, update_controller) + ) @callback def update_disable_on_entities(): @@ -82,8 +84,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entity.registry_entry.entity_id, disabled_by=disabled_by ) - async_dispatcher_connect( - hass, controller.signal_options_update, update_disable_on_entities + controller.listeners.append( + async_dispatcher_connect( + hass, controller.signal_options_update, update_disable_on_entities + ) ) update_controller() diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py new file mode 100644 index 00000000000..e4f9b0df6c9 --- /dev/null +++ b/homeassistant/components/unifi/sensor.py @@ -0,0 +1,172 @@ +"""Support for bandwidth sensors with UniFi clients.""" +import logging + +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 import Entity +from homeassistant.helpers.entity_registry import DISABLED_CONFIG_ENTRY + +LOGGER = logging.getLogger(__name__) + +ATTR_RECEIVING = "receiving" +ATTR_TRANSMITTING = "transmitting" + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Sensor platform doesn't support configuration through configuration.yaml.""" + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up sensors for UniFi integration.""" + controller = get_controller_from_config_entry(hass, config_entry) + sensors = {} + + registry = await entity_registry.async_get_registry(hass) + + @callback + def update_controller(): + """Update the values of the controller.""" + update_items(controller, async_add_entities, sensors) + + controller.listeners.append( + async_dispatcher_connect(hass, controller.signal_update, update_controller) + ) + + @callback + def update_disable_on_entities(): + """Update the values of the controller.""" + for entity in sensors.values(): + + disabled_by = None + if not entity.entity_registry_enabled_default and entity.enabled: + disabled_by = DISABLED_CONFIG_ENTRY + + registry.async_update_entity( + entity.registry_entry.entity_id, disabled_by=disabled_by + ) + + controller.listeners.append( + async_dispatcher_connect( + hass, controller.signal_options_update, update_disable_on_entities + ) + ) + + update_controller() + + +@callback +def update_items(controller, async_add_entities, sensors): + """Update sensors from the controller.""" + new_sensors = [] + + for client_id in controller.api.clients: + for direction, sensor_class in ( + ("rx", UniFiRxBandwidthSensor), + ("tx", UniFiTxBandwidthSensor), + ): + item_id = f"{direction}-{client_id}" + + if item_id in sensors: + sensor = sensors[item_id] + if sensor.enabled: + sensor.async_schedule_update_ha_state() + continue + + sensors[item_id] = sensor_class( + controller.api.clients[client_id], controller + ) + new_sensors.append(sensors[item_id]) + + if new_sensors: + async_add_entities(new_sensors) + + +class UniFiBandwidthSensor(Entity): + """UniFi Bandwidth sensor base class.""" + + def __init__(self, client, controller): + """Set up client.""" + self.client = client + self.controller = controller + self.is_wired = self.client.mac not in controller.wireless_clients + + @property + def entity_registry_enabled_default(self): + """Return if the entity should be enabled when first added to the entity registry.""" + if self.controller.option_allow_bandwidth_sensors: + return True + return False + + async def async_added_to_hass(self): + """Client entity created.""" + LOGGER.debug("New UniFi bandwidth sensor %s (%s)", self.name, self.client.mac) + + async def async_update(self): + """Synchronize state with controller. + + 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 bandwidth sensor %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 + + @property + def available(self) -> bool: + """Return if controller is available.""" + return self.controller.available + + @property + def device_info(self): + """Return a device description for device registry.""" + return {"connections": {(CONNECTION_NETWORK_MAC, self.client.mac)}} + + +class UniFiRxBandwidthSensor(UniFiBandwidthSensor): + """Receiving bandwidth sensor.""" + + @property + def state(self): + """Return the state of the sensor.""" + if self.is_wired: + return self.client.wired_rx_bytes / 1000000 + return self.client.raw.get("rx_bytes", 0) / 1000000 + + @property + def name(self): + """Return the name of the client.""" + name = self.client.name or self.client.hostname + return f"{name} RX" + + @property + def unique_id(self): + """Return a unique identifier for this bandwidth sensor.""" + return f"rx-{self.client.mac}" + + +class UniFiTxBandwidthSensor(UniFiBandwidthSensor): + """Transmitting bandwidth sensor.""" + + @property + def state(self): + """Return the state of the sensor.""" + if self.is_wired: + return self.client.wired_tx_bytes / 1000000 + return self.client.raw.get("tx_bytes", 0) / 1000000 + + @property + def name(self): + """Return the name of the client.""" + name = self.client.name or self.client.hostname + return f"{name} TX" + + @property + def unique_id(self): + """Return a unique identifier for this bandwidth sensor.""" + return f"tx-{self.client.mac}" diff --git a/homeassistant/components/unifi/strings.json b/homeassistant/components/unifi/strings.json index c484bfbf09f..ce2f2345917 100644 --- a/homeassistant/components/unifi/strings.json +++ b/homeassistant/components/unifi/strings.json @@ -35,6 +35,11 @@ "track_devices": "Track network devices (Ubiquiti devices)", "track_wired_clients": "Include wired network clients" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Create bandwidth usage sensors for network clients" + } } } } diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index f0183a7ecb3..82aa6f0384d 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -14,7 +14,6 @@ LOGGER = logging.getLogger(__name__) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Component doesn't support configuration through configuration.yaml.""" - pass async def async_setup_entry(hass, config_entry, async_add_entities): @@ -54,7 +53,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Update the values of the controller.""" update_items(controller, async_add_entities, switches, switches_off) - async_dispatcher_connect(hass, controller.signal_update, update_controller) + controller.listeners.append( + async_dispatcher_connect(hass, controller.signal_update, update_controller) + ) update_controller() switches_off.clear() @@ -231,8 +232,6 @@ class UniFiPOEClientSwitch(UniFiClient, SwitchDevice, RestoreEntity): """Return the device state attributes.""" attributes = { "power": self.port.poe_power, - "received": self.client.wired_rx_bytes / 1000000, - "sent": self.client.wired_tx_bytes / 1000000, "switch": self.client.sw_mac, "port": self.client.sw_port, "poe_mode": self.poe_mode, diff --git a/homeassistant/components/upcloud/__init__.py b/homeassistant/components/upcloud/__init__.py index 3656ba48e74..c77b0fe3cdd 100644 --- a/homeassistant/components/upcloud/__init__.py +++ b/homeassistant/components/upcloud/__init__.py @@ -1,15 +1,16 @@ """Support for UpCloud.""" -import logging from datetime import timedelta +import logging +import upcloud_api import voluptuous as vol from homeassistant.const import ( - CONF_USERNAME, CONF_PASSWORD, CONF_SCAN_INTERVAL, - STATE_ON, + CONF_USERNAME, STATE_OFF, + STATE_ON, STATE_PROBLEM, ) from homeassistant.core import callback @@ -60,8 +61,6 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): """Set up the UpCloud component.""" - import upcloud_api - conf = config[DOMAIN] username = conf.get(CONF_USERNAME) password = conf.get(CONF_PASSWORD) diff --git a/homeassistant/components/updater/__init__.py b/homeassistant/components/updater/__init__.py index dd270a0bb75..22c11d0c38e 100644 --- a/homeassistant/components/updater/__init__.py +++ b/homeassistant/components/updater/__init__.py @@ -10,6 +10,7 @@ import uuid import aiohttp import async_timeout +from distro import linux_distribution import voluptuous as vol from homeassistant.const import __version__ as current_version @@ -145,9 +146,7 @@ async def get_newest_version(hass, huuid, include_components): if include_components: info_object["components"] = list(hass.config.components) - import distro - - linux_dist = await hass.async_add_executor_job(distro.linux_distribution, False) + linux_dist = await hass.async_add_executor_job(linux_distribution, False) info_object["distribution"] = linux_dist[0] info_object["os_version"] = linux_dist[1] diff --git a/homeassistant/components/upnp/.translations/fr.json b/homeassistant/components/upnp/.translations/fr.json index a87ea9ec9c7..6864658b379 100644 --- a/homeassistant/components/upnp/.translations/fr.json +++ b/homeassistant/components/upnp/.translations/fr.json @@ -8,6 +8,10 @@ "no_sensors_or_port_mapping": "Activer au moins les capteurs ou la cartographie des ports", "single_instance_allowed": "Une seule configuration UPnP / IGD est n\u00e9cessaire." }, + "error": { + "one": "Vide", + "other": "Vide" + }, "step": { "confirm": { "description": "Voulez-vous configurer UPnP / IGD?", diff --git a/homeassistant/components/upnp/.translations/nn.json b/homeassistant/components/upnp/.translations/nn.json index cfbedd994af..8e173e4297f 100644 --- a/homeassistant/components/upnp/.translations/nn.json +++ b/homeassistant/components/upnp/.translations/nn.json @@ -8,8 +8,16 @@ "other": "Andre" }, "step": { + "confirm": { + "title": "UPnP/IGD" + }, "init": { "title": "UPnP / IGD" + }, + "user": { + "data": { + "igd": "UPnP/IGD" + } } }, "title": "UPnP / IGD" diff --git a/homeassistant/components/upnp/.translations/ru.json b/homeassistant/components/upnp/.translations/ru.json index 3351f0d5d8a..9599832799f 100644 --- a/homeassistant/components/upnp/.translations/ru.json +++ b/homeassistant/components/upnp/.translations/ru.json @@ -2,10 +2,10 @@ "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.", - "incomplete_device": "\u0418\u0433\u043d\u043e\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u043d\u0435\u043f\u043e\u043b\u043d\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 UPnP", - "no_devices_discovered": "\u041d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e UPnP / IGD", + "incomplete_device": "\u0418\u0433\u043d\u043e\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u043d\u0435\u043f\u043e\u043b\u043d\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 UPnP.", + "no_devices_discovered": "\u041d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e UPnP / IGD.", "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 UPnP / IGD \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", - "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", + "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." }, "step": { diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py index 5a5c7b38e7e..7f7f0f5b93a 100644 --- a/homeassistant/components/upnp/device.py +++ b/homeassistant/components/upnp/device.py @@ -3,6 +3,7 @@ import asyncio from ipaddress import IPv4Address import aiohttp +from async_upnp_client.profiles.igd import IgdDevice from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import HomeAssistantType @@ -29,9 +30,6 @@ class Device: if local_ip: local_ip = IPv4Address(local_ip) - # discover devices - from async_upnp_client.profiles.igd import IgdDevice - discovery_infos = await IgdDevice.async_search(source_ip=local_ip, timeout=10) # add extra info and store devices @@ -61,9 +59,6 @@ class Device: factory = UpnpFactory(requester, disable_state_variable_validation=True) upnp_device = await factory.async_create_device(ssdp_description) - # wrap with async_upnp_client.IgdDevice - from async_upnp_client.profiles.igd import IgdDevice - igd_device = IgdDevice(upnp_device, None) return cls(igd_device) diff --git a/homeassistant/components/upnp/sensor.py b/homeassistant/components/upnp/sensor.py index b721fa29cdd..40cb7ef2032 100644 --- a/homeassistant/components/upnp/sensor.py +++ b/homeassistant/components/upnp/sensor.py @@ -164,7 +164,6 @@ class PerSecondUPnPIGDSensor(UpnpSensor): """Get unit we are measuring in.""" raise NotImplementedError() - @property def _async_fetch_value(self): """Fetch a value from the IGD.""" raise NotImplementedError() diff --git a/homeassistant/components/uscis/sensor.py b/homeassistant/components/uscis/sensor.py index bda6ad9041b..3f5175ad09d 100644 --- a/homeassistant/components/uscis/sensor.py +++ b/homeassistant/components/uscis/sensor.py @@ -1,7 +1,8 @@ """Support for USCIS Case Status.""" - import logging from datetime import timedelta + +import uscisstatus import voluptuous as vol from homeassistant.helpers.entity import Entity @@ -67,8 +68,6 @@ class UscisSensor(Entity): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Fetch data from the USCIS website and update state attributes.""" - import uscisstatus - try: status = uscisstatus.get_case_status(self._case_id) self._attributes = {self.CURRENT_STATUS: status["status"]} diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index 9bc376916c6..55e56415b0d 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -6,7 +6,7 @@ import logging import voluptuous as vol from homeassistant.components import group -from homeassistant.const import ( +from homeassistant.const import ( # noqa: F401 # STATE_PAUSED/IDLE are API ATTR_BATTERY_LEVEL, ATTR_COMMAND, SERVICE_TOGGLE, @@ -68,8 +68,6 @@ VACUUM_SEND_COMMAND_SERVICE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( STATE_CLEANING = "cleaning" STATE_DOCKED = "docked" -STATE_IDLE = STATE_IDLE -STATE_PAUSED = STATE_PAUSED STATE_RETURNING = "returning" STATE_ERROR = "error" diff --git a/homeassistant/components/vacuum/reproduce_state.py b/homeassistant/components/vacuum/reproduce_state.py new file mode 100644 index 00000000000..485ffef0c9f --- /dev/null +++ b/homeassistant/components/vacuum/reproduce_state.py @@ -0,0 +1,101 @@ +"""Reproduce an Vacuum state.""" +import asyncio +import logging +from typing import Iterable, Optional + +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_IDLE, + STATE_OFF, + STATE_ON, + STATE_PAUSED, +) +from homeassistant.core import Context, State +from homeassistant.helpers.typing import HomeAssistantType + +from . import ( + ATTR_FAN_SPEED, + DOMAIN, + SERVICE_PAUSE, + SERVICE_RETURN_TO_BASE, + SERVICE_SET_FAN_SPEED, + SERVICE_START, + SERVICE_STOP, + STATE_CLEANING, + STATE_DOCKED, + STATE_RETURNING, +) + +_LOGGER = logging.getLogger(__name__) + +VALID_STATES_TOGGLE = {STATE_ON, STATE_OFF} +VALID_STATES_STATE = { + STATE_CLEANING, + STATE_DOCKED, + STATE_IDLE, + STATE_RETURNING, + STATE_PAUSED, +} + + +async def _async_reproduce_state( + hass: HomeAssistantType, state: State, context: Optional[Context] = None +) -> None: + """Reproduce a single state.""" + cur_state = hass.states.get(state.entity_id) + + if cur_state is None: + _LOGGER.warning("Unable to find entity %s", state.entity_id) + return + + if state.state not in VALID_STATES_TOGGLE and state.state not in VALID_STATES_STATE: + _LOGGER.warning( + "Invalid state specified for %s: %s", state.entity_id, state.state + ) + return + + # Return if we are already at the right state. + if cur_state.state == state.state and cur_state.attributes.get( + ATTR_FAN_SPEED + ) == state.attributes.get(ATTR_FAN_SPEED): + return + + service_data = {ATTR_ENTITY_ID: state.entity_id} + + if cur_state.state != state.state: + # Wrong state + if state.state == STATE_ON: + service = SERVICE_TURN_ON + elif state.state == STATE_OFF: + service = SERVICE_TURN_OFF + elif state.state == STATE_CLEANING: + service = SERVICE_START + elif state.state == STATE_DOCKED or state.state == STATE_RETURNING: + service = SERVICE_RETURN_TO_BASE + elif state.state == STATE_IDLE: + service = SERVICE_STOP + elif state.state == STATE_PAUSED: + service = SERVICE_PAUSE + + await hass.services.async_call( + DOMAIN, service, service_data, context=context, blocking=True + ) + + if cur_state.attributes.get(ATTR_FAN_SPEED) != state.attributes.get(ATTR_FAN_SPEED): + # Wrong fan speed + service_data["fan_speed"] = state.attributes[ATTR_FAN_SPEED] + await hass.services.async_call( + DOMAIN, SERVICE_SET_FAN_SPEED, service_data, context=context, blocking=True + ) + + +async def async_reproduce_states( + hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None +) -> None: + """Reproduce Vacuum states.""" + # Reproduce states in parallel. + await asyncio.gather( + *(_async_reproduce_state(hass, state, context) for state in states) + ) diff --git a/homeassistant/components/vallox/__init__.py b/homeassistant/components/vallox/__init__.py index c107e4f8894..eb5edfe7fcf 100644 --- a/homeassistant/components/vallox/__init__.py +++ b/homeassistant/components/vallox/__init__.py @@ -252,7 +252,7 @@ class ValloxServiceHandler: async def async_handle(self, service): """Dispatch a service call.""" method = SERVICE_TO_METHOD.get(service.service) - params = {key: value for key, value in service.data.items()} + params = service.data.copy() if not hasattr(self, method["method"]): _LOGGER.error("Service not implemented: %s", method["method"]) diff --git a/homeassistant/components/vasttrafik/sensor.py b/homeassistant/components/vasttrafik/sensor.py index 0da730165fe..d13383a0832 100644 --- a/homeassistant/components/vasttrafik/sensor.py +++ b/homeassistant/components/vasttrafik/sensor.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +import vasttrafik import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -54,7 +55,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the departure sensor.""" - import vasttrafik planner = vasttrafik.JournyPlanner(config.get(CONF_KEY), config.get(CONF_SECRET)) sensors = [] @@ -62,7 +62,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for departure in config.get(CONF_DEPARTURES): sensors.append( VasttrafikDepartureSensor( - vasttrafik, planner, departure.get(CONF_NAME), departure.get(CONF_FROM), @@ -77,9 +76,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class VasttrafikDepartureSensor(Entity): """Implementation of a Vasttrafik Departure Sensor.""" - def __init__(self, vasttrafik, planner, name, departure, heading, lines, delay): + def __init__(self, planner, name, departure, heading, lines, delay): """Initialize the sensor.""" - self._vasttrafik = vasttrafik self._planner = planner self._name = name or departure self._departure = planner.location_name(departure)[0] @@ -119,7 +117,7 @@ class VasttrafikDepartureSensor(Entity): direction=self._heading["id"] if self._heading else None, date=now() + self._delay, ) - except self._vasttrafik.Error: + except vasttrafik.Error: _LOGGER.debug("Unable to read departure board, updating token") self._planner.update_token() diff --git a/homeassistant/components/velbus/.translations/ru.json b/homeassistant/components/velbus/.translations/ru.json index 3434c584221..10ae06ffa7c 100644 --- a/homeassistant/components/velbus/.translations/ru.json +++ b/homeassistant/components/velbus/.translations/ru.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f Velbus", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", "port": "\u0421\u0442\u0440\u043e\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f" }, "title": "Velbus" diff --git a/homeassistant/components/venstar/climate.py b/homeassistant/components/venstar/climate.py index 7be31d56c08..81afef97541 100644 --- a/homeassistant/components/venstar/climate.py +++ b/homeassistant/components/venstar/climate.py @@ -1,6 +1,7 @@ """Support for Venstar WiFi Thermostats.""" import logging +from venstarcolortouch import VenstarColorTouch import voluptuous as vol from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA @@ -71,7 +72,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Venstar thermostat.""" - import venstarcolortouch username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) @@ -84,7 +84,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): else: proto = "http" - client = venstarcolortouch.VenstarColorTouch( + client = VenstarColorTouch( addr=host, timeout=timeout, user=username, password=password, proto=proto ) diff --git a/homeassistant/components/vera/__init__.py b/homeassistant/components/vera/__init__.py index 5d9dd80061c..8fcc8a4a2fe 100644 --- a/homeassistant/components/vera/__init__.py +++ b/homeassistant/components/vera/__init__.py @@ -2,6 +2,7 @@ import logging from collections import defaultdict +import pyvera as veraApi import voluptuous as vol from requests.exceptions import RequestException @@ -65,7 +66,6 @@ VERA_COMPONENTS = [ def setup(hass, base_config): """Set up for Vera devices.""" - import pyvera as veraApi def stop_subscription(event): """Shutdown Vera subscriptions and subscription thread on exit.""" @@ -118,7 +118,6 @@ def setup(hass, base_config): def map_vera_device(vera_device, remap): """Map vera classes to Home Assistant types.""" - import pyvera as veraApi if isinstance(vera_device, veraApi.VeraDimmer): return "light" diff --git a/homeassistant/components/vera/sensor.py b/homeassistant/components/vera/sensor.py index c33187cd904..e409a123887 100644 --- a/homeassistant/components/vera/sensor.py +++ b/homeassistant/components/vera/sensor.py @@ -2,6 +2,8 @@ from datetime import timedelta import logging +import pyvera as veraApi + from homeassistant.components.sensor import ENTITY_ID_FORMAT from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.helpers.entity import Entity @@ -44,7 +46,6 @@ class VeraSensor(VeraDevice, Entity): @property def unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" - import pyvera as veraApi if self.vera_device.category == veraApi.CATEGORY_TEMPERATURE_SENSOR: return self._temperature_units @@ -59,7 +60,6 @@ class VeraSensor(VeraDevice, Entity): def update(self): """Update the state.""" - import pyvera as veraApi if self.vera_device.category == veraApi.CATEGORY_TEMPERATURE_SENSOR: self.current_value = self.vera_device.temperature diff --git a/homeassistant/components/verisure/__init__.py b/homeassistant/components/verisure/__init__.py index df95ed07ca5..f4313c7c1ac 100644 --- a/homeassistant/components/verisure/__init__.py +++ b/homeassistant/components/verisure/__init__.py @@ -3,6 +3,8 @@ import logging import threading from datetime import timedelta +from jsonpath import jsonpath +import verisure import voluptuous as vol from homeassistant.const import ( @@ -71,10 +73,8 @@ CAPTURE_IMAGE_SCHEMA = vol.Schema({vol.Required(ATTR_DEVICE_SERIAL): cv.string}) def setup(hass, config): """Set up the Verisure component.""" - import verisure - global HUB - HUB = VerisureHub(config[DOMAIN], verisure) + HUB = VerisureHub(config[DOMAIN]) HUB.update_overview = Throttle(config[DOMAIN][CONF_SCAN_INTERVAL])( HUB.update_overview ) @@ -109,13 +109,12 @@ def setup(hass, config): class VerisureHub: """A Verisure hub wrapper class.""" - def __init__(self, domain_config, verisure): + def __init__(self, domain_config): """Initialize the Verisure hub.""" self.overview = {} self.imageseries = {} self.config = domain_config - self._verisure = verisure self._lock = threading.Lock() @@ -125,15 +124,11 @@ class VerisureHub: self.giid = domain_config.get(CONF_GIID) - import jsonpath - - self.jsonpath = jsonpath.jsonpath - def login(self): """Login to Verisure.""" try: self.session.login() - except self._verisure.Error as ex: + except verisure.Error as ex: _LOGGER.error("Could not log in to verisure, %s", ex) return False if self.giid: @@ -144,7 +139,7 @@ class VerisureHub: """Logout from Verisure.""" try: self.session.logout() - except self._verisure.Error as ex: + except verisure.Error as ex: _LOGGER.error("Could not log out from verisure, %s", ex) return False return True @@ -153,7 +148,7 @@ class VerisureHub: """Set installation GIID.""" try: self.session.set_giid(self.giid) - except self._verisure.Error as ex: + except verisure.Error as ex: _LOGGER.error("Could not set installation GIID, %s", ex) return False return True @@ -162,7 +157,7 @@ class VerisureHub: """Update the overview.""" try: self.overview = self.session.get_overview() - except self._verisure.ResponseError as ex: + except verisure.ResponseError as ex: _LOGGER.error("Could not read overview, %s", ex) if ex.status_code == 503: # Service unavailable _LOGGER.info("Trying to log in again") @@ -182,7 +177,7 @@ class VerisureHub: def get(self, jpath, *args): """Get values from the overview that matches the jsonpath.""" - res = self.jsonpath(self.overview, jpath % args) + res = jsonpath(self.overview, jpath % args) return res if res else [] def get_first(self, jpath, *args): @@ -192,5 +187,5 @@ class VerisureHub: def get_image_info(self, jpath, *args): """Get values from the imageseries that matches the jsonpath.""" - res = self.jsonpath(self.imageseries, jpath % args) + res = jsonpath(self.imageseries, jpath % args) return res if res else [] diff --git a/homeassistant/components/vesync/.translations/ru.json b/homeassistant/components/vesync/.translations/ru.json index 38b86e9e29f..23cb6fdfac7 100644 --- a/homeassistant/components/vesync/.translations/ru.json +++ b/homeassistant/components/vesync/.translations/ru.json @@ -4,7 +4,7 @@ "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." }, "error": { - "invalid_login": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c" + "invalid_login": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c." }, "step": { "user": { diff --git a/homeassistant/components/vicare/climate.py b/homeassistant/components/vicare/climate.py index 7010f943707..0dcb83f758a 100644 --- a/homeassistant/components/vicare/climate.py +++ b/homeassistant/components/vicare/climate.py @@ -21,6 +21,7 @@ _LOGGER = logging.getLogger(__name__) VICARE_MODE_DHW = "dhw" VICARE_MODE_DHWANDHEATING = "dhwAndHeating" +VICARE_MODE_DHWANDHEATINGCOOLING = "dhwAndHeatingCooling" VICARE_MODE_FORCEDREDUCED = "forcedReduced" VICARE_MODE_FORCEDNORMAL = "forcedNormal" VICARE_MODE_OFF = "standby" @@ -46,6 +47,7 @@ SUPPORT_FLAGS_HEATING = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE VICARE_TO_HA_HVAC_HEATING = { VICARE_MODE_DHW: HVAC_MODE_OFF, VICARE_MODE_DHWANDHEATING: HVAC_MODE_AUTO, + VICARE_MODE_DHWANDHEATINGCOOLING: HVAC_MODE_AUTO, VICARE_MODE_FORCEDREDUCED: HVAC_MODE_OFF, VICARE_MODE_FORCEDNORMAL: HVAC_MODE_HEAT, VICARE_MODE_OFF: HVAC_MODE_OFF, @@ -200,9 +202,8 @@ class ViCareClimate(ClimateDevice): """Set new target temperatures.""" temp = kwargs.get(ATTR_TEMPERATURE) if temp is not None: - self._api.setProgramTemperature( - self._current_program, self._target_temperature - ) + self._api.setProgramTemperature(self._current_program, temp) + self._target_temperature = temp @property def preset_mode(self): diff --git a/homeassistant/components/vivotek/manifest.json b/homeassistant/components/vivotek/manifest.json index 20b2ac347f6..ff498991127 100644 --- a/homeassistant/components/vivotek/manifest.json +++ b/homeassistant/components/vivotek/manifest.json @@ -6,5 +6,7 @@ "libpyvivotek==0.2.2" ], "dependencies": [], - "codeowners": [] + "codeowners": [ + "@HarlemSquirrel" + ] } diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py index b844f94a187..f64fd2ca531 100644 --- a/homeassistant/components/vizio/media_player.py +++ b/homeassistant/components/vizio/media_player.py @@ -1,7 +1,10 @@ """Vizio SmartCast Device support.""" from datetime import timedelta import logging + import voluptuous as vol +from pyvizio import Vizio + from homeassistant import util from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA from homeassistant.components.media_player.const import ( @@ -122,7 +125,6 @@ class VizioDevice(MediaPlayerDevice): def __init__(self, host, token, name, volume_step, device_type): """Initialize Vizio device.""" - import pyvizio self._name = name self._state = None @@ -132,7 +134,7 @@ class VizioDevice(MediaPlayerDevice): self._available_inputs = None self._device_type = device_type self._supported_commands = SUPPORTED_COMMANDS[device_type] - self._device = pyvizio.Vizio(DEVICE_ID, host, DEFAULT_NAME, token, device_type) + self._device = Vizio(DEVICE_ID, host, DEFAULT_NAME, token, device_type) self._max_volume = float(self._device.get_max_volume()) @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) diff --git a/homeassistant/components/vlc/media_player.py b/homeassistant/components/vlc/media_player.py index aaef128f33d..30b316cb4e8 100644 --- a/homeassistant/components/vlc/media_player.py +++ b/homeassistant/components/vlc/media_player.py @@ -2,6 +2,7 @@ import logging import voluptuous as vol +import vlc from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA from homeassistant.components.media_player.const import ( @@ -17,6 +18,7 @@ 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" @@ -51,8 +53,6 @@ class VlcDevice(MediaPlayerDevice): def __init__(self, name, arguments): """Initialize the vlc device.""" - import vlc - self._instance = vlc.Instance(arguments) self._vlc = self._instance.media_player_new() self._name = name @@ -65,8 +65,6 @@ class VlcDevice(MediaPlayerDevice): def update(self): """Get the latest details from the device.""" - import vlc - status = self._vlc.get_state() if status == vlc.State.Playing: self._state = STATE_PLAYING diff --git a/homeassistant/components/w800rf32/__init__.py b/homeassistant/components/w800rf32/__init__.py index 9f15e0b2aa1..805cca47023 100644 --- a/homeassistant/components/w800rf32/__init__.py +++ b/homeassistant/components/w800rf32/__init__.py @@ -2,6 +2,7 @@ import logging import voluptuous as vol +import W800rf32 as w800 from homeassistant.const import ( CONF_DEVICE, @@ -26,8 +27,6 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): """Set up the w800rf32 component.""" - # Try to load the W800rf32 module. - import W800rf32 as w800 # Declare the Handle event def handle_receive(event): diff --git a/homeassistant/components/w800rf32/binary_sensor.py b/homeassistant/components/w800rf32/binary_sensor.py index f1f1890f7aa..e08111da8ba 100644 --- a/homeassistant/components/w800rf32/binary_sensor.py +++ b/homeassistant/components/w800rf32/binary_sensor.py @@ -2,6 +2,7 @@ import logging import voluptuous as vol +import W800rf32 as w800 from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA, @@ -104,9 +105,8 @@ class W800rf32BinarySensor(BinarySensorDevice): @callback def binary_sensor_update(self, event): """Call for control updates from the w800rf32 gateway.""" - import W800rf32 as w800rf32mod - if not isinstance(event, w800rf32mod.W800rf32Event): + if not isinstance(event, w800.W800rf32Event): return dev_id = event.device diff --git a/homeassistant/components/wake_on_lan/__init__.py b/homeassistant/components/wake_on_lan/__init__.py index 421f6265c0c..b4aad4925b9 100644 --- a/homeassistant/components/wake_on_lan/__init__.py +++ b/homeassistant/components/wake_on_lan/__init__.py @@ -3,6 +3,7 @@ from functools import partial import logging import voluptuous as vol +import wakeonlan from homeassistant.const import CONF_MAC import homeassistant.helpers.config_validation as cv @@ -22,7 +23,6 @@ WAKE_ON_LAN_SEND_MAGIC_PACKET_SCHEMA = vol.Schema( async def async_setup(hass, config): """Set up the wake on LAN component.""" - import wakeonlan async def send_magic_packet(call): """Send magic packet to wake up a device.""" diff --git a/homeassistant/components/wake_on_lan/switch.py b/homeassistant/components/wake_on_lan/switch.py index 453685b13f6..01f69679829 100644 --- a/homeassistant/components/wake_on_lan/switch.py +++ b/homeassistant/components/wake_on_lan/switch.py @@ -4,6 +4,7 @@ import platform import subprocess as sp import voluptuous as vol +import wakeonlan from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import CONF_HOST, CONF_NAME @@ -48,8 +49,6 @@ class WOLSwitch(SwitchDevice): def __init__(self, hass, name, host, mac_address, off_action, broadcast_address): """Initialize the WOL switch.""" - import wakeonlan - self._hass = hass self._name = name self._host = host @@ -57,7 +56,6 @@ class WOLSwitch(SwitchDevice): self._broadcast_address = broadcast_address self._off_script = Script(hass, off_action) if off_action else None self._state = False - self._wol = wakeonlan @property def is_on(self): @@ -72,11 +70,11 @@ class WOLSwitch(SwitchDevice): def turn_on(self, **kwargs): """Turn the device on.""" if self._broadcast_address: - self._wol.send_magic_packet( + wakeonlan.send_magic_packet( self._mac_address, ip_address=self._broadcast_address ) else: - self._wol.send_magic_packet(self._mac_address) + wakeonlan.send_magic_packet(self._mac_address) def turn_off(self, **kwargs): """Turn the device off if an off action is present.""" diff --git a/homeassistant/components/waqi/sensor.py b/homeassistant/components/waqi/sensor.py index dbfe6de1a60..b53723a29b6 100644 --- a/homeassistant/components/waqi/sensor.py +++ b/homeassistant/components/waqi/sensor.py @@ -5,6 +5,7 @@ from datetime import timedelta import aiohttp import voluptuous as vol +from waqiasync import WaqiClient from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv @@ -60,13 +61,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the requested World Air Quality Index locations.""" - import waqiasync token = config.get(CONF_TOKEN) station_filter = config.get(CONF_STATIONS) locations = config.get(CONF_LOCATIONS) - client = waqiasync.WaqiClient(token, async_get_clientsession(hass), timeout=TIMEOUT) + client = WaqiClient(token, async_get_clientsession(hass), timeout=TIMEOUT) dev = [] try: for location_name in locations: @@ -128,6 +128,16 @@ class WaqiSensor(Entity): return self._data.get("aqi") return None + @property + def available(self): + """Return sensor availability.""" + return self._data is not None + + @property + def unique_id(self): + """Return unique ID.""" + return self.uid + @property def unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" diff --git a/homeassistant/components/waterfurnace/__init__.py b/homeassistant/components/waterfurnace/__init__.py index acc1c22c734..b6eb22c89ae 100644 --- a/homeassistant/components/waterfurnace/__init__.py +++ b/homeassistant/components/waterfurnace/__init__.py @@ -5,6 +5,7 @@ import time import threading import voluptuous as vol +from waterfurnace.waterfurnace import WaterFurnace, WFCredentialError, WFException from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback @@ -37,19 +38,18 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, base_config): """Set up waterfurnace platform.""" - import waterfurnace.waterfurnace as wf config = base_config.get(DOMAIN) username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) - wfconn = wf.WaterFurnace(username, password) + wfconn = WaterFurnace(username, password) # NOTE(sdague): login will throw an exception if this doesn't # work, which will abort the setup. try: wfconn.login() - except wf.WFCredentialError: + except WFCredentialError: _LOGGER.error("Invalid credentials for waterfurnace login.") return False @@ -83,7 +83,6 @@ class WaterFurnaceData(threading.Thread): def _reconnect(self): """Reconnect on a failure.""" - import waterfurnace.waterfurnace as wf self._fails += 1 if self._fails > MAX_FAILS: @@ -105,7 +104,7 @@ class WaterFurnaceData(threading.Thread): try: self.client.login() self.data = self.client.read() - except wf.WFException: + except WFException: _LOGGER.exception("Failed to reconnect attempt %s", self._fails) else: _LOGGER.debug("Reconnected to furnace") @@ -113,7 +112,6 @@ class WaterFurnaceData(threading.Thread): def run(self): """Thread run loop.""" - import waterfurnace.waterfurnace as wf @callback def register(): @@ -143,7 +141,7 @@ class WaterFurnaceData(threading.Thread): try: self.data = self.client.read() - except wf.WFException: + except WFException: # WFExceptions are things the WF library understands # that pretty much can all be solved by logging in and # back out again. diff --git a/homeassistant/components/watson_iot/__init__.py b/homeassistant/components/watson_iot/__init__.py index aef2cc8ccce..adc05893fde 100644 --- a/homeassistant/components/watson_iot/__init__.py +++ b/homeassistant/components/watson_iot/__init__.py @@ -4,6 +4,8 @@ import queue import threading import time +from ibmiotf import MissingMessageEncoderException +from ibmiotf.gateway import Client import voluptuous as vol from homeassistant.const import ( @@ -67,7 +69,6 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): """Set up the Watson IoT Platform component.""" - from ibmiotf import gateway conf = config[DOMAIN] @@ -85,7 +86,7 @@ def setup(hass, config): "auth-method": "token", "auth-token": conf[CONF_TOKEN], } - watson_gateway = gateway.Client(client_args) + watson_gateway = Client(client_args) def event_to_json(event): """Add an event to the outgoing list.""" @@ -190,7 +191,6 @@ class WatsonIOTThread(threading.Thread): def write_to_watson(self, events): """Write preprocessed events to watson.""" - import ibmiotf for event in events: for retry in range(MAX_TRIES + 1): @@ -208,7 +208,7 @@ class WatsonIOTThread(threading.Thread): _LOGGER.error("Failed to publish message to Watson IoT") continue break - except (ibmiotf.MissingMessageEncoderException, IOError): + except (MissingMessageEncoderException, IOError): if retry < MAX_TRIES: time.sleep(RETRY_DELAY) else: diff --git a/homeassistant/components/waze_travel_time/sensor.py b/homeassistant/components/waze_travel_time/sensor.py index 340c0adbc97..4392a20d801 100644 --- a/homeassistant/components/waze_travel_time/sensor.py +++ b/homeassistant/components/waze_travel_time/sensor.py @@ -3,21 +3,22 @@ from datetime import timedelta import logging import re +import WazeRouteCalculator import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( ATTR_ATTRIBUTION, - CONF_NAME, - CONF_REGION, - EVENT_HOMEASSISTANT_START, ATTR_LATITUDE, ATTR_LONGITUDE, - CONF_UNIT_SYSTEM_METRIC, + CONF_NAME, + CONF_REGION, CONF_UNIT_SYSTEM_IMPERIAL, + CONF_UNIT_SYSTEM_METRIC, + EVENT_HOMEASSISTANT_START, ) -import homeassistant.helpers.config_validation as cv from homeassistant.helpers import location +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -237,7 +238,6 @@ class WazeTravelTimeData: vehicle_type, ): """Set up WazeRouteCalculator.""" - import WazeRouteCalculator self._calc = WazeRouteCalculator diff --git a/homeassistant/components/webhook/__init__.py b/homeassistant/components/webhook/__init__.py index a12e55c771a..5a41bfa9851 100644 --- a/homeassistant/components/webhook/__init__.py +++ b/homeassistant/components/webhook/__init__.py @@ -1,7 +1,7 @@ """Webhooks for Home Assistant.""" import logging -from aiohttp.web import Response +from aiohttp.web import Response, Request import voluptuous as vol from homeassistant.core import callback @@ -98,9 +98,11 @@ class WebhookView(HomeAssistantView): url = URL_WEBHOOK_PATH name = "api:webhook" requires_auth = False + cors_allowed = True - async def _handle(self, request, webhook_id): + async def _handle(self, request: Request, webhook_id): """Handle webhook call.""" + _LOGGER.debug("Handling webhook %s payload for %s", request.method, webhook_id) hass = request.app["hass"] return await async_handle_webhook(hass, webhook_id, request) diff --git a/homeassistant/components/websocket_api/auth.py b/homeassistant/components/websocket_api/auth.py index 716b20f4ca4..3971d39ee73 100644 --- a/homeassistant/components/websocket_api/auth.py +++ b/homeassistant/components/websocket_api/auth.py @@ -3,7 +3,6 @@ import voluptuous as vol from voluptuous.humanize import humanize_error from homeassistant.auth.models import RefreshToken, User -from homeassistant.auth.providers import legacy_api_password from homeassistant.components.http.ban import process_wrong_login, process_success_login from homeassistant.const import __version__ @@ -74,19 +73,6 @@ class AuthPhase: if refresh_token is not None: return await self._async_finish_auth(refresh_token.user, refresh_token) - elif self._hass.auth.support_legacy and "api_password" in msg: - self._logger.info( - "Received api_password, it is going to deprecate, please use" - " access_token instead. For instructions, see https://" - "developers.home-assistant.io/docs/en/external_api_websocket" - ".html#authentication-phase" - ) - user = await legacy_api_password.async_validate_password( - self._hass, msg["api_password"] - ) - if user is not None: - return await self._async_finish_auth(user, None) - self._send_message(auth_invalid_message("Invalid access token or password")) await process_wrong_login(self._request) raise Disconnect diff --git a/homeassistant/components/websocket_api/http.py b/homeassistant/components/websocket_api/http.py index 08a0430ee2a..be1830aa07b 100644 --- a/homeassistant/components/websocket_api/http.py +++ b/homeassistant/components/websocket_api/http.py @@ -2,6 +2,7 @@ import asyncio from contextlib import suppress import logging +from typing import Optional from aiohttp import web, WSMsgType import async_timeout @@ -25,7 +26,7 @@ from .error import Disconnect from .messages import error_message -# mypy: allow-untyped-calls, allow-untyped-defs +# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs class WebsocketAPIView(HomeAssistantView): @@ -47,7 +48,7 @@ class WebSocketHandler: """Initialize an active connection.""" self.hass = hass self.request = request - self.wsock = None + self.wsock: Optional[web.WebSocketResponse] = None self._to_write: asyncio.Queue = asyncio.Queue(maxsize=MAX_PENDING_MSG) self._handle_task = None self._writer_task = None @@ -115,7 +116,7 @@ class WebSocketHandler: # Py3.7+ if hasattr(asyncio, "current_task"): # pylint: disable=no-member - self._handle_task = asyncio.current_task() # type: ignore + self._handle_task = asyncio.current_task() else: self._handle_task = asyncio.Task.current_task() diff --git a/homeassistant/components/websocket_api/sensor.py b/homeassistant/components/websocket_api/sensor.py index 20a6a90860b..f8f1257aefc 100644 --- a/homeassistant/components/websocket_api/sensor.py +++ b/homeassistant/components/websocket_api/sensor.py @@ -10,7 +10,7 @@ from .const import ( ) -# mypy: allow-untyped-calls, allow-untyped-defs +# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): diff --git a/homeassistant/components/wemo/__init__.py b/homeassistant/components/wemo/__init__.py index 9e479991d15..df2d8ed1f31 100644 --- a/homeassistant/components/wemo/__init__.py +++ b/homeassistant/components/wemo/__init__.py @@ -1,6 +1,7 @@ """Support for WeMo device discovery.""" import logging +import pywemo import requests import voluptuous as vol @@ -87,7 +88,6 @@ def setup(hass, config): async def async_setup_entry(hass, entry): """Set up a wemo config entry.""" - import pywemo config = hass.data[DOMAIN] diff --git a/homeassistant/components/wemo/binary_sensor.py b/homeassistant/components/wemo/binary_sensor.py index 4ef18f29021..bc300fde571 100644 --- a/homeassistant/components/wemo/binary_sensor.py +++ b/homeassistant/components/wemo/binary_sensor.py @@ -3,6 +3,7 @@ import asyncio import logging import async_timeout +from pywemo import discovery import requests from homeassistant.components.binary_sensor import BinarySensorDevice @@ -15,7 +16,6 @@ _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_entities, discovery_info=None): """Register discovered WeMo binary sensors.""" - from pywemo import discovery if discovery_info is not None: location = discovery_info["ssdp_description"] diff --git a/homeassistant/components/wemo/fan.py b/homeassistant/components/wemo/fan.py index dde5aa1cd89..91273fa033f 100644 --- a/homeassistant/components/wemo/fan.py +++ b/homeassistant/components/wemo/fan.py @@ -3,11 +3,12 @@ import asyncio import logging from datetime import timedelta -import requests import async_timeout +from pywemo import discovery +import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv +import homeassistant.helpers.config_validation as cv from homeassistant.components.fan import ( DOMAIN, SUPPORT_SET_SPEED, @@ -96,7 +97,6 @@ RESET_FILTER_LIFE_SCHEMA = vol.Schema({vol.Required(ATTR_ENTITY_ID): cv.entity_i def setup_platform(hass, config, add_entities, discovery_info=None): """Set up discovered WeMo humidifiers.""" - from pywemo import discovery if DATA_KEY not in hass.data: hass.data[DATA_KEY] = {} diff --git a/homeassistant/components/wemo/light.py b/homeassistant/components/wemo/light.py index be6aa6f47f7..dab96eb8c94 100644 --- a/homeassistant/components/wemo/light.py +++ b/homeassistant/components/wemo/light.py @@ -3,8 +3,9 @@ import asyncio import logging from datetime import timedelta -import requests import async_timeout +from pywemo import discovery +import requests from homeassistant import util from homeassistant.components.light import ( @@ -35,7 +36,6 @@ SUPPORT_WEMO = ( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up discovered WeMo switches.""" - from pywemo import discovery if discovery_info is not None: location = discovery_info["ssdp_description"] diff --git a/homeassistant/components/wemo/switch.py b/homeassistant/components/wemo/switch.py index 1bc85506987..c1d07a06902 100644 --- a/homeassistant/components/wemo/switch.py +++ b/homeassistant/components/wemo/switch.py @@ -2,9 +2,10 @@ import asyncio import logging from datetime import datetime, timedelta -import requests import async_timeout +from pywemo import discovery +import requests from homeassistant.components.switch import SwitchDevice from homeassistant.exceptions import PlatformNotReady @@ -32,7 +33,6 @@ WEMO_STANDBY = 8 def setup_platform(hass, config, add_entities, discovery_info=None): """Set up discovered WeMo switches.""" - from pywemo import discovery if discovery_info is not None: location = discovery_info["ssdp_description"] diff --git a/homeassistant/components/whois/sensor.py b/homeassistant/components/whois/sensor.py index 09cf40f193f..3c78d80ba92 100644 --- a/homeassistant/components/whois/sensor.py +++ b/homeassistant/components/whois/sensor.py @@ -119,7 +119,10 @@ class WhoisSensor(Entity): attrs = {} expiration_date = response["expiration_date"] - attrs[ATTR_EXPIRES] = expiration_date.isoformat() + if isinstance(expiration_date, list): + attrs[ATTR_EXPIRES] = expiration_date[0].isoformat() + else: + attrs[ATTR_EXPIRES] = expiration_date.isoformat() if "nameservers" in response: attrs[ATTR_NAME_SERVERS] = " ".join(response["nameservers"]) diff --git a/homeassistant/components/wink/__init__.py b/homeassistant/components/wink/__init__.py index d0bb27c06e1..e2eb98938bb 100644 --- a/homeassistant/components/wink/__init__.py +++ b/homeassistant/components/wink/__init__.py @@ -5,6 +5,9 @@ import logging import os import time +from aiohttp.web import Response +import pywink +from pubnubsubhandler import PubNubSubscriptionHandler import voluptuous as vol from homeassistant.components.http import HomeAssistantView @@ -279,8 +282,6 @@ def _request_oauth_completion(hass, config): def setup(hass, config): """Set up the Wink component.""" - import pywink - from pubnubsubhandler import PubNubSubscriptionHandler if hass.data.get(DOMAIN) is None: hass.data[DOMAIN] = { @@ -689,8 +690,6 @@ class WinkAuthCallbackView(HomeAssistantView): @callback def get(self, request): """Finish OAuth callback request.""" - from aiohttp import web - hass = request.app["hass"] data = request.query @@ -715,15 +714,13 @@ class WinkAuthCallbackView(HomeAssistantView): hass.async_add_job(setup, hass, self.config) - return web.Response( + return Response( text=html_response.format(response_message), content_type="text/html" ) error_msg = "No code returned from Wink API" _LOGGER.error(error_msg) - return web.Response( - text=html_response.format(error_msg), content_type="text/html" - ) + return Response(text=html_response.format(error_msg), content_type="text/html") class WinkDevice(Entity): diff --git a/homeassistant/components/wink/alarm_control_panel.py b/homeassistant/components/wink/alarm_control_panel.py index 4708b6efee8..654252f5ffe 100644 --- a/homeassistant/components/wink/alarm_control_panel.py +++ b/homeassistant/components/wink/alarm_control_panel.py @@ -1,6 +1,8 @@ """Support Wink alarm control panels.""" import logging +import pywink + import homeassistant.components.alarm_control_panel as alarm from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, @@ -17,7 +19,6 @@ STATE_ALARM_PRIVACY = "Private" def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Wink platform.""" - import pywink for camera in pywink.get_cameras(): # get_cameras returns multiple device types. diff --git a/homeassistant/components/wink/binary_sensor.py b/homeassistant/components/wink/binary_sensor.py index e82a767fde8..6dd22a3f7b8 100644 --- a/homeassistant/components/wink/binary_sensor.py +++ b/homeassistant/components/wink/binary_sensor.py @@ -1,6 +1,8 @@ """Support for Wink binary sensors.""" import logging +import pywink + from homeassistant.components.binary_sensor import BinarySensorDevice from . import DOMAIN, WinkDevice @@ -26,7 +28,6 @@ SENSOR_TYPES = { def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Wink binary sensor platform.""" - import pywink for sensor in pywink.get_sensors(): _id = sensor.object_id() + sensor.name() diff --git a/homeassistant/components/wink/climate.py b/homeassistant/components/wink/climate.py index 38f25ef0a83..6323fa7bbfe 100644 --- a/homeassistant/components/wink/climate.py +++ b/homeassistant/components/wink/climate.py @@ -283,10 +283,6 @@ class WinkThermostat(WinkDevice, ClimateDevice): target_temp_high = target_temp if self.hvac_mode == HVAC_MODE_HEAT: target_temp_low = target_temp - if target_temp_low is not None: - target_temp_low = target_temp_low - if target_temp_high is not None: - target_temp_high = target_temp_high self.wink.set_temperature(target_temp_low, target_temp_high) def set_hvac_mode(self, hvac_mode): diff --git a/homeassistant/components/wink/cover.py b/homeassistant/components/wink/cover.py index fa39909512a..1ce7f9b8875 100644 --- a/homeassistant/components/wink/cover.py +++ b/homeassistant/components/wink/cover.py @@ -1,4 +1,6 @@ """Support for Wink covers.""" +import pywink + from homeassistant.components.cover import ATTR_POSITION, CoverDevice from . import DOMAIN, WinkDevice @@ -6,7 +8,6 @@ from . import DOMAIN, WinkDevice def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Wink cover platform.""" - import pywink for shade in pywink.get_shades(): _id = shade.object_id() + shade.name() diff --git a/homeassistant/components/wink/fan.py b/homeassistant/components/wink/fan.py index 9f5f2f9b3a0..d1d4e30ada3 100644 --- a/homeassistant/components/wink/fan.py +++ b/homeassistant/components/wink/fan.py @@ -1,6 +1,8 @@ """Support for Wink fans.""" import logging +import pywink + from homeassistant.components.fan import ( SPEED_HIGH, SPEED_LOW, @@ -21,7 +23,6 @@ SUPPORTED_FEATURES = SUPPORT_DIRECTION + SUPPORT_SET_SPEED def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Wink platform.""" - import pywink for fan in pywink.get_fans(): if fan.object_id() + fan.name() not in hass.data[DOMAIN]["unique_ids"]: diff --git a/homeassistant/components/wink/light.py b/homeassistant/components/wink/light.py index 76576f804fa..bd125e6a7c2 100644 --- a/homeassistant/components/wink/light.py +++ b/homeassistant/components/wink/light.py @@ -1,4 +1,6 @@ """Support for Wink lights.""" +import pywink + from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, @@ -18,7 +20,6 @@ from . import DOMAIN, WinkDevice def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Wink lights.""" - import pywink for light in pywink.get_light_bulbs(): _id = light.object_id() + light.name() diff --git a/homeassistant/components/wink/lock.py b/homeassistant/components/wink/lock.py index 5246fb49eed..37b27c0d500 100644 --- a/homeassistant/components/wink/lock.py +++ b/homeassistant/components/wink/lock.py @@ -1,6 +1,7 @@ """Support for Wink locks.""" import logging +import pywink import voluptuous as vol from homeassistant.components.lock import LockDevice @@ -70,7 +71,6 @@ ADD_KEY_SCHEMA = vol.Schema( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Wink platform.""" - import pywink for lock in pywink.get_locks(): _id = lock.object_id() + lock.name() diff --git a/homeassistant/components/wink/scene.py b/homeassistant/components/wink/scene.py index a00600ad784..ff083598b2e 100644 --- a/homeassistant/components/wink/scene.py +++ b/homeassistant/components/wink/scene.py @@ -1,6 +1,8 @@ """Support for Wink scenes.""" import logging +import pywink + from homeassistant.components.scene import Scene from . import DOMAIN, WinkDevice @@ -10,7 +12,6 @@ _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Wink platform.""" - import pywink for scene in pywink.get_scenes(): _id = scene.object_id() + scene.name() diff --git a/homeassistant/components/wink/sensor.py b/homeassistant/components/wink/sensor.py index 030a1e5b9ec..2d0313ec211 100644 --- a/homeassistant/components/wink/sensor.py +++ b/homeassistant/components/wink/sensor.py @@ -1,6 +1,8 @@ """Support for Wink sensors.""" import logging +import pywink + from homeassistant.const import TEMP_CELSIUS from . import DOMAIN, WinkDevice @@ -12,7 +14,6 @@ SENSOR_TYPES = ["temperature", "humidity", "balance", "proximity"] def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Wink platform.""" - import pywink for sensor in pywink.get_sensors(): _id = sensor.object_id() + sensor.name() diff --git a/homeassistant/components/wink/switch.py b/homeassistant/components/wink/switch.py index 07d3ff4becc..cf2264e7eeb 100644 --- a/homeassistant/components/wink/switch.py +++ b/homeassistant/components/wink/switch.py @@ -1,6 +1,8 @@ """Support for Wink switches.""" import logging +import pywink + from homeassistant.helpers.entity import ToggleEntity from . import DOMAIN, WinkDevice @@ -10,7 +12,6 @@ _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Wink platform.""" - import pywink for switch in pywink.get_switches(): _id = switch.object_id() + switch.name() diff --git a/homeassistant/components/wink/water_heater.py b/homeassistant/components/wink/water_heater.py index 4fceeeb313d..11330c7c9a5 100644 --- a/homeassistant/components/wink/water_heater.py +++ b/homeassistant/components/wink/water_heater.py @@ -1,6 +1,8 @@ """Support for Wink water heaters.""" import logging +import pywink + from homeassistant.components.water_heater import ( ATTR_TEMPERATURE, STATE_ECO, @@ -42,7 +44,6 @@ WINK_STATE_TO_HA = {value: key for key, value in HA_STATE_TO_WINK.items()} def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Wink water heater devices.""" - import pywink for water_heater in pywink.get_water_heaters(): _id = water_heater.object_id() + water_heater.name() diff --git a/homeassistant/components/withings/.translations/de.json b/homeassistant/components/withings/.translations/de.json index 15b6f4e3b01..dabf184d7ed 100644 --- a/homeassistant/components/withings/.translations/de.json +++ b/homeassistant/components/withings/.translations/de.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_flows": "Withings muss konfiguriert werden, bevor die Integration authentifiziert werden kann. Bitte lies die Dokumentation." + }, "create_entry": { "default": "Erfolgreiche Authentifizierung mit Withings f\u00fcr das ausgew\u00e4hlte Profil." }, diff --git a/homeassistant/components/withings/__init__.py b/homeassistant/components/withings/__init__.py index ecefa681b87..baed9300d46 100644 --- a/homeassistant/components/withings/__init__.py +++ b/homeassistant/components/withings/__init__.py @@ -92,8 +92,4 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): """Unload Withings config entry.""" - await hass.async_create_task( - hass.config_entries.async_forward_entry_unload(entry, "sensor") - ) - - return True + return await hass.config_entries.async_forward_entry_unload(entry, "sensor") diff --git a/homeassistant/components/withings/sensor.py b/homeassistant/components/withings/sensor.py index 67cf966c1bc..0293784fd3e 100644 --- a/homeassistant/components/withings/sensor.py +++ b/homeassistant/components/withings/sensor.py @@ -397,7 +397,7 @@ class WithingsHealthSensor(Entity): ] if not measure_groups: - _LOGGER.warning("No measure groups found, setting state to %s", None) + _LOGGER.debug("No measure groups found, setting state to %s", None) self._state = None return @@ -417,7 +417,7 @@ class WithingsHealthSensor(Entity): return if not data.series: - _LOGGER.warning("No sleep data, setting state to %s", None) + _LOGGER.debug("No sleep data, setting state to %s", None) self._state = None return @@ -444,7 +444,7 @@ class WithingsHealthSensor(Entity): return if not data.series: - _LOGGER.warning("Sleep data has no series, setting state to %s", None) + _LOGGER.debug("Sleep data has no series, setting state to %s", None) self._state = None return diff --git a/homeassistant/components/workday/binary_sensor.py b/homeassistant/components/workday/binary_sensor.py index 19edf231624..3ca2afcc749 100644 --- a/homeassistant/components/workday/binary_sensor.py +++ b/homeassistant/components/workday/binary_sensor.py @@ -2,6 +2,7 @@ import logging from datetime import datetime, timedelta +import holidays import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -141,8 +142,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Workday sensor.""" - import holidays - sensor_name = config.get(CONF_NAME) country = config.get(CONF_COUNTRY) province = config.get(CONF_PROVINCE) diff --git a/homeassistant/components/wunderlist/__init__.py b/homeassistant/components/wunderlist/__init__.py index ce044499c63..122d09feaa4 100644 --- a/homeassistant/components/wunderlist/__init__.py +++ b/homeassistant/components/wunderlist/__init__.py @@ -2,6 +2,7 @@ import logging import voluptuous as vol +from wunderpy2 import WunderApi import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_NAME, CONF_ACCESS_TOKEN @@ -59,9 +60,7 @@ class Wunderlist: def __init__(self, access_token, client_id): """Create new instance of Wunderlist component.""" - import wunderpy2 - - api = wunderpy2.WunderApi() + api = WunderApi() self._client = api.get_client(access_token, client_id) _LOGGER.debug("Instance created") diff --git a/homeassistant/components/wwlln/.translations/pt.json b/homeassistant/components/wwlln/.translations/pt.json new file mode 100644 index 00000000000..c7081cd694a --- /dev/null +++ b/homeassistant/components/wwlln/.translations/pt.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "latitude": "Latitude", + "longitude": "Longitude" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index 67dc12565d8..acac60e108a 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -43,7 +43,8 @@ MODEL_AIRPURIFIER_SA2 = "zhimi.airpurifier.sa2" MODEL_AIRPURIFIER_2S = "zhimi.airpurifier.mc1" MODEL_AIRHUMIDIFIER_V1 = "zhimi.humidifier.v1" -MODEL_AIRHUMIDIFIER_CA = "zhimi.humidifier.ca1" +MODEL_AIRHUMIDIFIER_CA1 = "zhimi.humidifier.ca1" +MODEL_AIRHUMIDIFIER_CB1 = "zhimi.humidifier.cb1" MODEL_AIRFRESH_VA2 = "zhimi.airfresh.va2" @@ -68,7 +69,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( MODEL_AIRPURIFIER_SA2, MODEL_AIRPURIFIER_2S, MODEL_AIRHUMIDIFIER_V1, - MODEL_AIRHUMIDIFIER_CA, + MODEL_AIRHUMIDIFIER_CA1, + MODEL_AIRHUMIDIFIER_CB1, MODEL_AIRFRESH_VA2, ] ), @@ -235,7 +237,7 @@ AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER = { ATTR_BUTTON_PRESSED: "button_pressed", } -AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA = { +AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA_AND_CB = { **AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_COMMON, ATTR_MOTOR_SPEED: "motor_speed", ATTR_DEPTH: "depth", @@ -335,7 +337,7 @@ FEATURE_FLAGS_AIRHUMIDIFIER = ( | FEATURE_SET_TARGET_HUMIDITY ) -FEATURE_FLAGS_AIRHUMIDIFIER_CA = FEATURE_FLAGS_AIRHUMIDIFIER | FEATURE_SET_DRY +FEATURE_FLAGS_AIRHUMIDIFIER_CA_AND_CB = FEATURE_FLAGS_AIRHUMIDIFIER | FEATURE_SET_DRY FEATURE_FLAGS_AIRFRESH = ( FEATURE_SET_BUZZER @@ -880,9 +882,9 @@ class XiaomiAirHumidifier(XiaomiGenericDevice): super().__init__(name, device, model, unique_id) - if self._model == MODEL_AIRHUMIDIFIER_CA: - self._device_features = FEATURE_FLAGS_AIRHUMIDIFIER_CA - self._available_attributes = AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA + if self._model in [MODEL_AIRHUMIDIFIER_CA1, MODEL_AIRHUMIDIFIER_CB1]: + self._device_features = FEATURE_FLAGS_AIRHUMIDIFIER_CA_AND_CB + self._available_attributes = AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA_AND_CB self._speed_list = [ mode.name for mode in OperationMode if mode is not OperationMode.Strong ] diff --git a/homeassistant/components/xiaomi_miio/manifest.json b/homeassistant/components/xiaomi_miio/manifest.json index 4c01cce2d3c..b675e6e6746 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.5" + "python-miio==0.4.6" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/xmpp/notify.py b/homeassistant/components/xmpp/notify.py index 3719113f7c9..5aa9dbfffd1 100644 --- a/homeassistant/components/xmpp/notify.py +++ b/homeassistant/components/xmpp/notify.py @@ -7,6 +7,14 @@ import random 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, +) import voluptuous as vol from homeassistant.const import ( @@ -118,14 +126,6 @@ async def async_send_message( data=None, ): """Send a message over XMPP.""" - 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, - ) class SendNotificationBot(slixmpp.ClientXMPP): """Service for sending Jabber (XMPP) messages.""" diff --git a/homeassistant/components/yamaha/media_player.py b/homeassistant/components/yamaha/media_player.py index e699ab74e68..eabb1ef34f1 100644 --- a/homeassistant/components/yamaha/media_player.py +++ b/homeassistant/components/yamaha/media_player.py @@ -2,6 +2,7 @@ import logging import requests +import rxv import voluptuous as vol from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA @@ -82,7 +83,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Yamaha platform.""" - import rxv # Keep track of configured receivers so that we don't end up # discovering a receiver dynamically that we have static config @@ -336,8 +336,6 @@ class YamahaDevice(MediaPlayerDevice): self._call_playback_function(self.receiver.next, "next track") def _call_playback_function(self, function, function_text): - import rxv - try: function() except rxv.exceptions.ResponseException: diff --git a/homeassistant/components/yamaha_musiccast/media_player.py b/homeassistant/components/yamaha_musiccast/media_player.py index 38e606a0962..18b80cc4085 100644 --- a/homeassistant/components/yamaha_musiccast/media_player.py +++ b/homeassistant/components/yamaha_musiccast/media_player.py @@ -1,6 +1,8 @@ """Support for Yamaha MusicCast Receivers.""" import logging +import socket +import pymusiccast import voluptuous as vol from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA @@ -61,8 +63,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Yamaha MusicCast platform.""" - import socket - import pymusiccast known_hosts = hass.data.get(KNOWN_HOSTS_KEY) if known_hosts is None: diff --git a/homeassistant/components/yandex_transport/manifest.json b/homeassistant/components/yandex_transport/manifest.json index 91267b43480..44dcf5b100c 100644 --- a/homeassistant/components/yandex_transport/manifest.json +++ b/homeassistant/components/yandex_transport/manifest.json @@ -3,7 +3,7 @@ "name": "Yandex Transport", "documentation": "https://www.home-assistant.io/integrations/yandex_transport", "requirements": [ - "ya_ma==0.3.7" + "ya_ma==0.3.8" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/yandex_transport/sensor.py b/homeassistant/components/yandex_transport/sensor.py index 26311a4c72e..4bf634a61f4 100644 --- a/homeassistant/components/yandex_transport/sensor.py +++ b/homeassistant/components/yandex_transport/sensor.py @@ -79,18 +79,21 @@ class DiscoverMoscowYandexTransport(Entity): transport_list = stop_metadata["Transport"] for transport in transport_list: route = transport["name"] - if self._routes and route not in self._routes: - # skip unnecessary route info - continue - if "Events" in transport["BriefSchedule"]: - for event in transport["BriefSchedule"]["Events"]: - if "Estimated" in event: - posix_time_next = int(event["Estimated"]["value"]) - if closer_time is None or closer_time > posix_time_next: - closer_time = posix_time_next - if route not in attrs: - attrs[route] = [] - attrs[route].append(event["Estimated"]["text"]) + for thread in transport["threads"]: + if self._routes and route not in self._routes: + # skip unnecessary route info + continue + if "Events" not in thread["BriefSchedule"]: + continue + for event in thread["BriefSchedule"]["Events"]: + if "Estimated" not in event: + continue + posix_time_next = int(event["Estimated"]["value"]) + if closer_time is None or closer_time > posix_time_next: + closer_time = posix_time_next + if route not in attrs: + attrs[route] = [] + attrs[route].append(event["Estimated"]["text"]) attrs[STOP_NAME] = stop_name attrs[ATTR_ATTRIBUTION] = ATTRIBUTION if closer_time is None: diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index ab63e6fb319..772fb00977b 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -2,8 +2,16 @@ import logging import voluptuous as vol -from yeelight import RGBTransition, SleepTransition, Flow, BulbException +import yeelight +from yeelight import ( + RGBTransition, + SleepTransition, + Flow, + BulbException, + transitions as yee_transitions, +) from yeelight.enums import PowerMode, LightType, BulbType, SceneClass + from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.service import extract_entity_ids import homeassistant.helpers.config_validation as cv @@ -190,8 +198,6 @@ SERVICE_SCHEMA_SET_AUTO_DELAY_OFF = YEELIGHT_SERVICE_SCHEMA.extend( def _transitions_config_parser(transitions): """Parse transitions config into initialized objects.""" - import yeelight - transition_objects = [] for transition_config in transitions: transition, params = list(transition_config.items())[0] @@ -652,39 +658,23 @@ class YeelightGenericLight(Light): def set_effect(self, effect) -> None: """Activate effect.""" if effect: - from yeelight.transitions import ( - disco, - temp, - strobe, - pulse, - strobe_color, - alarm, - police, - police2, - christmas, - rgb, - randomloop, - lsd, - slowdown, - ) - if effect == EFFECT_STOP: self._bulb.stop_flow(light_type=self.light_type) return effects_map = { - EFFECT_DISCO: disco, - EFFECT_TEMP: temp, - EFFECT_STROBE: strobe, - EFFECT_STROBE_COLOR: strobe_color, - EFFECT_ALARM: alarm, - EFFECT_POLICE: police, - EFFECT_POLICE2: police2, - EFFECT_CHRISTMAS: christmas, - EFFECT_RGB: rgb, - EFFECT_RANDOM_LOOP: randomloop, - EFFECT_LSD: lsd, - EFFECT_SLOWDOWN: slowdown, + 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 in self.custom_effects_names: @@ -692,13 +682,15 @@ class YeelightGenericLight(Light): elif effect in effects_map: flow = Flow(count=0, transitions=effects_map[effect]()) elif effect == EFFECT_FAST_RANDOM_LOOP: - flow = Flow(count=0, transitions=randomloop(duration=250)) + flow = Flow( + count=0, transitions=yee_transitions.randomloop(duration=250) + ) elif effect == EFFECT_WHATSAPP: - flow = Flow(count=2, transitions=pulse(37, 211, 102)) + flow = Flow(count=2, transitions=yee_transitions.pulse(37, 211, 102)) elif effect == EFFECT_FACEBOOK: - flow = Flow(count=2, transitions=pulse(59, 89, 152)) + flow = Flow(count=2, transitions=yee_transitions.pulse(59, 89, 152)) elif effect == EFFECT_TWITTER: - flow = Flow(count=2, transitions=pulse(0, 172, 237)) + flow = Flow(count=2, transitions=yee_transitions.pulse(0, 172, 237)) try: self._bulb.start_flow(flow, light_type=self.light_type) diff --git a/homeassistant/components/yeelightsunflower/light.py b/homeassistant/components/yeelightsunflower/light.py index fa836f2776f..3424014e8f4 100644 --- a/homeassistant/components/yeelightsunflower/light.py +++ b/homeassistant/components/yeelightsunflower/light.py @@ -1,6 +1,7 @@ """Support for Yeelight Sunflower color bulbs (not Yeelight Blue or WiFi).""" import logging +import yeelightsunflower import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -24,7 +25,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 Yeelight Sunflower Light platform.""" - import yeelightsunflower host = config.get(CONF_HOST) hub = yeelightsunflower.Hub(host) diff --git a/homeassistant/components/yr/sensor.py b/homeassistant/components/yr/sensor.py index 3d8c63621be..f562f519ab5 100644 --- a/homeassistant/components/yr/sensor.py +++ b/homeassistant/components/yr/sensor.py @@ -7,6 +7,7 @@ from xml.parsers.expat import ExpatError import aiohttp import async_timeout +import xmltodict import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -155,7 +156,6 @@ class YrData: async def fetching_data(self, *_): """Get the latest data from yr.no.""" - import xmltodict def try_again(err: str): """Retry in 15 to 20 minutes.""" diff --git a/homeassistant/components/yweather/sensor.py b/homeassistant/components/yweather/sensor.py index 4dc23699872..c7f752a8836 100644 --- a/homeassistant/components/yweather/sensor.py +++ b/homeassistant/components/yweather/sensor.py @@ -1,17 +1,23 @@ """Support for the Yahoo! Weather service.""" -import logging from datetime import timedelta +import logging import voluptuous as vol +from yahooweather import ( # pylint: disable=import-error + UNIT_C, + UNIT_F, + YahooWeather, + get_woeid, +) -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - TEMP_CELSIUS, + ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS, CONF_NAME, - ATTR_ATTRIBUTION, + TEMP_CELSIUS, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -20,6 +26,7 @@ _LOGGER = logging.getLogger(__name__) ATTRIBUTION = "Weather details provided by Yahoo! Inc." CONF_FORECAST = "forecast" + CONF_WOEID = "woeid" DEFAULT_NAME = "Yweather" @@ -52,8 +59,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Yahoo! weather sensor.""" - from yahooweather import get_woeid, UNIT_C, UNIT_F - unit = hass.config.units.temperature_unit woeid = config.get(CONF_WOEID) forecast = config.get(CONF_FORECAST) @@ -181,8 +186,6 @@ class YahooWeatherData: def __init__(self, woeid, temp_unit): """Initialize the data object.""" - from yahooweather import YahooWeather - self._yahoo = YahooWeather(woeid, temp_unit) @property diff --git a/homeassistant/components/yweather/weather.py b/homeassistant/components/yweather/weather.py index 6779fd1896d..202124aa340 100644 --- a/homeassistant/components/yweather/weather.py +++ b/homeassistant/components/yweather/weather.py @@ -3,6 +3,12 @@ from datetime import timedelta import logging import voluptuous as vol +from yahooweather import ( # pylint: disable=import-error + UNIT_C, + UNIT_F, + YahooWeather, + get_woeid, +) from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, @@ -21,7 +27,6 @@ DATA_CONDITION = "yahoo_condition" ATTRIBUTION = "Weather details provided by Yahoo! Inc." - CONF_WOEID = "woeid" DEFAULT_NAME = "Yweather" @@ -46,7 +51,6 @@ CONDITION_CLASSES = { "exceptional": [0, 1, 2, 19, 22], } - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional(CONF_WOEID): cv.string, @@ -57,8 +61,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Yahoo! weather platform.""" - from yahooweather import get_woeid, UNIT_C, UNIT_F - unit = hass.config.units.temperature_unit woeid = config.get(CONF_WOEID) name = config.get(CONF_NAME) @@ -181,8 +183,6 @@ class YahooWeatherData: def __init__(self, woeid, temp_unit): """Initialize the data object.""" - from yahooweather import YahooWeather - self._yahoo = YahooWeather(woeid, temp_unit) @property diff --git a/homeassistant/components/zengge/light.py b/homeassistant/components/zengge/light.py index a75cbba5f42..d890b193d72 100644 --- a/homeassistant/components/zengge/light.py +++ b/homeassistant/components/zengge/light.py @@ -1,6 +1,7 @@ """Support for Zengge lights.""" import logging +from zengge import zengge import voluptuous as vol from homeassistant.const import CONF_DEVICES, CONF_NAME @@ -47,12 +48,11 @@ class ZenggeLight(Light): def __init__(self, device): """Initialize the light.""" - import zengge self._name = device["name"] self._address = device["address"] self.is_valid = True - self._bulb = zengge.zengge(self._address) + self._bulb = zengge(self._address) self._white = 0 self._brightness = 0 self._hs_color = (0, 0) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index af107a6ae0d..2f9fb7b4580 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -11,7 +11,11 @@ import voluptuous as vol from zeroconf import ServiceBrowser, ServiceInfo, ServiceStateChange, Zeroconf from homeassistant import util -from homeassistant.const import EVENT_HOMEASSISTANT_STOP, __version__ +from homeassistant.const import ( + EVENT_HOMEASSISTANT_STOP, + EVENT_HOMEASSISTANT_START, + __version__, +) from homeassistant.generated.zeroconf import ZEROCONF, HOMEKIT _LOGGER = logging.getLogger(__name__) @@ -33,6 +37,7 @@ CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) def setup(hass, config): """Set up Zeroconf and make Home Assistant discoverable.""" + zeroconf = Zeroconf() zeroconf_name = f"{hass.config.location_name}.{ZEROCONF_TYPE}" params = { @@ -58,9 +63,15 @@ def setup(hass, config): properties=params, ) - zeroconf = Zeroconf() + def zeroconf_hass_start(_event): + """Expose Home Assistant on zeroconf when it starts. - zeroconf.register_service(info) + Wait till started or otherwise HTTP is not up and running. + """ + _LOGGER.info("Starting Zeroconf broadcast") + zeroconf.register_service(info) + + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, zeroconf_hass_start) def service_update(zeroconf, service_type, name, state_change): """Service state changed.""" diff --git a/homeassistant/components/zestimate/sensor.py b/homeassistant/components/zestimate/sensor.py index 703e3bf25a0..4b8bdf5fa2e 100644 --- a/homeassistant/components/zestimate/sensor.py +++ b/homeassistant/components/zestimate/sensor.py @@ -3,6 +3,7 @@ from datetime import timedelta import logging import requests +import xmltodict import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -101,7 +102,6 @@ class ZestimateDataSensor(Entity): def update(self): """Get the latest data and update the states.""" - import xmltodict try: response = requests.get(_RESOURCE, params=self.params, timeout=5) diff --git a/homeassistant/components/zha/.translations/de.json b/homeassistant/components/zha/.translations/de.json index 9ffd5211a1f..3329eafa1c6 100644 --- a/homeassistant/components/zha/.translations/de.json +++ b/homeassistant/components/zha/.translations/de.json @@ -18,11 +18,50 @@ "title": "ZHA" }, "device_automation": { + "action_type": { + "squawk": "Kreischen", + "warn": "Warnen" + }, "trigger_subtype": { + "both_buttons": "Beide Tasten", + "button_1": "Erste Taste", + "button_2": "Zweite Taste", + "button_3": "Dritte Taste", + "button_4": "Vierte Taste", + "button_5": "F\u00fcnfte Taste", + "button_6": "Sechste Taste", "close": "Schlie\u00dfen", + "dim_down": "Dimmer runter", + "dim_up": "Dimmer hoch", + "face_1": "mit Fl\u00e4che 1 aktiviert", + "face_2": "mit Fl\u00e4che 2 aktiviert", + "face_3": "mit Fl\u00e4che 3 aktiviert", + "face_4": "mit Fl\u00e4che 4 aktiviert", + "face_5": "mit Fl\u00e4che 5 aktiviert", + "face_6": "mit Fl\u00e4che 6 aktiviert", + "face_any": "Mit einer beliebigen/festgelegten Fl\u00e4che(n) aktiviert", "left": "Links", "open": "Offen", - "right": "Rechts" + "right": "Rechts", + "turn_off": "Ausschalten", + "turn_on": "Einschalten" + }, + "trigger_type": { + "device_dropped": "Ger\u00e4t ist gefallen", + "device_flipped": "Ger\u00e4t umgedreht \"{subtype}\"", + "device_knocked": "Ger\u00e4t klopfte \"{subtype}\"", + "device_rotated": "Ger\u00e4t wurde gedreht \"{subtype}\"", + "device_shaken": "Ger\u00e4t ersch\u00fcttert", + "device_slid": "Ger\u00e4t gerutscht \"{subtype}\"", + "device_tilted": "Ger\u00e4t gekippt", + "remote_button_double_press": "\"{subtype}\" Taste doppelt angeklickt", + "remote_button_long_press": "\"{subtype}\" Taste kontinuierlich gedr\u00fcckt", + "remote_button_long_release": "\"{subtype}\" Taste nach langem Dr\u00fccken losgelassen", + "remote_button_quadruple_press": "\"{subtype}\" Taste vierfach geklickt", + "remote_button_quintuple_press": "\"{subtype}\" Taste f\u00fcnffach geklickt", + "remote_button_short_press": "\"{subtype}\" Taste gedr\u00fcckt", + "remote_button_short_release": "\"{subtype}\" Taste losgelassen", + "remote_button_triple_press": "\"{subtype}\" Taste dreimal geklickt" } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/fr.json b/homeassistant/components/zha/.translations/fr.json index f8b78af5721..9b1ba025d7c 100644 --- a/homeassistant/components/zha/.translations/fr.json +++ b/homeassistant/components/zha/.translations/fr.json @@ -31,6 +31,15 @@ "button_5": "Cinqui\u00e8me bouton", "button_6": "Sixi\u00e8me bouton", "close": "Fermer", + "dim_down": "Assombrir", + "dim_up": "\u00c9claircir", + "face_1": "avec face 1 activ\u00e9e", + "face_2": "avec face 2 activ\u00e9e", + "face_3": "avec face 3 activ\u00e9e", + "face_4": "avec face 4 activ\u00e9e", + "face_5": "avec face 5 activ\u00e9e", + "face_6": "avec face 6 activ\u00e9e", + "face_any": "Avec n'importe quelle face / face sp\u00e9cifi\u00e9e(s) activ\u00e9e", "left": "Gauche", "open": "Ouvert", "right": "Droite", @@ -38,10 +47,21 @@ "turn_on": "Allumer" }, "trigger_type": { + "device_dropped": "Appareil tomb\u00e9", + "device_flipped": "Appareil retourn\u00e9 \"{subtype}\"", + "device_knocked": "Appareil frapp\u00e9 \"{subtype}\"", + "device_rotated": "Appareil tourn\u00e9 \"{subtype}\"", "device_shaken": "Appareil secou\u00e9", + "device_slid": "Appareil gliss\u00e9 \"{subtype}\"", "device_tilted": "Dispositif inclin\u00e9", + "remote_button_double_press": "Bouton \"{subtype}\" double cliqu\u00e9", + "remote_button_long_press": "Bouton \"{subtype}\" appuy\u00e9 continuellement", + "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_release": "Bouton \" {subtype} \" est rel\u00e2ch\u00e9", - "remote_button_triple_press": "Bouton\"{sous-type}\" \u00e0 trois clics" + "remote_button_triple_press": "Bouton \"{subtype}\" \u00e0 trois clics" } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/ko.json b/homeassistant/components/zha/.translations/ko.json index 44f45f43570..3a62f5d7ebe 100644 --- a/homeassistant/components/zha/.translations/ko.json +++ b/homeassistant/components/zha/.translations/ko.json @@ -16,5 +16,52 @@ } }, "title": "ZHA" + }, + "device_automation": { + "action_type": { + "squawk": "\ube44\uc0c1", + "warn": "\uacbd\uace0" + }, + "trigger_subtype": { + "both_buttons": "\ub450 \uac1c", + "button_1": "\uccab \ubc88\uc9f8", + "button_2": "\ub450 \ubc88\uc9f8", + "button_3": "\uc138 \ubc88\uc9f8", + "button_4": "\ub124 \ubc88\uc9f8", + "button_5": "\ub2e4\uc12f \ubc88\uc9f8", + "button_6": "\uc5ec\uc12f \ubc88\uc9f8", + "close": "\ub2eb\uae30", + "dim_down": "\uc5b4\ub461\uac8c \ud558\uae30", + "dim_up": "\ubc1d\uac8c \ud558\uae30", + "face_1": "\uba74 1\uc744 \ud65c\uc131\ud654 \ud55c \ucc44\ub85c", + "face_2": "\uba74 2\ub97c \ud65c\uc131\ud654 \ud55c \ucc44\ub85c", + "face_3": "\uba74 3\uc744 \ud65c\uc131\ud654 \ud55c \ucc44\ub85c", + "face_4": "\uba74 4\ub97c \ud65c\uc131\ud654 \ud55c \ucc44\ub85c", + "face_5": "\uba74 5\ub97c \ud65c\uc131\ud654 \ud55c \ucc44\ub85c", + "face_6": "\uba74 6\uc744 \ud65c\uc131\ud654 \ud55c \ucc44\ub85c", + "face_any": "\uc784\uc758\uc758 \uba74 \ub610\ub294 \ud2b9\uc815 \uba74\uc744 \ud65c\uc131\ud654 \ud55c \ucc44\ub85c", + "left": "\uc67c\ucabd", + "open": "\uc5f4\uae30", + "right": "\uc624\ub978\ucabd", + "turn_off": "\ub044\uae30", + "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" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/nl.json b/homeassistant/components/zha/.translations/nl.json index 5e5c666b1a4..fc7ae970503 100644 --- a/homeassistant/components/zha/.translations/nl.json +++ b/homeassistant/components/zha/.translations/nl.json @@ -18,7 +18,28 @@ "title": "ZHA" }, "device_automation": { + "action_type": { + "squawk": "Schreeuw", + "warn": "Waarschuwen" + }, "trigger_subtype": { + "both_buttons": "Beide knoppen", + "button_1": "Eerste knop", + "button_2": "Tweede knop", + "button_3": "Derde knop", + "button_4": "Vierde knop", + "button_5": "Vijfde knop", + "button_6": "Zesde knop", + "close": "Sluiten", + "dim_down": "Dim omlaag", + "dim_up": "Dim omhoog", + "face_1": "met gezicht 1 geactiveerd", + "face_2": "met gezicht 2 geactiveerd", + "face_3": "met gezicht 3 geactiveerd", + "face_4": "met gezicht 4 geactiveerd", + "face_5": "met gezicht 5 geactiveerd", + "face_6": "met gezicht 6 geactiveerd", + "face_any": "Met elk/opgegeven gezicht (en) geactiveerd", "left": "Links", "open": "Open", "right": "Rechts", diff --git a/homeassistant/components/zha/.translations/no.json b/homeassistant/components/zha/.translations/no.json index 18c4c3c9ff2..a70f5ad1c33 100644 --- a/homeassistant/components/zha/.translations/no.json +++ b/homeassistant/components/zha/.translations/no.json @@ -19,8 +19,8 @@ }, "device_automation": { "action_type": { - "squawk": "Squawk", - "warn": "Advarer" + "squawk": "Varsle", + "warn": "Advar" }, "trigger_subtype": { "both_buttons": "Begge knapper", @@ -39,7 +39,7 @@ "face_4": "med ansikt 4 aktivert", "face_5": "med ansikt 5 aktivert", "face_6": "med ansikt 6 aktivert", - "face_any": "Med alle/angitte ansikt (er) aktivert", + "face_any": "Med alle/angitte ansikt(er) aktivert", "left": "Venstre", "open": "\u00c5pen", "right": "H\u00f8yre", @@ -47,7 +47,7 @@ "turn_on": "Sl\u00e5 p\u00e5" }, "trigger_type": { - "device_dropped": "Enheten ble brutt", + "device_dropped": "Enheten ble sluppet", "device_flipped": "Enheten snudd \"{subtype}\"", "device_knocked": "Enheten sl\u00e5tt \"{subtype}\"", "device_rotated": "Enheten roterte \"{subtype}\"", @@ -55,13 +55,13 @@ "device_slid": "Enheten skled \"{subtype}\"", "device_tilted": "Enheten skr\u00e5stilt", "remote_button_double_press": "\"{subtype}\"-knappen ble dobbeltklikket", - "remote_button_long_press": "\"{subtype}\"-knappen ble kontinuerlig trykket", + "remote_button_long_press": "\"{subtype}\"-knappen ble holdt inne", "remote_button_long_release": "\"{subtype}\"-knappen sluppet etter langt trykk", - "remote_button_quadruple_press": "\"{subtype}\"-knappen ble firedoblet klikket", - "remote_button_quintuple_press": "\"{subtype}\"-knappen ble femdobbelt klikket", + "remote_button_quadruple_press": "\"{subtype}\"-knappen ble trykket fire ganger", + "remote_button_quintuple_press": "\"{subtype}\"-knappen ble trykket fem ganger", "remote_button_short_press": "\"{subtype}\"-knappen ble trykket", "remote_button_short_release": "\"{subtype}\"-knappen sluppet", - "remote_button_triple_press": "\"{subtype}\"-knappen ble trippel klikket" + "remote_button_triple_press": "\"{subtype}\"-knappen ble trippelklikket" } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/pl.json b/homeassistant/components/zha/.translations/pl.json index 0e1b7028dbb..4189ea6d9be 100644 --- a/homeassistant/components/zha/.translations/pl.json +++ b/homeassistant/components/zha/.translations/pl.json @@ -30,9 +30,9 @@ "button_4": "czwarty przycisk", "button_5": "pi\u0105ty przycisk", "button_6": "sz\u00f3sty przycisk", - "close": "zamkni\u0119cie", - "dim_down": "zmniejszenie jasno\u015bci", - "dim_up": "zwi\u0119kszenie jasno\u015bci", + "close": "nast\u0105pi zamkni\u0119cie", + "dim_down": "nast\u0105pi zmniejszenie jasno\u015bci", + "dim_up": "nast\u0105pi zwi\u0119kszenie jasno\u015bci", "face_1": "z aktywowan\u0105 twarz\u0105 1", "face_2": "z aktywowan\u0105 twarz\u0105 2", "face_3": "z aktywowan\u0105 twarz\u0105 3", @@ -43,25 +43,25 @@ "left": "w lewo", "open": "otwarcie", "right": "w prawo", - "turn_off": "wy\u0142\u0105czenie", - "turn_on": "w\u0142\u0105czenie" + "turn_off": "nast\u0105pi wy\u0142\u0105czenie", + "turn_on": "nast\u0105pi w\u0142\u0105czenie" }, "trigger_type": { - "device_dropped": "upadek urz\u0105dzenia", - "device_flipped": "odwr\u00f3cenie urz\u0105dzenia \"{subtype}\"", - "device_knocked": "pukni\u0119cie urz\u0105dzenia \"{subtype}\"", - "device_rotated": "obr\u00f3cenie urz\u0105dzenia \"{subtype}\"", - "device_shaken": "potrz\u0105\u015bni\u0119cie urz\u0105dzeniem", - "device_slid": "przesuni\u0119cie urz\u0105dzenia \"{subtype}\"", - "device_tilted": "przechylenie urz\u0105dzenia", - "remote_button_double_press": "przycisk \"{subtype}\" podw\u00f3jnie naci\u015bni\u0119ty", - "remote_button_long_press": "przycisk \"{subtype}\" naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y", - "remote_button_long_release": "przycisk \"{subtype}\" zwolniony po d\u0142ugim naci\u015bni\u0119ciu", + "device_dropped": "nast\u0105pi upadek urz\u0105dzenia", + "device_flipped": "nast\u0105pi odwr\u00f3cenie urz\u0105dzenia \"{subtype}\"", + "device_knocked": "nast\u0105pi pukni\u0119cie w urz\u0105dzenie \"{subtype}\"", + "device_rotated": "nast\u0105pi obr\u00f3cenie urz\u0105dzenia \"{subtype}\"", + "device_shaken": "nast\u0105pi potrz\u0105\u015bni\u0119cie urz\u0105dzeniem", + "device_slid": "nast\u0105pi przesuni\u0119cie urz\u0105dzenia \"{subtype}\"", + "device_tilted": "nast\u0105pi przechylenie urz\u0105dzenia", + "remote_button_double_press": "przycisk \"{subtype}\" zostanie podw\u00f3jnie naci\u015bni\u0119ty", + "remote_button_long_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y", + "remote_button_long_release": "przycisk \"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu", "remote_button_quadruple_press": "przycisk \"{subtype}\" czterokrotnie naci\u015bni\u0119ty", - "remote_button_quintuple_press": "przycisk \"{subtype}\" pi\u0119ciokrotnie naci\u015bni\u0119ty", - "remote_button_short_press": "przycisk \"{subtype}\" naci\u015bni\u0119ty", - "remote_button_short_release": "przycisk \"{subtype}\" zwolniony", - "remote_button_triple_press": "przycisk \"{subtype}\" trzykrotnie naci\u015bni\u0119ty" + "remote_button_quintuple_press": "przycisk \"{subtype}\" zostanie pi\u0119ciokrotnie naci\u015bni\u0119ty", + "remote_button_short_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty", + "remote_button_short_release": "przycisk \"{subtype}\" zostanie zwolniony", + "remote_button_triple_press": "przycisk \"{subtype}\" zostanie trzykrotnie naci\u015bni\u0119ty" } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/pt.json b/homeassistant/components/zha/.translations/pt.json index 8606a04e197..0c86dc95d09 100644 --- a/homeassistant/components/zha/.translations/pt.json +++ b/homeassistant/components/zha/.translations/pt.json @@ -16,5 +16,13 @@ } }, "title": "ZHA" + }, + "device_automation": { + "action_type": { + "warn": "Avisar" + }, + "trigger_subtype": { + "left": "Esquerda" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/ru.json b/homeassistant/components/zha/.translations/ru.json index 2f6f42311c3..1779ed613fc 100644 --- a/homeassistant/components/zha/.translations/ru.json +++ b/homeassistant/components/zha/.translations/ru.json @@ -4,7 +4,7 @@ "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": { - "cannot_connect": "\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" + "cannot_connect": "\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." }, "step": { "user": { @@ -19,7 +19,42 @@ }, "device_automation": { "action_type": { - "warn": "\u041f\u0440\u0435\u0434\u0443\u043f\u0440\u0435\u0436\u0434\u0435\u043d\u0438\u0435" + "squawk": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0441\u0438\u0440\u0435\u043d\u0443", + "warn": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043e\u043f\u043e\u0432\u0435\u0449\u0435\u043d\u0438\u0435" + }, + "trigger_subtype": { + "both_buttons": "\u041e\u0431\u0435 \u043a\u043d\u043e\u043f\u043a\u0438", + "button_1": "\u041f\u0435\u0440\u0432\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "button_2": "\u0412\u0442\u043e\u0440\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "button_3": "\u0422\u0440\u0435\u0442\u044c\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "button_4": "\u0427\u0435\u0442\u0432\u0435\u0440\u0442\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "button_5": "\u041f\u044f\u0442\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "button_6": "\u0428\u0435\u0441\u0442\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "close": "\u0417\u0430\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", + "dim_down": "\u042f\u0440\u043a\u043e\u0441\u0442\u044c \u0443\u043c\u0435\u043d\u044c\u0448\u0430\u0435\u0442\u0441\u044f", + "dim_up": "\u042f\u0440\u043a\u043e\u0441\u0442\u044c \u0443\u0432\u0435\u043b\u0438\u0447\u0438\u0432\u0430\u0435\u0442\u0441\u044f", + "left": "\u041d\u0430\u043b\u0435\u0432\u043e", + "open": "\u041e\u0442\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", + "right": "\u041d\u0430\u043f\u0440\u0430\u0432\u043e", + "turn_off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f", + "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f" + }, + "trigger_type": { + "device_dropped": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0441\u0431\u0440\u043e\u0441\u0438\u043b\u0438", + "device_flipped": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \"{subtype}\"", + "device_knocked": "\u041f\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443 \u043f\u043e\u0441\u0442\u0443\u0447\u0430\u043b\u0438 \"{subtype}\"", + "device_rotated": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u043e\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \"{subtype}\"", + "device_shaken": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0432\u0441\u0442\u0440\u044f\u0445\u043d\u0443\u043b\u0438", + "device_slid": "\u0421\u0434\u0432\u0438\u0433 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \"{subtype}\"", + "device_tilted": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0430\u043a\u043b\u043e\u043d\u0438\u043b\u0438", + "remote_button_double_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0434\u0432\u0430 \u0440\u0430\u0437\u0430", + "remote_button_long_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0435\u043f\u0440\u0435\u0440\u044b\u0432\u043d\u043e \u043d\u0430\u0436\u0430\u0442\u0430", + "remote_button_long_release": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u043d\u0435\u043f\u0440\u0435\u0440\u044b\u0432\u043d\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", + "remote_button_quadruple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0447\u0435\u0442\u044b\u0440\u0435 \u0440\u0430\u0437\u0430", + "remote_button_quintuple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u043f\u044f\u0442\u044c \u0440\u0430\u0437", + "remote_button_short_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430", + "remote_button_short_release": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430", + "remote_button_triple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0442\u0440\u0438 \u0440\u0430\u0437\u0430" } } } \ No newline at end of file diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index ff9f27d4843..6f24db442dd 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -4,6 +4,8 @@ import asyncio import logging import voluptuous as vol +from zigpy.types.named import EUI64 +import zigpy.zdo.types as zdo_types from homeassistant.components import websocket_api from homeassistant.core import callback @@ -44,7 +46,7 @@ from .core.const import ( WARNING_DEVICE_STROBE_HIGH, WARNING_DEVICE_STROBE_YES, ) -from .core.helpers import async_is_bindable_target, convert_ieee, get_matched_clusters +from .core.helpers import async_is_bindable_target, get_matched_clusters _LOGGER = logging.getLogger(__name__) @@ -76,16 +78,16 @@ IEEE_SERVICE = "ieee_based_service" SERVICE_SCHEMAS = { SERVICE_PERMIT: vol.Schema( { - vol.Optional(ATTR_IEEE_ADDRESS, default=None): convert_ieee, + vol.Optional(ATTR_IEEE_ADDRESS, default=None): EUI64.convert, vol.Optional(ATTR_DURATION, default=60): vol.All( vol.Coerce(int), vol.Range(0, 254) ), } ), - IEEE_SERVICE: vol.Schema({vol.Required(ATTR_IEEE_ADDRESS): convert_ieee}), + IEEE_SERVICE: vol.Schema({vol.Required(ATTR_IEEE_ADDRESS): EUI64.convert}), SERVICE_SET_ZIGBEE_CLUSTER_ATTRIBUTE: vol.Schema( { - vol.Required(ATTR_IEEE): convert_ieee, + vol.Required(ATTR_IEEE): EUI64.convert, vol.Required(ATTR_ENDPOINT_ID): cv.positive_int, vol.Required(ATTR_CLUSTER_ID): cv.positive_int, vol.Optional(ATTR_CLUSTER_TYPE, default=CLUSTER_TYPE_IN): cv.string, @@ -96,7 +98,7 @@ SERVICE_SCHEMAS = { ), SERVICE_WARNING_DEVICE_SQUAWK: vol.Schema( { - vol.Required(ATTR_IEEE): convert_ieee, + vol.Required(ATTR_IEEE): EUI64.convert, vol.Optional( ATTR_WARNING_DEVICE_MODE, default=WARNING_DEVICE_SQUAWK_MODE_ARMED ): cv.positive_int, @@ -110,7 +112,7 @@ SERVICE_SCHEMAS = { ), SERVICE_WARNING_DEVICE_WARN: vol.Schema( { - vol.Required(ATTR_IEEE): convert_ieee, + vol.Required(ATTR_IEEE): EUI64.convert, vol.Optional( ATTR_WARNING_DEVICE_MODE, default=WARNING_DEVICE_MODE_EMERGENCY ): cv.positive_int, @@ -131,7 +133,7 @@ SERVICE_SCHEMAS = { ), SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND: vol.Schema( { - vol.Required(ATTR_IEEE): convert_ieee, + vol.Required(ATTR_IEEE): EUI64.convert, vol.Required(ATTR_ENDPOINT_ID): cv.positive_int, vol.Required(ATTR_CLUSTER_ID): cv.positive_int, vol.Optional(ATTR_CLUSTER_TYPE, default=CLUSTER_TYPE_IN): cv.string, @@ -149,7 +151,7 @@ SERVICE_SCHEMAS = { @websocket_api.websocket_command( { vol.Required("type"): "zha/devices/permit", - vol.Optional(ATTR_IEEE, default=None): convert_ieee, + vol.Optional(ATTR_IEEE, default=None): EUI64.convert, vol.Optional(ATTR_DURATION, default=60): vol.All( vol.Coerce(int), vol.Range(0, 254) ), @@ -200,7 +202,7 @@ async def websocket_get_devices(hass, connection, msg): @websocket_api.require_admin @websocket_api.async_response @websocket_api.websocket_command( - {vol.Required(TYPE): "zha/device", vol.Required(ATTR_IEEE): convert_ieee} + {vol.Required(TYPE): "zha/device", vol.Required(ATTR_IEEE): EUI64.convert} ) async def websocket_get_device(hass, connection, msg): """Get ZHA devices.""" @@ -252,7 +254,7 @@ def async_get_device_info(hass, device, ha_device_registry=None): @websocket_api.websocket_command( { vol.Required(TYPE): "zha/devices/reconfigure", - vol.Required(ATTR_IEEE): convert_ieee, + vol.Required(ATTR_IEEE): EUI64.convert, } ) async def websocket_reconfigure_node(hass, connection, msg): @@ -267,7 +269,7 @@ async def websocket_reconfigure_node(hass, connection, msg): @websocket_api.require_admin @websocket_api.async_response @websocket_api.websocket_command( - {vol.Required(TYPE): "zha/devices/clusters", vol.Required(ATTR_IEEE): convert_ieee} + {vol.Required(TYPE): "zha/devices/clusters", vol.Required(ATTR_IEEE): EUI64.convert} ) async def websocket_device_clusters(hass, connection, msg): """Return a list of device clusters.""" @@ -305,7 +307,7 @@ async def websocket_device_clusters(hass, connection, msg): @websocket_api.websocket_command( { vol.Required(TYPE): "zha/devices/clusters/attributes", - vol.Required(ATTR_IEEE): convert_ieee, + vol.Required(ATTR_IEEE): EUI64.convert, vol.Required(ATTR_ENDPOINT_ID): int, vol.Required(ATTR_CLUSTER_ID): int, vol.Required(ATTR_CLUSTER_TYPE): str, @@ -346,7 +348,7 @@ async def websocket_device_cluster_attributes(hass, connection, msg): @websocket_api.websocket_command( { vol.Required(TYPE): "zha/devices/clusters/commands", - vol.Required(ATTR_IEEE): convert_ieee, + vol.Required(ATTR_IEEE): EUI64.convert, vol.Required(ATTR_ENDPOINT_ID): int, vol.Required(ATTR_CLUSTER_ID): int, vol.Required(ATTR_CLUSTER_TYPE): str, @@ -400,7 +402,7 @@ async def websocket_device_cluster_commands(hass, connection, msg): @websocket_api.websocket_command( { vol.Required(TYPE): "zha/devices/clusters/attributes/value", - vol.Required(ATTR_IEEE): convert_ieee, + vol.Required(ATTR_IEEE): EUI64.convert, vol.Required(ATTR_ENDPOINT_ID): int, vol.Required(ATTR_CLUSTER_ID): int, vol.Required(ATTR_CLUSTER_TYPE): str, @@ -444,7 +446,7 @@ async def websocket_read_zigbee_cluster_attributes(hass, connection, msg): @websocket_api.require_admin @websocket_api.async_response @websocket_api.websocket_command( - {vol.Required(TYPE): "zha/devices/bindable", vol.Required(ATTR_IEEE): convert_ieee} + {vol.Required(TYPE): "zha/devices/bindable", vol.Required(ATTR_IEEE): EUI64.convert} ) async def websocket_get_bindable_devices(hass, connection, msg): """Directly bind devices.""" @@ -472,8 +474,8 @@ async def websocket_get_bindable_devices(hass, connection, msg): @websocket_api.websocket_command( { vol.Required(TYPE): "zha/devices/bind", - vol.Required(ATTR_SOURCE_IEEE): convert_ieee, - vol.Required(ATTR_TARGET_IEEE): convert_ieee, + vol.Required(ATTR_SOURCE_IEEE): EUI64.convert, + vol.Required(ATTR_TARGET_IEEE): EUI64.convert, } ) async def websocket_bind_devices(hass, connection, msg): @@ -494,8 +496,8 @@ async def websocket_bind_devices(hass, connection, msg): @websocket_api.websocket_command( { vol.Required(TYPE): "zha/devices/unbind", - vol.Required(ATTR_SOURCE_IEEE): convert_ieee, - vol.Required(ATTR_TARGET_IEEE): convert_ieee, + vol.Required(ATTR_SOURCE_IEEE): EUI64.convert, + vol.Required(ATTR_TARGET_IEEE): EUI64.convert, } ) async def websocket_unbind_devices(hass, connection, msg): @@ -513,7 +515,6 @@ async def websocket_unbind_devices(hass, connection, msg): async def async_binding_operation(zha_gateway, source_ieee, target_ieee, operation): """Create or remove a direct zigbee binding between 2 devices.""" - from zigpy.zdo import types as zdo_types source_device = zha_gateway.get_device(source_ieee) target_device = zha_gateway.get_device(target_ieee) diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py index 37b0bec207b..66a31ff8f21 100644 --- a/homeassistant/components/zha/core/channels/__init__.py +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -11,6 +11,8 @@ from functools import wraps import logging from random import uniform +import zigpy.exceptions + from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -48,8 +50,6 @@ def decorate_command(channel, command): @wraps(command) async def wrapper(*args, **kwds): - from zigpy.exceptions import DeliveryError - try: result = await command(*args, **kwds) channel.debug( @@ -61,7 +61,7 @@ def decorate_command(channel, command): ) return result - except (DeliveryError, Timeout) as ex: + except (zigpy.exceptions.DeliveryError, Timeout) as ex: channel.debug("command failed: %s exception: %s", command.__name__, str(ex)) return ex @@ -143,12 +143,10 @@ class ZigbeeChannel(LogMixin): This also swallows DeliveryError exceptions that are thrown when devices are unreachable. """ - from zigpy.exceptions import DeliveryError - try: res = await self.cluster.bind() self.debug("bound '%s' cluster: %s", self.cluster.ep_attribute, res[0]) - except (DeliveryError, Timeout) as ex: + except (zigpy.exceptions.DeliveryError, Timeout) as ex: self.debug( "Failed to bind '%s' cluster: %s", self.cluster.ep_attribute, str(ex) ) @@ -167,8 +165,6 @@ class ZigbeeChannel(LogMixin): This also swallows DeliveryError exceptions that are thrown when devices are unreachable. """ - from zigpy.exceptions import DeliveryError - attr_name = self.cluster.attributes.get(attr, [attr])[0] kwargs = {} @@ -189,7 +185,7 @@ class ZigbeeChannel(LogMixin): reportable_change, res, ) - except (DeliveryError, Timeout) as ex: + except (zigpy.exceptions.DeliveryError, Timeout) as ex: self.debug( "failed to set reporting for '%s' attr on '%s' cluster: %s", attr_name, diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index e9e2c3b7ea6..b3be8037ff6 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -10,6 +10,10 @@ from enum import Enum import logging import time +import zigpy.exceptions +import zigpy.quirks +from zigpy.profiles import zha, zll + from homeassistant.core import callback from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, @@ -87,9 +91,7 @@ class ZHADevice(LogMixin): self._unsub = async_dispatcher_connect( self.hass, self._available_signal, self.async_initialize ) - from zigpy.quirks import CustomDevice - - self.quirk_applied = isinstance(self._zigpy_device, CustomDevice) + self.quirk_applied = isinstance(self._zigpy_device, zigpy.quirks.CustomDevice) self.quirk_class = "{}.{}".format( self._zigpy_device.__class__.__module__, self._zigpy_device.__class__.__name__, @@ -348,7 +350,6 @@ class ZHADevice(LogMixin): zdo_task = None for channel in channels: if channel.name == CHANNEL_ZDO: - # pylint: disable=E1111 if zdo_task is None: # We only want to do this once zdo_task = self._async_create_task( semaphore, channel, task_name, *args @@ -373,8 +374,7 @@ class ZHADevice(LogMixin): @callback def async_unsub_dispatcher(self): """Unsubscribe the dispatcher.""" - if self._unsub: - self._unsub() + self._unsub() @callback def async_update_last_seen(self, last_seen): @@ -396,7 +396,6 @@ class ZHADevice(LogMixin): @callback def async_get_std_clusters(self): """Get ZHA and ZLL clusters for this device.""" - from zigpy.profiles import zha, zll return { ep_id: { @@ -450,8 +449,6 @@ class ZHADevice(LogMixin): if cluster is None: return None - from zigpy.exceptions import DeliveryError - try: response = await cluster.write_attributes( {attribute: value}, manufacturer=manufacturer @@ -465,7 +462,7 @@ class ZHADevice(LogMixin): response, ) return response - except DeliveryError as exc: + except zigpy.exceptions.DeliveryError as exc: self.debug( "failed to set attribute: %s %s %s %s %s", f"{ATTR_VALUE}: {value}", diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py index 622adead803..e23862a7d3e 100644 --- a/homeassistant/components/zha/core/discovery.py +++ b/homeassistant/components/zha/core/discovery.py @@ -7,6 +7,9 @@ https://home-assistant.io/integrations/zha/ import logging +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 @@ -52,8 +55,6 @@ def async_process_endpoint( is_new_join, ): """Process an endpoint on a zigpy device.""" - import zigpy.profiles - if endpoint_id == 0: # ZDO _async_create_cluster_channel( endpoint, zha_device, is_new_join, channel_class=ZDOChannel @@ -179,8 +180,6 @@ def _async_handle_single_cluster_matches( hass, endpoint, zha_device, profile_clusters, device_key, is_new_join ): """Dispatch single cluster matches to HA components.""" - from zigpy.zcl.clusters.general import OnOff, PowerConfiguration - cluster_matches = [] cluster_match_results = [] matched_power_configuration = False diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index a64e8cf7fd9..77702c8f3de 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -108,9 +108,9 @@ class ZHAGateway: baudrate = self._config.get(CONF_BAUDRATE, DEFAULT_BAUDRATE) radio_type = self._config_entry.data.get(CONF_RADIO_TYPE) - radio_details = RADIO_TYPES[radio_type][ZHA_GW_RADIO]() - radio = radio_details[ZHA_GW_RADIO] - self.radio_description = RADIO_TYPES[radio_type][ZHA_GW_RADIO_DESCRIPTION] + radio_details = RADIO_TYPES[radio_type] + radio = radio_details[ZHA_GW_RADIO]() + self.radio_description = radio_details[ZHA_GW_RADIO_DESCRIPTION] await radio.connect(usb_path, baudrate) if CONF_DATABASE in self._config: diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index 88a472716cc..d3f06090dae 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -8,6 +8,16 @@ 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 ( @@ -49,25 +59,17 @@ async def safe_read( async def check_zigpy_connection(usb_path, radio_type, database_path): """Test zigpy radio connection.""" if radio_type == RadioType.ezsp.name: - import bellows.ezsp - from bellows.zigbee.application import ControllerApplication - radio = bellows.ezsp.EZSP() + ControllerApplication = bellows.zigbee.application.ControllerApplication elif radio_type == RadioType.xbee.name: - import zigpy_xbee.api - from zigpy_xbee.zigbee.application import ControllerApplication - radio = zigpy_xbee.api.XBee() + ControllerApplication = zigpy_xbee.zigbee.application.ControllerApplication elif radio_type == RadioType.deconz.name: - import zigpy_deconz.api - from zigpy_deconz.zigbee.application import ControllerApplication - radio = zigpy_deconz.api.Deconz() + ControllerApplication = zigpy_deconz.zigbee.application.ControllerApplication elif radio_type == RadioType.zigate.name: - import zigpy_zigate.api - from zigpy_zigate.zigbee.application import ControllerApplication - radio = zigpy_zigate.api.ZiGate() + ControllerApplication = zigpy_zigate.zigbee.application.ControllerApplication try: await radio.connect(usb_path, DEFAULT_BAUDRATE) controller = ControllerApplication(radio, database_path) @@ -78,15 +80,6 @@ async def check_zigpy_connection(usb_path, radio_type, database_path): return True -def convert_ieee(ieee_str): - """Convert given ieee string to EUI64.""" - from zigpy.types import EUI64, uint8_t - - if ieee_str is None: - return None - return EUI64([uint8_t(p, base=16) for p in ieee_str.split(":")]) - - def get_attr_id_by_name(cluster, attr_name): """Get the attribute id for a cluster attribute by its name.""" return next( @@ -145,7 +138,7 @@ async def async_get_zha_device(hass, device_id): registry_device = device_registry.async_get(device_id) zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] ieee_address = list(list(registry_device.identifiers)[0])[1] - ieee = convert_ieee(ieee_address) + ieee = zigpy.types.EUI64.convert(ieee_address) return zha_gateway.devices[ieee] diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index 43ddc888d2f..571e77d4fae 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -6,6 +6,18 @@ https://home-assistant.io/integrations/zha/ """ import collections +import bellows.ezsp +import bellows.zigbee.application +import zigpy.profiles.zha +import zigpy.profiles.zll +import zigpy.zcl as zcl +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.components.binary_sensor import DOMAIN as BINARY_SENSOR from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER from homeassistant.components.fan import DOMAIN as FAN @@ -14,6 +26,8 @@ from homeassistant.components.lock import DOMAIN as LOCK from homeassistant.components.sensor import DOMAIN as SENSOR from homeassistant.components.switch import DOMAIN as SWITCH +# importing channels updates registries +from . import channels # noqa pylint: disable=wrong-import-position,unused-import from .const import ( CONTROLLER, SENSOR_ACCELERATION, @@ -63,9 +77,6 @@ COMPONENT_CLUSTERS = { ZIGBEE_CHANNEL_REGISTRY = DictRegistry() -# importing channels updates registries -from . import channels # noqa pylint: disable=wrong-import-position,unused-import - def establish_device_mappings(): """Establish mappings between ZCL objects and HA ZHA objects. @@ -73,56 +84,27 @@ def establish_device_mappings(): These cannot be module level, as importing bellows must be done in a in a function. """ - from zigpy import zcl - from zigpy.profiles import zha, zll - - def get_ezsp_radio(): - import bellows.ezsp - from bellows.zigbee.application import ControllerApplication - - return {ZHA_GW_RADIO: bellows.ezsp.EZSP(), CONTROLLER: ControllerApplication} - RADIO_TYPES[RadioType.ezsp.name] = { - ZHA_GW_RADIO: get_ezsp_radio, + ZHA_GW_RADIO: bellows.ezsp.EZSP, + CONTROLLER: bellows.zigbee.application.ControllerApplication, ZHA_GW_RADIO_DESCRIPTION: "EZSP", } - def get_deconz_radio(): - import zigpy_deconz.api - from zigpy_deconz.zigbee.application import ControllerApplication - - return { - ZHA_GW_RADIO: zigpy_deconz.api.Deconz(), - CONTROLLER: ControllerApplication, - } - RADIO_TYPES[RadioType.deconz.name] = { - ZHA_GW_RADIO: get_deconz_radio, + ZHA_GW_RADIO: zigpy_deconz.api.Deconz, + CONTROLLER: zigpy_deconz.zigbee.application.ControllerApplication, ZHA_GW_RADIO_DESCRIPTION: "Deconz", } - def get_xbee_radio(): - import zigpy_xbee.api - from zigpy_xbee.zigbee.application import ControllerApplication - - return {ZHA_GW_RADIO: zigpy_xbee.api.XBee(), CONTROLLER: ControllerApplication} - RADIO_TYPES[RadioType.xbee.name] = { - ZHA_GW_RADIO: get_xbee_radio, + ZHA_GW_RADIO: zigpy_xbee.api.XBee, + CONTROLLER: zigpy_xbee.zigbee.application.ControllerApplication, ZHA_GW_RADIO_DESCRIPTION: "XBee", } - def get_zigate_radio(): - import zigpy_zigate.api - from zigpy_zigate.zigbee.application import ControllerApplication - - return { - ZHA_GW_RADIO: zigpy_zigate.api.ZiGate(), - CONTROLLER: ControllerApplication, - } - RADIO_TYPES[RadioType.zigate.name] = { - ZHA_GW_RADIO: get_zigate_radio, + ZHA_GW_RADIO: zigpy_zigate.api.ZiGate, + CONTROLLER: zigpy_zigate.zigbee.application.ControllerApplication, ZHA_GW_RADIO_DESCRIPTION: "ZiGate", } @@ -137,33 +119,33 @@ def establish_device_mappings(): } ) - DEVICE_CLASS[zha.PROFILE_ID].update( + DEVICE_CLASS[zigpy.profiles.zha.PROFILE_ID].update( { SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE: DEVICE_TRACKER, - zha.DeviceType.COLOR_DIMMABLE_LIGHT: LIGHT, - zha.DeviceType.COLOR_TEMPERATURE_LIGHT: LIGHT, - zha.DeviceType.DIMMABLE_BALLAST: LIGHT, - zha.DeviceType.DIMMABLE_LIGHT: LIGHT, - zha.DeviceType.DIMMABLE_PLUG_IN_UNIT: LIGHT, - zha.DeviceType.EXTENDED_COLOR_LIGHT: LIGHT, - zha.DeviceType.LEVEL_CONTROLLABLE_OUTPUT: LIGHT, - zha.DeviceType.ON_OFF_BALLAST: SWITCH, - zha.DeviceType.ON_OFF_LIGHT: LIGHT, - zha.DeviceType.ON_OFF_LIGHT_SWITCH: SWITCH, - zha.DeviceType.ON_OFF_PLUG_IN_UNIT: SWITCH, - zha.DeviceType.SMART_PLUG: SWITCH, + zigpy.profiles.zha.DeviceType.COLOR_DIMMABLE_LIGHT: LIGHT, + zigpy.profiles.zha.DeviceType.COLOR_TEMPERATURE_LIGHT: LIGHT, + zigpy.profiles.zha.DeviceType.DIMMABLE_BALLAST: LIGHT, + zigpy.profiles.zha.DeviceType.DIMMABLE_LIGHT: LIGHT, + zigpy.profiles.zha.DeviceType.DIMMABLE_PLUG_IN_UNIT: LIGHT, + zigpy.profiles.zha.DeviceType.EXTENDED_COLOR_LIGHT: LIGHT, + zigpy.profiles.zha.DeviceType.LEVEL_CONTROLLABLE_OUTPUT: LIGHT, + zigpy.profiles.zha.DeviceType.ON_OFF_BALLAST: SWITCH, + zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT: LIGHT, + zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT_SWITCH: SWITCH, + zigpy.profiles.zha.DeviceType.ON_OFF_PLUG_IN_UNIT: SWITCH, + zigpy.profiles.zha.DeviceType.SMART_PLUG: SWITCH, } ) - DEVICE_CLASS[zll.PROFILE_ID].update( + DEVICE_CLASS[zigpy.profiles.zll.PROFILE_ID].update( { - zll.DeviceType.COLOR_LIGHT: LIGHT, - zll.DeviceType.COLOR_TEMPERATURE_LIGHT: LIGHT, - zll.DeviceType.DIMMABLE_LIGHT: LIGHT, - zll.DeviceType.DIMMABLE_PLUGIN_UNIT: LIGHT, - zll.DeviceType.EXTENDED_COLOR_LIGHT: LIGHT, - zll.DeviceType.ON_OFF_LIGHT: LIGHT, - zll.DeviceType.ON_OFF_PLUGIN_UNIT: SWITCH, + zigpy.profiles.zll.DeviceType.COLOR_LIGHT: LIGHT, + zigpy.profiles.zll.DeviceType.COLOR_TEMPERATURE_LIGHT: LIGHT, + zigpy.profiles.zll.DeviceType.DIMMABLE_LIGHT: LIGHT, + zigpy.profiles.zll.DeviceType.DIMMABLE_PLUGIN_UNIT: LIGHT, + zigpy.profiles.zll.DeviceType.EXTENDED_COLOR_LIGHT: LIGHT, + zigpy.profiles.zll.DeviceType.ON_OFF_LIGHT: LIGHT, + zigpy.profiles.zll.DeviceType.ON_OFF_PLUGIN_UNIT: SWITCH, } ) @@ -207,19 +189,21 @@ def establish_device_mappings(): } ) - zhap = zha.PROFILE_ID - REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.COLOR_CONTROLLER) - REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.COLOR_DIMMER_SWITCH) - REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.COLOR_SCENE_CONTROLLER) - REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.DIMMER_SWITCH) - REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.NON_COLOR_CONTROLLER) - REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.NON_COLOR_SCENE_CONTROLLER) - REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.REMOTE_CONTROL) - REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.SCENE_SELECTOR) + 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) + REMOTE_DEVICE_TYPES[zha.PROFILE_ID].append(zha.DeviceType.COLOR_SCENE_CONTROLLER) + REMOTE_DEVICE_TYPES[zha.PROFILE_ID].append(zha.DeviceType.DIMMER_SWITCH) + REMOTE_DEVICE_TYPES[zha.PROFILE_ID].append(zha.DeviceType.NON_COLOR_CONTROLLER) + REMOTE_DEVICE_TYPES[zha.PROFILE_ID].append( + zha.DeviceType.NON_COLOR_SCENE_CONTROLLER + ) + REMOTE_DEVICE_TYPES[zha.PROFILE_ID].append(zha.DeviceType.REMOTE_CONTROL) + REMOTE_DEVICE_TYPES[zha.PROFILE_ID].append(zha.DeviceType.SCENE_SELECTOR) - zllp = zll.PROFILE_ID - REMOTE_DEVICE_TYPES[zllp].append(zll.DeviceType.COLOR_CONTROLLER) - REMOTE_DEVICE_TYPES[zllp].append(zll.DeviceType.COLOR_SCENE_CONTROLLER) - REMOTE_DEVICE_TYPES[zllp].append(zll.DeviceType.CONTROL_BRIDGE) - REMOTE_DEVICE_TYPES[zllp].append(zll.DeviceType.CONTROLLER) - REMOTE_DEVICE_TYPES[zllp].append(zll.DeviceType.SCENE_CONTROLLER) + zll = zigpy.profiles.zll + REMOTE_DEVICE_TYPES[zll.PROFILE_ID].append(zll.DeviceType.COLOR_CONTROLLER) + REMOTE_DEVICE_TYPES[zll.PROFILE_ID].append(zll.DeviceType.COLOR_SCENE_CONTROLLER) + 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) diff --git a/homeassistant/components/zha/device_action.py b/homeassistant/components/zha/device_action.py index 460676a75a0..60cfa0eec00 100644 --- a/homeassistant/components/zha/device_action.py +++ b/homeassistant/components/zha/device_action.py @@ -5,7 +5,7 @@ import voluptuous as vol from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_TYPE from homeassistant.core import Context, HomeAssistant -from homeassistant.helpers import config_validation as cv, service +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.typing import ConfigType, TemplateVarsType from . import DOMAIN @@ -78,13 +78,10 @@ async def _execute_service_based_action( service_name = SERVICE_NAMES[action_type] zha_device = await async_get_zha_device(hass, config[CONF_DEVICE_ID]) - service_action = { - service.CONF_SERVICE: "{}.{}".format(DOMAIN, service_name), - ATTR_DATA: {ATTR_IEEE: str(zha_device.ieee)}, - } + service_data = {ATTR_IEEE: str(zha_device.ieee)} - await service.async_call_from_config( - hass, service_action, blocking=True, variables=variables, context=context + await hass.services.async_call( + DOMAIN, service_name, service_data, blocking=True, context=context ) diff --git a/homeassistant/components/zha/device_trigger.py b/homeassistant/components/zha/device_trigger.py index c1ea3c2b761..cdd62b11d1e 100644 --- a/homeassistant/components/zha/device_trigger.py +++ b/homeassistant/components/zha/device_trigger.py @@ -21,24 +21,36 @@ TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( ) +async def async_validate_trigger_config(hass, config): + """Validate config.""" + config = TRIGGER_SCHEMA(config) + + if "zha" in hass.config.components: + trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) + zha_device = await async_get_zha_device(hass, config[CONF_DEVICE_ID]) + if ( + zha_device.device_automation_triggers is None + or trigger not in zha_device.device_automation_triggers + ): + raise InvalidDeviceAutomationConfig + + return config + + async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) zha_device = await async_get_zha_device(hass, config[CONF_DEVICE_ID]) - if ( - zha_device.device_automation_triggers is None - or trigger not in zha_device.device_automation_triggers - ): - raise InvalidDeviceAutomationConfig - trigger = zha_device.device_automation_triggers[trigger] event_config = { + event.CONF_PLATFORM: "event", event.CONF_EVENT_TYPE: ZHA_EVENT, event.CONF_EVENT_DATA: {DEVICE_IEEE: str(zha_device.ieee), **trigger}, } + event_config = event.TRIGGER_SCHEMA(event_config) return await event.async_attach_trigger( hass, event_config, action, automation_info, platform_type="device" ) diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index 00c3942358e..c11cd405a99 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -40,7 +40,7 @@ class ZhaEntity(RestoreEntity, LogMixin, entity.Entity): self._unique_id = unique_id if not skip_entity_id: ieee = zha_device.ieee - ieeetail = "".join(["%02x" % (o,) for o in ieee[-4:]]) + ieeetail = "".join([f"{o:02x}" for o in ieee[:4]]) self.entity_id = "{}.{}_{}_{}_{}{}".format( self._domain, slugify(zha_device.manufacturer), diff --git a/homeassistant/components/zha/fan.py b/homeassistant/components/zha/fan.py index 1f119ef6657..43ad2291cb7 100644 --- a/homeassistant/components/zha/fan.py +++ b/homeassistant/components/zha/fan.py @@ -43,7 +43,7 @@ SPEED_LIST = [ SPEED_SMART, ] -VALUE_TO_SPEED = {i: speed for i, speed in enumerate(SPEED_LIST)} +VALUE_TO_SPEED = dict(enumerate(SPEED_LIST)) SPEED_TO_VALUE = {speed: i for i, speed in enumerate(SPEED_LIST)} diff --git a/homeassistant/components/zha/lock.py b/homeassistant/components/zha/lock.py index afc4618343c..a2151b4bdcb 100644 --- a/homeassistant/components/zha/lock.py +++ b/homeassistant/components/zha/lock.py @@ -27,7 +27,7 @@ _LOGGER = logging.getLogger(__name__) STATE_LIST = [STATE_UNLOCKED, STATE_LOCKED, STATE_UNLOCKED] -VALUE_TO_STATE = {i: state for i, state in enumerate(STATE_LIST)} +VALUE_TO_STATE = dict(enumerate(STATE_LIST)) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 9790fbffd06..9821ec2025b 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -6,10 +6,10 @@ "requirements": [ "bellows-homeassistant==0.10.0", "zha-quirks==0.0.26", - "zigpy-deconz==0.5.0", - "zigpy-homeassistant==0.9.0", - "zigpy-xbee-homeassistant==0.5.0", - "zigpy-zigate==0.4.1" + "zigpy-deconz==0.6.0", + "zigpy-homeassistant==0.10.0", + "zigpy-xbee-homeassistant==0.6.0", + "zigpy-zigate==0.5.0" ], "dependencies": [], "codeowners": ["@dmulcahey", "@adminiuga"] diff --git a/homeassistant/components/zigbee/__init__.py b/homeassistant/components/zigbee/__init__.py index 31cbc0c65b6..e74726a70f9 100644 --- a/homeassistant/components/zigbee/__init__.py +++ b/homeassistant/components/zigbee/__init__.py @@ -2,6 +2,11 @@ import logging from binascii import hexlify, unhexlify +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 homeassistant.const import ( @@ -75,12 +80,6 @@ def setup(hass, config): global ZIGBEE_EXCEPTION global ZIGBEE_TX_FAILURE - 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 - GPIO_DIGITAL_OUTPUT_LOW = xb_const.GPIO_DIGITAL_OUTPUT_LOW GPIO_DIGITAL_OUTPUT_HIGH = xb_const.GPIO_DIGITAL_OUTPUT_HIGH ADC_PERCENTAGE = xb_const.ADC_PERCENTAGE diff --git a/homeassistant/components/zone/config_flow.py b/homeassistant/components/zone/config_flow.py index d23fb5a4757..39633754772 100644 --- a/homeassistant/components/zone/config_flow.py +++ b/homeassistant/components/zone/config_flow.py @@ -20,7 +20,7 @@ from homeassistant.util import slugify from .const import CONF_PASSIVE, DOMAIN, HOME_ZONE -# mypy: allow-untyped-defs +# mypy: allow-untyped-defs, no-check-untyped-defs @callback diff --git a/homeassistant/components/zwave/.translations/ru.json b/homeassistant/components/zwave/.translations/ru.json index ed2e20f3527..4243f583082 100644 --- a/homeassistant/components/zwave/.translations/ru.json +++ b/homeassistant/components/zwave/.translations/ru.json @@ -2,7 +2,7 @@ "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.", - "one_instance_only": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043c\u043e\u0436\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u0441 \u043e\u0434\u043d\u0438\u043c \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u043e\u043c Z-Wave" + "one_instance_only": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043c\u043e\u0436\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u0441 \u043e\u0434\u043d\u0438\u043c \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u043e\u043c Z-Wave." }, "error": { "option_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 Z-Wave. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043f\u0443\u0442\u044c \u043a USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443." @@ -13,7 +13,7 @@ "network_key": "\u041a\u043b\u044e\u0447 \u0441\u0435\u0442\u0438 (\u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0443\u0441\u0442\u044b\u043c \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438)", "usb_path": "\u041f\u0443\u0442\u044c \u043a USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" }, - "description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439](https://www.home-assistant.io/docs/z-wave/installation/) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430", + "description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439](https://www.home-assistant.io/docs/z-wave/installation/) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430.", "title": "Z-Wave" } }, diff --git a/homeassistant/config.py b/homeassistant/config.py index 97c996d9e59..9f49889791e 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -66,9 +66,11 @@ VERSION_FILE = ".HA_VERSION" CONFIG_DIR_NAME = ".homeassistant" DATA_CUSTOMIZE = "hass_customize" -FILE_MIGRATION = (("ios.conf", ".ios.conf"),) +GROUP_CONFIG_PATH = "groups.yaml" +AUTOMATION_CONFIG_PATH = "automations.yaml" +SCRIPT_CONFIG_PATH = "scripts.yaml" -DEFAULT_CONFIG = """ +DEFAULT_CONFIG = f""" # Configure a default setup of Home Assistant (frontend, api, etc) default_config: @@ -80,9 +82,9 @@ default_config: tts: - platform: google_translate -group: !include groups.yaml -automation: !include automations.yaml -script: !include scripts.yaml +group: !include {GROUP_CONFIG_PATH} +automation: !include {AUTOMATION_CONFIG_PATH} +script: !include {SCRIPT_CONFIG_PATH} """ DEFAULT_SECRETS = """ # Use this file to store secrets like usernames and passwords. @@ -253,12 +255,6 @@ async def async_create_default_config( def _write_default_config(config_dir: str) -> Optional[str]: """Write the default config.""" - from homeassistant.components.config.group import CONFIG_PATH as GROUP_CONFIG_PATH - from homeassistant.components.config.automation import ( - CONFIG_PATH as AUTOMATION_CONFIG_PATH, - ) - from homeassistant.components.config.script import CONFIG_PATH as SCRIPT_CONFIG_PATH - config_path = os.path.join(config_dir, YAML_CONFIG_FILE) secret_path = os.path.join(config_dir, SECRET_YAML) version_path = os.path.join(config_dir, VERSION_FILE) @@ -407,12 +403,6 @@ def process_ha_config_upgrade(hass: HomeAssistant) -> None: with open(version_path, "wt") as outp: outp.write(__version__) - _LOGGER.debug("Migrating old system configuration files to new locations") - for oldf, newf in FILE_MIGRATION: - if os.path.isfile(hass.config.path(oldf)): - _LOGGER.info("Migrating %s to %s", oldf, newf) - os.rename(hass.config.path(oldf), hass.config.path(newf)) - @callback def async_log_exception( @@ -468,12 +458,7 @@ def _format_config_error(ex: Exception, domain: str, config: Dict) -> str: return message -async def async_process_ha_core_config( - hass: HomeAssistant, - config: Dict, - api_password: Optional[str] = None, - trusted_networks: Optional[Any] = None, -) -> None: +async def async_process_ha_core_config(hass: HomeAssistant, config: Dict) -> None: """Process the [homeassistant] section from the configuration. This method is a coroutine. @@ -486,14 +471,6 @@ async def async_process_ha_core_config( if auth_conf is None: auth_conf = [{"type": "homeassistant"}] - if api_password: - auth_conf.append( - {"type": "legacy_api_password", "api_password": api_password} - ) - if trusted_networks: - auth_conf.append( - {"type": "trusted_networks", "trusted_networks": trusted_networks} - ) mfa_conf = config.get( CONF_AUTH_MFA_MODULES, diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 8a40cff1bd5..aee15d6c0ce 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -15,7 +15,7 @@ from homeassistant.setup import async_setup_component, async_process_deps_reqs from homeassistant.util.decorator import Registry from homeassistant.helpers import entity_registry -# mypy: allow-untyped-defs +# mypy: allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) _UNDEF = object() @@ -337,7 +337,7 @@ class ConfigEntry: return False if result: # pylint: disable=protected-access - hass.config_entries._async_schedule_save() # type: ignore + hass.config_entries._async_schedule_save() return result except Exception: # pylint: disable=broad-except _LOGGER.exception( @@ -676,7 +676,7 @@ async def _old_conf_migrator(old_config): class ConfigFlow(data_entry_flow.FlowHandler): """Base class for config flows with some helpers.""" - def __init_subclass__(cls, domain=None, **kwargs): + def __init_subclass__(cls, domain: Optional[str] = None, **kwargs: Any) -> None: """Initialize a subclass, register if possible.""" super().__init_subclass__(**kwargs) # type: ignore if domain is not None: diff --git a/homeassistant/const.py b/homeassistant/const.py index 8c7299e2962..f6f1a4f2de2 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,10 +1,10 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -MINOR_VERSION = 100 -PATCH_VERSION = "3" +MINOR_VERSION = 101 +PATCH_VERSION = "0" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) -REQUIRED_PYTHON_VER = (3, 6, 0) +REQUIRED_PYTHON_VER = (3, 6, 1) # Format for platform files PLATFORM_FORMAT = "{platform}.{domain}" @@ -124,6 +124,7 @@ CONF_RECIPIENT = "recipient" CONF_REGION = "region" CONF_RESOURCE = "resource" CONF_RESOURCES = "resources" +CONF_RESOURCE_TEMPLATE = "resource_template" CONF_RGB = "rgb" CONF_ROOM = "room" CONF_SCAN_INTERVAL = "scan_interval" @@ -451,7 +452,6 @@ HTTP_SERVICE_UNAVAILABLE = 503 HTTP_BASIC_AUTHENTICATION = "basic" HTTP_DIGEST_AUTHENTICATION = "digest" -HTTP_HEADER_HA_AUTH = "X-HA-access" HTTP_HEADER_X_REQUESTED_WITH = "X-Requested-With" CONTENT_TYPE_JSON = "application/json" diff --git a/homeassistant/core.py b/homeassistant/core.py index feb4445d36d..ec11b14edaa 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -77,7 +77,8 @@ from homeassistant.util.unit_system import ( # NOQA # Typing imports that create a circular dependency # pylint: disable=using-constant-test if TYPE_CHECKING: - from homeassistant.config_entries import ConfigEntries # noqa + from homeassistant.config_entries import ConfigEntries + from homeassistant.components.http import HomeAssistantHTTP # pylint: disable=invalid-name T = TypeVar("T") @@ -162,6 +163,9 @@ class CoreState(enum.Enum): class HomeAssistant: """Root object of the Home Assistant home automation.""" + http: "HomeAssistantHTTP" = None # type: ignore + config_entries: "ConfigEntries" = None # type: ignore + def __init__(self, loop: Optional[asyncio.events.AbstractEventLoop] = None) -> None: """Initialize new Home Assistant object.""" self.loop: asyncio.events.AbstractEventLoop = (loop or asyncio.get_event_loop()) @@ -186,7 +190,6 @@ class HomeAssistant: self.data: dict = {} self.state = CoreState.not_running self.exit_code = 0 - self.config_entries: Optional[ConfigEntries] = None # If not None, use to signal end-of-loop self._stopped: Optional[asyncio.Event] = None diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 0bc27498f76..c06c69d9213 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -168,7 +168,7 @@ class FlowHandler: """Handle the configuration flow of a component.""" # Set by flow manager - flow_id: Optional[str] = None + flow_id: str = None # type: ignore hass: Optional[HomeAssistant] = None handler: Optional[Hashable] = None cur_step: Optional[Dict[str, str]] = None diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 21f57934e95..4668528fedb 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -6,12 +6,15 @@ To update, run python3 -m script.hassfest # fmt: off FLOWS = [ + "abode", "adguard", + "airly", "ambiclimate", "ambient_station", "axis", "cast", "cert_expiry", + "coolmaster", "daikin", "deconz", "dialogflow", @@ -20,6 +23,7 @@ FLOWS = [ "esphome", "geofency", "geonetnz_quakes", + "glances", "gpslogger", "hangouts", "heos", @@ -42,8 +46,10 @@ FLOWS = [ "met", "mobile_app", "mqtt", + "neato", "nest", "notion", + "opentherm_gw", "openuv", "owntracks", "plaato", @@ -55,6 +61,7 @@ FLOWS = [ "smartthings", "smhi", "solaredge", + "solarlog", "soma", "somfy", "sonos", diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index 922878fb324..7a1512957a2 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -3,7 +3,7 @@ from typing import Callable, Awaitable, Union from homeassistant import config_entries from .typing import HomeAssistantType -# mypy: allow-untyped-defs +# mypy: allow-untyped-defs, no-check-untyped-defs DiscoveryFunctionType = Callable[[], Union[Awaitable[bool], bool]] @@ -38,7 +38,7 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow): if user_input is None: return self.async_show_form(step_id="confirm") - if ( + if ( # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context and self.context.get("source") != config_entries.SOURCE_DISCOVERY ): @@ -124,7 +124,10 @@ class WebhookFlowHandler(config_entries.ConfigFlow): webhook_id = self.hass.components.webhook.async_generate_id() - if self.hass.components.cloud.async_active_subscription(): + if ( + "cloud" in self.hass.config.components + and self.hass.components.cloud.async_active_subscription() + ): webhook_url = await self.hass.components.cloud.async_create_cloudhook( webhook_id ) diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py new file mode 100644 index 00000000000..7fb954378ee --- /dev/null +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -0,0 +1,420 @@ +"""Config Flow using OAuth2. + +This module exists of the following parts: + - OAuth2 config flow which supports multiple OAuth2 implementations + - OAuth2 implementation that works with local provided client ID/secret + +""" +import asyncio +from abc import ABCMeta, ABC, abstractmethod +import logging +from typing import Optional, Any, Dict, cast +import time + +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 .aiohttp_client import async_get_clientsession + + +DATA_JWT_SECRET = "oauth2_jwt_secret" +DATA_VIEW_REGISTERED = "oauth2_view_reg" +DATA_IMPLEMENTATIONS = "oauth2_impl" +AUTH_CALLBACK_PATH = "/auth/external/callback" + + +class AbstractOAuth2Implementation(ABC): + """Base class to abstract OAuth2 authentication.""" + + @property + @abstractmethod + def name(self) -> str: + """Name of the implementation.""" + + @property + @abstractmethod + def domain(self) -> str: + """Domain that is providing the implementation.""" + + @abstractmethod + async def async_generate_authorize_url(self, flow_id: str) -> str: + """Generate a url for the user to authorize. + + This step is called when a config flow is initialized. It should redirect the + user to the vendor website where they can authorize Home Assistant. + + The implementation is responsible to get notified when the user is authorized + and pass this to the specified config flow. Do as little work as possible once + notified. You can do the work inside async_resolve_external_data. This will + give the best UX. + + Pass external data in with: + + ```python + await hass.config_entries.flow.async_configure( + flow_id=flow_id, user_input=external_data + ) + ``` + """ + + @abstractmethod + async def async_resolve_external_data(self, external_data: Any) -> dict: + """Resolve external data to tokens. + + Turn the data that the implementation passed to the config flow as external + step data into tokens. These tokens will be stored as 'token' in the + config entry data. + """ + + async def async_refresh_token(self, token: dict) -> dict: + """Refresh a token and update expires info.""" + new_token = await self._async_refresh_token(token) + new_token["expires_at"] = time.time() + new_token["expires_in"] + return new_token + + @abstractmethod + async def _async_refresh_token(self, token: dict) -> dict: + """Refresh a token.""" + + +class LocalOAuth2Implementation(AbstractOAuth2Implementation): + """Local OAuth2 implementation.""" + + def __init__( + self, + hass: HomeAssistant, + domain: str, + client_id: str, + client_secret: str, + authorize_url: str, + token_url: str, + ): + """Initialize local auth implementation.""" + self.hass = hass + self._domain = domain + self.client_id = client_id + self.client_secret = client_secret + self.authorize_url = authorize_url + self.token_url = token_url + + @property + def name(self) -> str: + """Name of the implementation.""" + return "Configuration.yaml" + + @property + def domain(self) -> str: + """Domain providing the implementation.""" + return self._domain + + @property + def redirect_uri(self) -> str: + """Return the redirect uri.""" + return f"{self.hass.config.api.base_url}{AUTH_CALLBACK_PATH}" # type: ignore + + async def async_generate_authorize_url(self, flow_id: str) -> str: + """Generate a url for the user to authorize.""" + return str( + URL(self.authorize_url).with_query( + { + "response_type": "code", + "client_id": self.client_id, + "redirect_uri": self.redirect_uri, + "state": _encode_jwt(self.hass, {"flow_id": flow_id}), + } + ) + ) + + async def async_resolve_external_data(self, external_data: Any) -> dict: + """Resolve the authorization code to tokens.""" + return await self._token_request( + { + "grant_type": "authorization_code", + "code": external_data, + "redirect_uri": self.redirect_uri, + } + ) + + async def _async_refresh_token(self, token: dict) -> dict: + """Refresh tokens.""" + new_token = await self._token_request( + { + "grant_type": "refresh_token", + "client_id": self.client_id, + "refresh_token": token["refresh_token"], + } + ) + return {**token, **new_token} + + async def _token_request(self, data: dict) -> dict: + """Make a token request.""" + session = async_get_clientsession(self.hass) + + data["client_id"] = self.client_id + + if self.client_secret is not None: + data["client_secret"] = self.client_secret + + resp = await session.post(self.token_url, data=data) + resp.raise_for_status() + return cast(dict, await resp.json()) + + +class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): + """Handle a config flow.""" + + DOMAIN = "" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_UNKNOWN + + def __init__(self) -> None: + """Instantiate config flow.""" + if self.DOMAIN == "": + raise TypeError( + f"Can't instantiate class {self.__class__.__name__} without DOMAIN being set" + ) + + self.external_data: Any = None + self.flow_impl: AbstractOAuth2Implementation = None # type: ignore + + @property + @abstractmethod + def logger(self) -> logging.Logger: + """Return logger.""" + + @property + def extra_authorize_data(self) -> dict: + """Extra data that needs to be appended to the authorize url.""" + return {} + + async def async_step_pick_implementation(self, user_input: dict = None) -> dict: + """Handle a flow start.""" + assert self.hass + implementations = await async_get_implementations(self.hass, self.DOMAIN) + + if user_input is not None: + self.flow_impl = implementations[user_input["implementation"]] + return await self.async_step_auth() + + if not implementations: + return self.async_abort(reason="missing_configuration") + + if len(implementations) == 1: + # Pick first implementation as we have only one. + self.flow_impl = list(implementations.values())[0] + return await self.async_step_auth() + + return self.async_show_form( + step_id="pick_implementation", + data_schema=vol.Schema( + { + vol.Required( + "implementation", default=list(implementations.keys())[0] + ): vol.In({key: impl.name for key, impl in implementations.items()}) + } + ), + ) + + async def async_step_auth(self, user_input: dict = None) -> dict: + """Create an entry for auth.""" + # Flow has been triggered by external data + if user_input: + self.external_data = user_input + return self.async_external_step_done(next_step_id="creation") + + try: + with async_timeout.timeout(10): + url = await self.flow_impl.async_generate_authorize_url(self.flow_id) + except asyncio.TimeoutError: + return self.async_abort(reason="authorize_url_timeout") + + url = str(URL(url).update_query(self.extra_authorize_data)) + + return self.async_external_step(step_id="auth", url=url) + + async def async_step_creation(self, user_input: dict = None) -> dict: + """Create config entry from external data.""" + token = await self.flow_impl.async_resolve_external_data(self.external_data) + token["expires_at"] = time.time() + token["expires_in"] + + self.logger.info("Successfully authenticated") + + return await self.async_oauth_create_entry( + {"auth_implementation": self.flow_impl.domain, "token": token} + ) + + async def async_oauth_create_entry(self, data: dict) -> dict: + """Create an entry for the flow. + + Ok to override if you want to fetch extra info or even add another step. + """ + return self.async_create_entry(title=self.flow_impl.name, data=data) + + async_step_user = async_step_pick_implementation + async_step_ssdp = async_step_pick_implementation + async_step_zeroconf = async_step_pick_implementation + async_step_homekit = async_step_pick_implementation + + @classmethod + def async_register_implementation( + cls, hass: HomeAssistant, local_impl: LocalOAuth2Implementation + ) -> None: + """Register a local implementation.""" + async_register_implementation(hass, cls.DOMAIN, local_impl) + + +@callback +def async_register_implementation( + hass: HomeAssistant, domain: str, implementation: AbstractOAuth2Implementation +) -> None: + """Register an OAuth2 flow implementation for an integration.""" + if isinstance(implementation, LocalOAuth2Implementation) and not hass.data.get( + DATA_VIEW_REGISTERED, False + ): + hass.http.register_view(OAuth2AuthorizeCallbackView()) # type: ignore + hass.data[DATA_VIEW_REGISTERED] = True + + implementations = hass.data.setdefault(DATA_IMPLEMENTATIONS, {}) + implementations.setdefault(domain, {})[implementation.domain] = implementation + + +async def async_get_implementations( + hass: HomeAssistant, domain: str +) -> Dict[str, AbstractOAuth2Implementation]: + """Return OAuth2 implementations for specified domain.""" + return cast( + Dict[str, AbstractOAuth2Implementation], + hass.data.setdefault(DATA_IMPLEMENTATIONS, {}).get(domain, {}), + ) + + +async def async_get_config_entry_implementation( + hass: HomeAssistant, config_entry: config_entries.ConfigEntry +) -> AbstractOAuth2Implementation: + """Return the implementation for this config entry.""" + implementations = await async_get_implementations(hass, config_entry.domain) + implementation = implementations.get(config_entry.data["auth_implementation"]) + + if implementation is None: + raise ValueError("Implementation not available") + + return implementation + + +class OAuth2AuthorizeCallbackView(HomeAssistantView): + """OAuth2 Authorization Callback View.""" + + requires_auth = False + url = AUTH_CALLBACK_PATH + name = "auth:external:callback" + + async def get(self, request: web.Request) -> web.Response: + """Receive authorization code.""" + if "code" not in request.query or "state" not in request.query: + return web.Response( + text=f"Missing code or state parameter in {request.url}" + ) + + hass = request.app["hass"] + + state = _decode_jwt(hass, request.query["state"]) + + if state is None: + return web.Response(text=f"Invalid state") + + await hass.config_entries.flow.async_configure( + flow_id=state["flow_id"], user_input=request.query["code"] + ) + + return web.Response( + headers={"content-type": "text/html"}, + text="", + ) + + +class OAuth2Session: + """Session to make requests authenticated with OAuth2.""" + + def __init__( + self, + hass: HomeAssistant, + config_entry: config_entries.ConfigEntry, + implementation: AbstractOAuth2Implementation, + ): + """Initialize an OAuth2 session.""" + self.hass = hass + self.config_entry = config_entry + self.implementation = implementation + + async def async_ensure_token_valid(self) -> None: + """Ensure that the current token is valid.""" + token = self.config_entry.data["token"] + + if token["expires_at"] > time.time(): + return + + new_token = await self.implementation.async_refresh_token(token) + + self.hass.config_entries.async_update_entry( # type: ignore + self.config_entry, data={**self.config_entry.data, "token": new_token} + ) + + async def async_request( + self, method: str, url: str, **kwargs: Any + ) -> client.ClientResponse: + """Make a request.""" + await self.async_ensure_token_valid() + return await async_oauth2_request( + self.hass, self.config_entry.data["token"], method, url, **kwargs + ) + + +async def async_oauth2_request( + hass: HomeAssistant, token: dict, method: str, url: str, **kwargs: Any +) -> client.ClientResponse: + """Make an OAuth2 authenticated request. + + This method will not refresh tokens. Use OAuth2 session for that. + """ + session = async_get_clientsession(hass) + + return await session.request( + method, + url, + **kwargs, + headers={ + **(kwargs.get("headers") or {}), + "authorization": f"Bearer {token['access_token']}", + }, + ) + + +@callback +def _encode_jwt(hass: HomeAssistant, data: dict) -> str: + """JWT encode data.""" + secret = hass.data.get(DATA_JWT_SECRET) + + if secret is None: + secret = hass.data[DATA_JWT_SECRET] = generate_secret() + + return jwt.encode(data, secret, algorithm="HS256").decode() + + +@callback +def _decode_jwt(hass: HomeAssistant, encoded: str) -> Optional[dict]: + """JWT encode data.""" + secret = cast(str, hass.data.get(DATA_JWT_SECRET)) + + try: + return jwt.decode(encoded, secret, algorithms=["HS256"]) + except jwt.InvalidTokenError: + return None diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 2d1bb89d23a..7ca5a7e86f9 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -386,6 +386,7 @@ def remove_falsy(value: List[T]) -> List[T]: def service(value): """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 raise vol.Invalid("Service {} does not match format .".format(value)) @@ -600,7 +601,8 @@ def deprecated( if module is not None: module_name = module.__name__ else: - # Unclear when it is None, but it happens, so let's guard. + # If Python is unable to access the sources files, the call stack frame + # will be missing information, so let's guard. # https://github.com/home-assistant/home-assistant/issues/24982 module_name = __name__ @@ -884,6 +886,8 @@ DEVICE_ACTION_BASE_SCHEMA = vol.Schema( DEVICE_ACTION_SCHEMA = DEVICE_ACTION_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA) +_SCRIPT_SCENE_SCHEMA = vol.Schema({vol.Required("scene"): entity_domain("scene")}) + SCRIPT_SCHEMA = vol.All( ensure_list, [ @@ -894,6 +898,7 @@ SCRIPT_SCHEMA = vol.All( EVENT_SCHEMA, CONDITION_SCHEMA, DEVICE_ACTION_SCHEMA, + _SCRIPT_SCENE_SCHEMA, ) ], ) diff --git a/homeassistant/helpers/deprecation.py b/homeassistant/helpers/deprecation.py index 881534b5bed..2a4fafde75b 100644 --- a/homeassistant/helpers/deprecation.py +++ b/homeassistant/helpers/deprecation.py @@ -54,7 +54,15 @@ def get_deprecated( and a warning is issued to the user. """ if old_name in config: - module_name = inspect.getmodule(inspect.stack()[1][0]).__name__ # type: ignore + module = inspect.getmodule(inspect.stack()[1][0]) + if module is not None: + module_name = module.__name__ + else: + # If Python is unable to access the sources files, the call stack frame + # will be missing information, so let's guard. + # https://github.com/home-assistant/home-assistant/issues/24982 + module_name = __name__ + logger = logging.getLogger(module_name) logger.warning( "'%s' is deprecated. Please rename '%s' to '%s' in your " diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 836ad954ae0..0d2182f88e1 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -148,7 +148,8 @@ class Entity: def state_attributes(self) -> Optional[Dict[str, Any]]: """Return the state attributes. - Implemented by component base class. + Implemented by component base class. Convention for attribute names + is lowercase snake_case. """ return None @@ -156,7 +157,8 @@ class Entity: def device_state_attributes(self) -> Optional[Dict[str, Any]]: """Return device specific state attributes. - Implemented by platform classes. + Implemented by platform classes. Convention for attribute names + is lowercase snake_case. """ return None @@ -551,6 +553,19 @@ class Entity: """Return the representation.""" return "".format(self.name, self.state) + # call an requests + async def async_request_call(self, coro): + """Process request batched.""" + + if self.parallel_updates: + await self.parallel_updates.acquire() + + try: + await coro + finally: + if self.parallel_updates: + self.parallel_updates.release() + class ToggleEntity(Entity): """An abstract class for entities that can be turned on and off.""" diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index b7707b844d4..e819da9873a 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -1,13 +1,13 @@ """Helpers for listening to events.""" from datetime import datetime, timedelta import functools as ft -from typing import Callable +from typing import Any, Callable, Iterable, Optional, Union import attr from homeassistant.loader import bind_hass from homeassistant.helpers.sun import get_astral_event_next -from homeassistant.core import HomeAssistant, callback, CALLBACK_TYPE +from homeassistant.core import HomeAssistant, callback, CALLBACK_TYPE, Event from homeassistant.const import ( ATTR_NOW, EVENT_STATE_CHANGED, @@ -240,7 +240,9 @@ track_point_in_utc_time = threaded_listener_factory(async_track_point_in_utc_tim @callback @bind_hass -def async_call_later(hass, delay, action): +def async_call_later( + hass: HomeAssistant, delay: float, action: Callable[..., None] +) -> CALLBACK_TYPE: """Add a listener that is called in .""" return async_track_point_in_utc_time( hass, action, dt_util.utcnow() + timedelta(seconds=delay) @@ -252,7 +254,9 @@ call_later = threaded_listener_factory(async_call_later) @callback @bind_hass -def async_track_time_interval(hass, action, interval): +def async_track_time_interval( + hass: HomeAssistant, action: Callable[..., None], interval: timedelta +) -> CALLBACK_TYPE: """Add a listener that fires repetitively at every timedelta interval.""" remove = None @@ -284,14 +288,14 @@ class SunListener: """Helper class to help listen to sun events.""" hass = attr.ib(type=HomeAssistant) - action = attr.ib(type=Callable) - event = attr.ib(type=str) - offset = attr.ib(type=timedelta) - _unsub_sun: CALLBACK_TYPE = attr.ib(default=None) - _unsub_config: CALLBACK_TYPE = attr.ib(default=None) + action: Callable[..., None] = attr.ib() + event: str = attr.ib() + offset: Optional[timedelta] = attr.ib() + _unsub_sun: Optional[CALLBACK_TYPE] = attr.ib(default=None) + _unsub_config: Optional[CALLBACK_TYPE] = attr.ib(default=None) @callback - def async_attach(self): + def async_attach(self) -> None: """Attach a sun listener.""" assert self._unsub_config is None @@ -302,7 +306,7 @@ class SunListener: self._listen_next_sun_event() @callback - def async_detach(self): + def async_detach(self) -> None: """Detach the sun listener.""" assert self._unsub_sun is not None assert self._unsub_config is not None @@ -313,7 +317,7 @@ class SunListener: self._unsub_config = None @callback - def _listen_next_sun_event(self): + def _listen_next_sun_event(self) -> None: """Set up the sun event listener.""" assert self._unsub_sun is None @@ -324,14 +328,14 @@ class SunListener: ) @callback - def _handle_sun_event(self, _now): + def _handle_sun_event(self, _now: Any) -> None: """Handle solar event.""" self._unsub_sun = None self._listen_next_sun_event() self.hass.async_run_job(self.action) @callback - def _handle_config_event(self, _event): + def _handle_config_event(self, _event: Any) -> None: """Handle core config update.""" assert self._unsub_sun is not None self._unsub_sun() @@ -341,7 +345,9 @@ class SunListener: @callback @bind_hass -def async_track_sunrise(hass, action, offset=None): +def async_track_sunrise( + hass: HomeAssistant, action: Callable[..., None], offset: Optional[timedelta] = None +) -> CALLBACK_TYPE: """Add a listener that will fire a specified offset from sunrise daily.""" listener = SunListener(hass, action, SUN_EVENT_SUNRISE, offset) listener.async_attach() @@ -353,7 +359,9 @@ track_sunrise = threaded_listener_factory(async_track_sunrise) @callback @bind_hass -def async_track_sunset(hass, action, offset=None): +def async_track_sunset( + hass: HomeAssistant, action: Callable[..., None], offset: Optional[timedelta] = None +) -> CALLBACK_TYPE: """Add a listener that will fire a specified offset from sunset daily.""" listener = SunListener(hass, action, SUN_EVENT_SUNSET, offset) listener.async_attach() @@ -366,8 +374,13 @@ track_sunset = threaded_listener_factory(async_track_sunset) @callback @bind_hass def async_track_utc_time_change( - hass, action, hour=None, minute=None, second=None, local=False -): + hass: HomeAssistant, + action: Callable[..., None], + hour: Optional[Any] = None, + minute: Optional[Any] = None, + second: Optional[Any] = None, + local: bool = False, +) -> CALLBACK_TYPE: """Add a listener that will fire if time matches a pattern.""" # We do not have to wrap the function with time pattern matching logic # if no pattern given @@ -386,7 +399,7 @@ def async_track_utc_time_change( next_time = None - def calculate_next(now): + def calculate_next(now: datetime) -> None: """Calculate and set the next time the trigger should fire.""" nonlocal next_time @@ -397,10 +410,10 @@ def async_track_utc_time_change( # Make sure rolling back the clock doesn't prevent the timer from # triggering. - last_now = None + last_now: Optional[datetime] = None @callback - def pattern_time_change_listener(event): + def pattern_time_change_listener(event: Event) -> None: """Listen for matching time_changed events.""" nonlocal next_time, last_now @@ -427,7 +440,13 @@ track_utc_time_change = threaded_listener_factory(async_track_utc_time_change) @callback @bind_hass -def async_track_time_change(hass, action, hour=None, minute=None, second=None): +def async_track_time_change( + hass: HomeAssistant, + action: Callable[..., None], + hour: Optional[Any] = None, + minute: Optional[Any] = None, + second: Optional[Any] = None, +) -> CALLBACK_TYPE: """Add a listener that will fire if UTC time matches a pattern.""" return async_track_utc_time_change(hass, action, hour, minute, second, local=True) @@ -435,7 +454,9 @@ def async_track_time_change(hass, action, hour=None, minute=None, second=None): track_time_change = threaded_listener_factory(async_track_time_change) -def _process_state_match(parameter): +def _process_state_match( + parameter: Union[None, str, Iterable[str]] +) -> Callable[[str], bool]: """Convert parameter to function that matches input against parameter.""" if parameter is None or parameter == MATCH_ALL: return lambda _: True @@ -443,5 +464,5 @@ def _process_state_match(parameter): if isinstance(parameter, str) or not hasattr(parameter, "__iter__"): return lambda state: state == parameter - parameter = tuple(parameter) - return lambda state: state in parameter + parameter_tuple = tuple(parameter) + return lambda state: state in parameter_tuple diff --git a/homeassistant/helpers/restore_state.py b/homeassistant/helpers/restore_state.py index fdf52c99075..5d47f34b002 100644 --- a/homeassistant/helpers/restore_state.py +++ b/homeassistant/helpers/restore_state.py @@ -164,23 +164,20 @@ class RestoreStateData: @callback def async_setup_dump(self, *args: Any) -> None: """Set up the restore state listeners.""" + + def _async_dump_states(*_: Any) -> None: + self.hass.async_create_task(self.async_dump_states()) + # Dump the initial states now. This helps minimize the risk of having # old states loaded by overwritting the last states once home assistant # has started and the old states have been read. - self.hass.async_create_task(self.async_dump_states()) + _async_dump_states() # Dump states periodically - async_track_time_interval( - self.hass, - lambda *_: self.hass.async_create_task(self.async_dump_states()), - STATE_DUMP_INTERVAL, - ) + async_track_time_interval(self.hass, _async_dump_states, STATE_DUMP_INTERVAL) # Dump states when stopping hass - self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, - lambda *_: self.hass.async_create_task(self.async_dump_states()), - ) + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_dump_states) @callback def async_restore_entity_added(self, entity_id: str) -> None: diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 05b28102726..1e65c24eaaf 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -9,12 +9,15 @@ from typing import Optional, Sequence, Callable, Dict, List, Set, Tuple, Any import voluptuous as vol 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, CONF_DEVICE_ID, CONF_DOMAIN, CONF_TIMEOUT, + SERVICE_TURN_ON, ) from homeassistant import exceptions from homeassistant.helpers import ( @@ -46,6 +49,7 @@ CONF_EVENT_DATA_TEMPLATE = "event_data_template" CONF_DELAY = "delay" CONF_WAIT_TEMPLATE = "wait_template" CONF_CONTINUE = "continue_on_timeout" +CONF_SCENE = "scene" ACTION_DELAY = "delay" @@ -54,6 +58,7 @@ ACTION_CHECK_CONDITION = "condition" ACTION_FIRE_EVENT = "event" ACTION_CALL_SERVICE = "call_service" ACTION_DEVICE_AUTOMATION = "device" +ACTION_ACTIVATE_SCENE = "scene" def _determine_action(action): @@ -73,6 +78,9 @@ def _determine_action(action): if CONF_DEVICE_ID in action: return ACTION_DEVICE_AUTOMATION + if CONF_SCENE in action: + return ACTION_ACTIVATE_SCENE + return ACTION_CALL_SERVICE @@ -147,6 +155,7 @@ class Script: ACTION_FIRE_EVENT: self._async_fire_event, ACTION_CALL_SERVICE: self._async_call_service, ACTION_DEVICE_AUTOMATION: self._async_device_automation, + ACTION_ACTIVATE_SCENE: self._async_activate_scene, } @property @@ -362,6 +371,21 @@ class Script: self.hass, action, variables, context ) + async def _async_activate_scene(self, action, variables, context): + """Activate the scene specified in the action. + + This method is a coroutine. + """ + self.last_action = action.get(CONF_ALIAS, "activate scene") + self._log("Executing step %s" % self.last_action) + await self.hass.services.async_call( + scene.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: action[CONF_SCENE]}, + blocking=True, + context=context, + ) + async def _async_fire_event(self, action, variables, context): """Fire an event.""" self.last_action = action.get(CONF_ALIAS, action[CONF_EVENT]) diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index 2f49a566a32..4cb7fb85bff 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -11,11 +11,9 @@ from homeassistant.loader import bind_hass, async_get_integration, IntegrationNo import homeassistant.util.dt as dt_util from homeassistant.components.notify import ATTR_MESSAGE, SERVICE_NOTIFY from homeassistant.components.sun import STATE_ABOVE_HORIZON, STATE_BELOW_HORIZON -from homeassistant.components.mysensors.switch import ATTR_IR_CODE, SERVICE_SEND_IR_CODE from homeassistant.components.cover import ATTR_POSITION, ATTR_TILT_POSITION from homeassistant.const import ( ATTR_ENTITY_ID, - ATTR_OPTION, SERVICE_ALARM_ARM_AWAY, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_DISARM, @@ -41,7 +39,6 @@ from homeassistant.const import ( STATE_OPEN, STATE_UNKNOWN, STATE_UNLOCKED, - SERVICE_SELECT_OPTION, ) from homeassistant.core import Context, State, DOMAIN as HASS_DOMAIN from .typing import HomeAssistantType @@ -54,8 +51,6 @@ GROUP_DOMAIN = "group" # Each item is a service with a list of required attributes. SERVICE_ATTRIBUTES = { SERVICE_NOTIFY: [ATTR_MESSAGE], - SERVICE_SEND_IR_CODE: [ATTR_IR_CODE], - SERVICE_SELECT_OPTION: [ATTR_OPTION], SERVICE_SET_COVER_POSITION: [ATTR_POSITION], SERVICE_SET_COVER_TILT_POSITION: [ATTR_TILT_POSITION], } diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index cd99a47cf57..bd18eebfb25 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -6,13 +6,14 @@ import os from typing import Dict, List, Optional, Callable, Union, Any, Type from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant, callback, CALLBACK_TYPE 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 STORAGE_DIR = ".storage" _LOGGER = logging.getLogger(__name__) @@ -71,8 +72,8 @@ class Store: self.hass = hass self._private = private self._data: Optional[Dict[str, Any]] = None - self._unsub_delay_listener = None - self._unsub_stop_listener = None + self._unsub_delay_listener: Optional[CALLBACK_TYPE] = None + self._unsub_stop_listener: Optional[CALLBACK_TYPE] = None self._write_lock = asyncio.Lock() self._load_task: Optional[asyncio.Future] = None self._encoder = encoder @@ -136,9 +137,7 @@ class Store: await self._async_handle_write_data() @callback - def async_delay_save( - self, data_func: Callable[[], Dict], delay: Optional[int] = None - ) -> None: + def async_delay_save(self, data_func: Callable[[], Dict], delay: float = 0) -> None: """Save data with an optional delay.""" self._data = {"version": self.version, "key": self.key, "data_func": data_func} diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 9af1998e894..1d9ca691451 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -884,6 +884,16 @@ def ordinal(value): ) +def from_json(value): + """Convert a JSON string to an object.""" + return json.loads(value) + + +def to_json(value): + """Convert an object to a JSON string.""" + return json.dumps(value) + + @contextfilter def random_every_time(context, values): """Choose a random value. @@ -916,6 +926,8 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): self.filters["timestamp_custom"] = timestamp_custom self.filters["timestamp_local"] = timestamp_local self.filters["timestamp_utc"] = timestamp_utc + self.filters["to_json"] = to_json + self.filters["from_json"] = from_json self.filters["is_defined"] = fail_when_undefined self.filters["max"] = max self.filters["min"] = min diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 19acae64b16..85bb00ce6eb 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -6,22 +6,22 @@ astral==1.10.1 async_timeout==3.0.1 attrs==19.2.0 bcrypt==3.1.7 -certifi>=2019.6.16 +certifi>=2019.9.11 contextvars==2.4;python_version<"3.7" -cryptography==2.7 +cryptography==2.8 distro==1.4.0 hass-nabucasa==0.22 -home-assistant-frontend==20191002.2 +home-assistant-frontend==20191025.1 importlib-metadata==0.23 jinja2>=2.10.1 netdisco==2.6.0 pip>=8.0.3 -python-slugify==3.0.4 -pytz>=2019.02 +python-slugify==3.0.6 +pytz>=2019.03 pyyaml==5.1.2 requests==2.22.0 ruamel.yaml==0.15.100 -sqlalchemy==1.3.8 +sqlalchemy==1.3.10 voluptuous-serialize==2.3.0 voluptuous==0.11.7 zeroconf==0.23.0 @@ -33,6 +33,3 @@ enum34==1000000000.0.0 # This is a old unmaintained library and is replaced with pycryptodome pycrypto==1000000000.0.0 - -# Contains code to modify Home Assistant to work around our rules -python-systemair-savecair==1000000000.0.0 diff --git a/homeassistant/scripts/keyring.py b/homeassistant/scripts/keyring.py index 6ca422b595b..0c5623a50ad 100644 --- a/homeassistant/scripts/keyring.py +++ b/homeassistant/scripts/keyring.py @@ -8,7 +8,7 @@ from homeassistant.util.yaml import _SECRET_NAMESPACE # mypy: allow-untyped-defs -REQUIREMENTS = ["keyring==17.1.1", "keyrings.alt==3.1.1"] +REQUIREMENTS = ["keyring==19.2.0", "keyrings.alt==3.1.1"] def run(args): diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py index 89d1dcfc4c1..640e5c5540a 100644 --- a/homeassistant/util/color.py +++ b/homeassistant/util/color.py @@ -167,8 +167,8 @@ COLORS = { class XYPoint: """Represents a CIE 1931 XY coordinate pair.""" - x = attr.ib(type=float) - y = attr.ib(type=float) + x = attr.ib(type=float) # pylint: disable=invalid-name + y = attr.ib(type=float) # pylint: disable=invalid-name @attr.s() diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index a948c4407ae..1abb4294398 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -220,7 +220,7 @@ def get_age(date: dt.datetime) -> str: def parse_time_expression(parameter: Any, min_value: int, max_value: int) -> List[int]: """Parse the time expression part and return a list of times to match.""" if parameter is None or parameter == MATCH_ALL: - res = [x for x in range(min_value, max_value + 1)] + res = list(range(min_value, max_value + 1)) elif isinstance(parameter, str) and parameter.startswith("/"): parameter = int(parameter[1:]) res = [x for x in range(min_value, max_value + 1) if x % parameter == 0] diff --git a/homeassistant/util/location.py b/homeassistant/util/location.py index f81c40a52bb..7c61a8ab1e9 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, cast +from typing import Any, Optional, Tuple, Dict import aiohttp @@ -159,7 +159,7 @@ def vincenty( if miles: s *= MILES_PER_KILOMETER # kilometers to miles - return round(cast(float, s), 6) + return round(s, 6) async def _get_ipapi(session: aiohttp.ClientSession) -> Optional[Dict[str, Any]]: diff --git a/homeassistant/util/logging.py b/homeassistant/util/logging.py index 99e606d2866..de04f23d9dd 100644 --- a/homeassistant/util/logging.py +++ b/homeassistant/util/logging.py @@ -130,7 +130,15 @@ def catch_log_exception( """Decorate a callback to catch and log exceptions.""" def log_exception(*args: Any) -> None: - module_name = inspect.getmodule(inspect.trace()[1][0]).__name__ # type: ignore + module = inspect.getmodule(inspect.stack()[1][0]) + if module is not None: + module_name = module.__name__ + else: + # If Python is unable to access the sources files, the call stack frame + # will be missing information, so let's guard. + # https://github.com/home-assistant/home-assistant/issues/24982 + module_name = __name__ + # Do not print the wrapper in the traceback frames = len(inspect.trace()) - 1 exc_msg = traceback.format_exc(-frames) @@ -178,9 +186,15 @@ def catch_log_coro_exception( try: return await target except Exception: # pylint: disable=broad-except - module_name = inspect.getmodule( # type: ignore - inspect.trace()[1][0] - ).__name__ + module = inspect.getmodule(inspect.stack()[1][0]) + if module is not None: + module_name = module.__name__ + else: + # If Python is unable to access the sources files, the frame + # will be missing information, so let's guard. + # https://github.com/home-assistant/home-assistant/issues/24982 + module_name = __name__ + # Do not print the wrapper in the traceback frames = len(inspect.trace()) - 1 exc_msg = traceback.format_exc(-frames) diff --git a/mypyrc b/mypyrc deleted file mode 100644 index 08413ecd23c..00000000000 --- a/mypyrc +++ /dev/null @@ -1,38 +0,0 @@ -homeassistant/*.py -homeassistant/auth/ -homeassistant/components/*.py -homeassistant/components/automation/ -homeassistant/components/binary_sensor/ -homeassistant/components/calendar/ -homeassistant/components/camera/ -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/ diff --git a/pylintrc b/pylintrc index bb4f1fe96d0..4aced384b63 100644 --- a/pylintrc +++ b/pylintrc @@ -1,8 +1,11 @@ [MASTER] ignore=tests +# Use a conservative default here; 2 should speed up most setups and not hurt +# any too bad. Override on command line as appropriate. +jobs=2 [BASIC] -good-names=i,j,k,ex,Run,_,fp +good-names=id,i,j,k,ex,Run,_,fp [MESSAGES CONTROL] # Reasons disabled: @@ -18,8 +21,8 @@ good-names=i,j,k,ex,Run,_,fp # too-few-* - same as too-many-* # abstract-method - with intro of async there are always methods missing # inconsistent-return-statements - doesn't handle raise -# not-an-iterable - https://github.com/PyCQA/pylint/issues/2311 # unnecessary-pass - readability for functions which only contain pass +# import-outside-toplevel - TODO disable= format, abstract-class-little-used, @@ -27,9 +30,9 @@ disable= cyclic-import, duplicate-code, global-statement, + import-outside-toplevel, inconsistent-return-statements, locally-disabled, - not-an-iterable, not-context-manager, redefined-variable-type, too-few-public-methods, diff --git a/requirements_all.txt b/requirements_all.txt index 448504eba5b..df9cafe7aa0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -4,15 +4,15 @@ astral==1.10.1 async_timeout==3.0.1 attrs==19.2.0 bcrypt==3.1.7 -certifi>=2019.6.16 +certifi>=2019.9.11 contextvars==2.4;python_version<"3.7" importlib-metadata==0.23 jinja2>=2.10.1 PyJWT==1.7.1 -cryptography==2.7 +cryptography==2.8 pip>=8.0.3 -python-slugify==3.0.4 -pytz>=2019.02 +python-slugify==3.0.6 +pytz>=2019.03 pyyaml==5.1.2 requests==2.22.0 ruamel.yaml==0.15.100 @@ -38,16 +38,16 @@ Adafruit-SHT31==1.0.2 HAP-python==2.6.0 # homeassistant.components.mastodon -Mastodon.py==1.4.6 +Mastodon.py==1.5.0 # homeassistant.components.orangepi_gpio -OPi.GPIO==0.3.6 +OPi.GPIO==0.4.0 # homeassistant.components.essent PyEssent==0.13 # homeassistant.components.github -PyGithub==1.43.5 +PyGithub==1.43.8 # homeassistant.components.isy994 PyISY==1.1.2 @@ -56,7 +56,7 @@ PyISY==1.1.2 PyMVGLive==1.1.4 # homeassistant.components.arduino -PyMata==2.14 +PyMata==2.20 # homeassistant.components.mobile_app # homeassistant.components.owntracks @@ -66,7 +66,7 @@ PyNaCl==1.3.0 PyQRCode==1.2.1 # homeassistant.components.rmvtransport -PyRMVtransport==0.1.3 +PyRMVtransport==0.2.9 # homeassistant.components.switchbot # PySwitchbot==0.6.2 @@ -82,10 +82,10 @@ PyXiaomiGateway==0.12.4 # homeassistant.components.mcp23017 # homeassistant.components.rpi_gpio -# RPi.GPIO==0.6.5 +# RPi.GPIO==0.7.0 # homeassistant.components.remember_the_milk -RtmAPI==0.7.0 +RtmAPI==0.7.2 # homeassistant.components.travisci TravisPy==0.3.5 @@ -103,7 +103,7 @@ WazeRouteCalculator==0.10 YesssSMS==0.4.1 # homeassistant.components.abode -abodepy==0.15.0 +abodepy==0.16.6 # homeassistant.components.mcp23017 adafruit-blinka==1.2.1 @@ -112,10 +112,10 @@ adafruit-blinka==1.2.1 adafruit-circuitpython-mcp230xx==1.1.2 # homeassistant.components.androidtv -adb-shell==0.0.4 +adb-shell==0.0.7 # homeassistant.components.adguard -adguardhome==0.2.1 +adguardhome==0.3.0 # homeassistant.components.frontier_silicon afsapi==0.0.4 @@ -139,7 +139,7 @@ aiobotocore==0.10.2 aiodns==2.0.0 # homeassistant.components.esphome -aioesphomeapi==2.2.0 +aioesphomeapi==2.4.2 # homeassistant.components.freebox aiofreepybox==0.0.8 @@ -184,6 +184,9 @@ aiounifi==11 # homeassistant.components.wwlln aiowwlln==2.0.2 +# homeassistant.components.airly +airly==0.0.2 + # homeassistant.components.aladdin_connect aladdin_connect==0.3 @@ -191,7 +194,7 @@ aladdin_connect==0.3 alarmdecoder==1.13.2 # homeassistant.components.alpha_vantage -alpha_vantage==2.1.0 +alpha_vantage==2.1.1 # homeassistant.components.ambiclimate ambiclimate==0.2.1 @@ -200,7 +203,7 @@ ambiclimate==0.2.1 amcrest==1.5.3 # homeassistant.components.androidtv -androidtv==0.0.30 +androidtv==0.0.32 # homeassistant.components.anel_pwrctrl anel_pwrctrl-homeassistant==0.0.1.dev2 @@ -214,6 +217,9 @@ apcaccess==0.0.13 # homeassistant.components.apns apns2==0.3.0 +# homeassistant.components.apprise +apprise==0.8.1 + # homeassistant.components.aprs aprslib==0.6.46 @@ -264,7 +270,7 @@ batinfo==0.4.2 # beacontools[scan]==1.2.3 # homeassistant.components.scrape -beautifulsoup4==4.8.0 +beautifulsoup4==4.8.1 # homeassistant.components.beewi_smartclim beewi_smartclim==0.0.7 @@ -279,7 +285,7 @@ bimmer_connected==0.6.0 bizkaibus==0.1.1 # homeassistant.components.blink -blinkpy==0.14.1 +blinkpy==0.14.2 # homeassistant.components.blinksticklight blinkstick==1.1.8 @@ -308,7 +314,7 @@ boto3==1.9.233 braviarc-homeassistant==0.3.7.dev0 # homeassistant.components.broadlink -broadlink==0.11.1 +broadlink==0.12.0 # homeassistant.components.brottsplatskartan brottsplatskartan==0.0.1 @@ -340,6 +346,9 @@ ciscosparkapi==0.4.2 # homeassistant.components.cppm_tracker clearpasspy==1.0.2 +# homeassistant.components.sinch +clx-sdk-xms==1.0.0 + # homeassistant.components.co2signal co2signal==0.4.2 @@ -399,7 +408,7 @@ directpy==0.5 discogs_client==2.2.1 # homeassistant.components.discord -discord.py==1.2.3 +discord.py==1.2.4 # homeassistant.components.updater distro==1.4.0 @@ -447,7 +456,7 @@ enocean==0.50 enturclient==0.2.0 # homeassistant.components.environment_canada -env_canada==0.0.25 +env_canada==0.0.27 # homeassistant.components.envirophat # envirophat==0.0.6 @@ -471,7 +480,7 @@ eternalegypt==0.0.10 # evdev==0.6.1 # homeassistant.components.evohome -evohome-async==0.3.3b4 +evohome-async==0.3.4b1 # homeassistant.components.dlib_face_detect # homeassistant.components.dlib_face_identify @@ -510,7 +519,7 @@ freesms==0.1.2 # homeassistant.components.fritz # homeassistant.components.fritzbox_callmonitor # homeassistant.components.fritzbox_netmonitor -# fritzconnection==0.6.5 +# fritzconnection==0.8.4 # homeassistant.components.fritzdect fritzhome==1.0.4 @@ -525,7 +534,7 @@ gearbest_parser==1.0.7 geizhals==0.0.9 # homeassistant.components.geniushub -geniushub-client==0.6.26 +geniushub-client==0.6.28 # homeassistant.components.geo_json_events # homeassistant.components.nsw_rural_fire_service_feed @@ -613,7 +622,7 @@ hass-nabucasa==0.22 hbmqtt==0.9.5 # homeassistant.components.jewish_calendar -hdate==0.9.0 +hdate==0.9.1 # homeassistant.components.heatmiser heatmiserV3==0.9.1 @@ -624,9 +633,6 @@ herepy==0.6.3.1 # homeassistant.components.hikvisioncam hikvision==0.4 -# homeassistant.components.hipchat -hipnotify==1.0.8 - # homeassistant.components.harman_kardon_avr hkavr==0.0.5 @@ -640,7 +646,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191002.2 +home-assistant-frontend==20191025.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 @@ -649,7 +655,7 @@ homeassistant-pyozw==0.1.4 homekit[IP]==0.15.0 # homeassistant.components.homematicip_cloud -homematicip==0.10.12 +homematicip==0.10.13 # homeassistant.components.horizon horimote==0.4.1 @@ -715,7 +721,7 @@ kaiterra-async-client==0.0.2 keba-kecontact==0.2.0 # homeassistant.scripts.keyring -keyring==17.1.1 +keyring==19.2.0 # homeassistant.scripts.keyring keyrings.alt==3.1.1 @@ -844,7 +850,7 @@ n26==0.2.7 nad_receiver==0.0.11 # homeassistant.components.keenetic_ndms2 -ndms2_client==0.0.9 +ndms2_client==0.0.10 # homeassistant.components.ness_alarm nessclient==0.9.15 @@ -878,7 +884,7 @@ nuheat==0.3.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.17.1 +numpy==1.17.3 # homeassistant.components.oasa_telematics oasatelematics==0.3 @@ -890,7 +896,7 @@ oauth2client==4.0.0 oemthermostat==1.1 # homeassistant.components.onkyo -onkyo-eiscp==1.2.4 +onkyo-eiscp==1.2.7 # homeassistant.components.onvif onvif-zeep-async==0.2.0 @@ -911,7 +917,10 @@ opensensemap-api==0.1.5 openwebifpy==3.1.1 # homeassistant.components.luci -openwrt-luci-rpc==1.1.1 +openwrt-luci-rpc==1.1.2 + +# homeassistant.components.oru +oru==0.1.9 # homeassistant.components.orvibo orvibo==1.1.1 @@ -953,7 +962,7 @@ pilight==0.1.1 # homeassistant.components.image_processing # homeassistant.components.proxy # homeassistant.components.qrcode -pillow==6.1.0 +pillow==6.2.0 # homeassistant.components.dominos pizzapi==0.0.3 @@ -962,7 +971,10 @@ pizzapi==0.0.3 plexapi==3.0.6 # homeassistant.components.plex -plexauth==0.0.4 +plexauth==0.0.5 + +# homeassistant.components.plex +plexwebsocket==0.0.3 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 @@ -1078,7 +1090,7 @@ pyalarmdotcom==0.3.2 pyarlo==0.2.3 # homeassistant.components.netatmo -pyatmo==2.2.1 +pyatmo==2.3.2 # homeassistant.components.atome pyatome==0.1.1 @@ -1096,7 +1108,7 @@ pyblackbird==0.5 # pybluez==0.22 # homeassistant.components.neato -pybotvac==0.0.15 +pybotvac==0.0.17 # homeassistant.components.nissan_leaf pycarwings2==2.9 @@ -1120,7 +1132,7 @@ pycomfoconnect==0.3 pycoolmasternet==0.0.4 # homeassistant.components.microsoft -pycsspeechtts==1.0.2 +pycsspeechtts==1.0.3 # homeassistant.components.cups # pycups==1.9.73 @@ -1132,7 +1144,7 @@ pydaikin==1.6.1 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==63 +pydeconz==64 # homeassistant.components.delijn pydelijn==0.5.1 @@ -1165,7 +1177,7 @@ pyeight==0.1.1 pyemby==1.6 # homeassistant.components.envisalink -pyenvisalink==3.8 +pyenvisalink==4.0 # homeassistant.components.ephember pyephember==0.2.0 @@ -1202,7 +1214,7 @@ pyfttt==0.3 # homeassistant.components.bluetooth_le_tracker # homeassistant.components.skybeacon -pygatt[GATTTOOL]==4.0.1 +pygatt[GATTTOOL]==4.0.5 # homeassistant.components.gogogate2 pygogogate2==0.1.1 @@ -1220,20 +1232,17 @@ pyhaversion==3.1.0 pyheos==0.6.0 # homeassistant.components.hikvision -pyhik==0.2.3 +pyhik==0.2.4 # homeassistant.components.hive -pyhiveapi==0.2.19.2 +pyhiveapi==0.2.19.3 # homeassistant.components.homematic -pyhomematic==0.1.60 +pyhomematic==0.1.61 # homeassistant.components.homeworks pyhomeworks==0.0.6 -# homeassistant.components.hydroquebec -pyhydroquebec==2.2.2 - # homeassistant.components.ialarm pyialarm==0.3 @@ -1298,7 +1307,7 @@ pymailgunner==1.4 pymediaroom==0.6.4 # homeassistant.components.somfy -pymfy==0.5.2 +pymfy==0.6.0 # homeassistant.components.xiaomi_tv pymitv==1.4.3 @@ -1312,6 +1321,9 @@ pymodbus==1.5.2 # homeassistant.components.monoprice pymonoprice==0.3 +# homeassistant.components.msteams +pymsteams==0.1.12 + # homeassistant.components.yamaha_musiccast pymusiccast==0.1.6 @@ -1364,7 +1376,7 @@ pyoppleio==1.0.5 pyota==2.0.5 # homeassistant.components.opentherm_gw -pyotgw==0.4b4 +pyotgw==0.5b0 # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp @@ -1390,7 +1402,7 @@ pypjlink2==1.2.0 pypoint==1.1.1 # homeassistant.components.ps4 -pyps4-homeassistant==0.8.7 +pyps4-2ndscreen==1.0.1 # homeassistant.components.qwikswitch pyqwikswitch==0.93 @@ -1411,7 +1423,7 @@ pyrepetier==3.0.5 pysabnzbd==1.1.0 # homeassistant.components.saj -pysaj==0.0.9 +pysaj==0.0.12 # homeassistant.components.sony_projector pysdcp==1 @@ -1450,7 +1462,7 @@ pysnmp==4.4.11 pysoma==0.0.10 # homeassistant.components.sonos -pysonos==0.0.23 +pysonos==0.0.24 # homeassistant.components.spc pyspcwebgw==0.4.0 @@ -1458,9 +1470,6 @@ pyspcwebgw==0.4.0 # homeassistant.components.stiebel_eltron pystiebeleltron==0.0.1.dev2 -# homeassistant.components.stride -pystride==0.1.7 - # homeassistant.components.suez_water pysuez==0.1.17 @@ -1468,7 +1477,7 @@ pysuez==0.1.17 pysupla==0.0.3 # homeassistant.components.syncthru -pysyncthru==0.4.3 +pysyncthru==0.5.0 # homeassistant.components.tautulli pytautulli==0.5.0 @@ -1528,7 +1537,7 @@ python-juicenet==0.0.5 # python-lirc==1.2.3 # homeassistant.components.xiaomi_miio -python-miio==0.4.5 +python-miio==0.4.6 # homeassistant.components.mpd python-mpd2==1.0.0 @@ -1594,7 +1603,7 @@ python_opendata_transport==0.1.4 pythonegardia==1.0.40 # homeassistant.components.tile -pytile==2.0.6 +pytile==3.0.0 # homeassistant.components.touchline pytouchline==0.7 @@ -1817,7 +1826,7 @@ spotipy-homeassistant==2.4.4.dev1 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.3.8 +sqlalchemy==1.3.10 # homeassistant.components.starlingbank starlingbank==3.1 @@ -1839,6 +1848,9 @@ stringcase==1.2.0 # homeassistant.components.ecovacs sucks==0.9.4 +# homeassistant.components.solarlog +sunwatcher==0.2.1 + # homeassistant.components.swiss_hydrological_data swisshydrodata==0.0.3 @@ -1873,7 +1885,7 @@ temperusb==1.5.3 # tensorflow==1.13.2 # homeassistant.components.tesla -teslajsonpy==0.0.25 +teslajsonpy==0.0.26 # homeassistant.components.thermoworks_smoke thermoworks_smoke==0.1.8 @@ -1896,9 +1908,6 @@ total_connect_client==0.28 # homeassistant.components.tplink_lte tp-connected==0.0.4 -# homeassistant.components.tplink -tplink==0.2.1 - # homeassistant.components.transmission transmissionrpc==0.11 @@ -1909,7 +1918,7 @@ tuyaha==0.0.4 twentemilieu==0.1.0 # homeassistant.components.twilio -twilio==6.19.1 +twilio==6.32.0 # homeassistant.components.upcloud upcloud-api==0.4.3 @@ -1999,7 +2008,7 @@ xmltodict==0.12.0 xs1-api-client==2.3.5 # homeassistant.components.yandex_transport -ya_ma==0.3.7 +ya_ma==0.3.8 # homeassistant.components.yweather yahooweather==0.10 @@ -2014,7 +2023,7 @@ yeelight==0.5.0 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2019.09.28 +youtube_dl==2019.10.22 # homeassistant.components.zengge zengge==0.2 @@ -2032,16 +2041,16 @@ zhong_hong_hvac==1.0.9 ziggo-mediabox-xl==1.1.0 # homeassistant.components.zha -zigpy-deconz==0.5.0 +zigpy-deconz==0.6.0 # homeassistant.components.zha -zigpy-homeassistant==0.9.0 +zigpy-homeassistant==0.10.0 # homeassistant.components.zha -zigpy-xbee-homeassistant==0.5.0 +zigpy-xbee-homeassistant==0.6.0 # homeassistant.components.zha -zigpy-zigate==0.4.1 +zigpy-zigate==0.5.0 # homeassistant.components.zoneminder zm-py==0.3.3 diff --git a/requirements_test.txt b/requirements_test.txt index 9da375b33c8..7af2ec0dde3 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -6,17 +6,17 @@ asynctest==0.13.0 black==19.3b0 codecov==2.0.15 -flake8-docstrings==1.3.1 +flake8-docstrings==1.5.0 flake8==3.7.8 mock-open==1.3.1 -mypy==0.730 +mypy==0.740 pre-commit==1.18.3 pydocstyle==4.0.1 -pylint==2.3.1 -astroid==2.2.5 +pylint==2.4.3 +astroid==2.3.2 pytest-aiohttp==0.3.0 -pytest-cov==2.7.1 +pytest-cov==2.8.1 pytest-sugar==0.9.2 pytest-timeout==1.3.3 -pytest==5.2.0 +pytest==5.2.1 requests_mock==1.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 22a69139b7a..36f423860d0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,19 +7,19 @@ asynctest==0.13.0 black==19.3b0 codecov==2.0.15 -flake8-docstrings==1.3.1 +flake8-docstrings==1.5.0 flake8==3.7.8 mock-open==1.3.1 -mypy==0.730 +mypy==0.740 pre-commit==1.18.3 pydocstyle==4.0.1 -pylint==2.3.1 -astroid==2.2.5 +pylint==2.4.3 +astroid==2.3.2 pytest-aiohttp==0.3.0 -pytest-cov==2.7.1 +pytest-cov==2.8.1 pytest-sugar==0.9.2 pytest-timeout==1.3.3 -pytest==5.2.0 +pytest==5.2.1 requests_mock==1.7.0 @@ -30,17 +30,29 @@ HAP-python==2.6.0 # homeassistant.components.owntracks PyNaCl==1.3.0 +# homeassistant.auth.mfa_modules.totp +PyQRCode==1.2.1 + # homeassistant.components.rmvtransport -PyRMVtransport==0.1.3 +PyRMVtransport==0.2.9 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 +# homeassistant.components.remember_the_milk +RtmAPI==0.7.2 + # homeassistant.components.yessssms YesssSMS==0.4.1 +# homeassistant.components.abode +abodepy==0.16.6 + +# homeassistant.components.androidtv +adb-shell==0.0.7 + # homeassistant.components.adguard -adguardhome==0.2.1 +adguardhome==0.3.0 # homeassistant.components.geonetnz_quakes aio_geojson_geonetnz_quakes==0.10 @@ -48,6 +60,9 @@ aio_geojson_geonetnz_quakes==0.10 # homeassistant.components.ambient_station aioambient==0.3.2 +# homeassistant.components.asuswrt +aioasuswrt==1.1.21 + # homeassistant.components.automatic aioautomatic==0.6.5 @@ -55,7 +70,7 @@ aioautomatic==0.6.5 aiobotocore==0.10.2 # homeassistant.components.esphome -aioesphomeapi==2.2.0 +aioesphomeapi==2.4.2 # homeassistant.components.emulated_hue # homeassistant.components.http @@ -76,18 +91,31 @@ aiounifi==11 # homeassistant.components.wwlln aiowwlln==2.0.2 +# homeassistant.components.airly +airly==0.0.2 + # homeassistant.components.ambiclimate ambiclimate==0.2.1 # homeassistant.components.androidtv -androidtv==0.0.30 +androidtv==0.0.32 # homeassistant.components.apns apns2==0.3.0 +# homeassistant.components.apprise +apprise==0.8.1 + # homeassistant.components.aprs aprslib==0.6.46 +# homeassistant.components.arcam_fmj +arcam-fmj==0.4.3 + +# homeassistant.components.dlna_dmr +# homeassistant.components.upnp +async-upnp-client==0.14.11 + # homeassistant.components.stream av==6.1.2 @@ -97,17 +125,46 @@ axis==25 # homeassistant.components.zha bellows-homeassistant==0.10.0 +# homeassistant.components.bom +bomradarloop==0.1.3 + +# homeassistant.components.broadlink +broadlink==0.12.0 + +# homeassistant.components.buienradar +buienradar==1.0.1 + # homeassistant.components.caldav caldav==0.6.1 # homeassistant.components.coinmarketcap coinmarketcap==5.0.3 +# homeassistant.scripts.check_config +colorlog==4.0.2 + +# homeassistant.components.eddystone_temperature +# homeassistant.components.eq3btsmart +# homeassistant.components.xiaomi_miio +construct==2.9.45 + +# homeassistant.scripts.credstash +# credstash==1.15.0 + +# homeassistant.components.datadog +datadog==0.15.0 + # homeassistant.components.ihc # homeassistant.components.namecheapdns # homeassistant.components.ohmconnect defusedxml==0.6.0 +# homeassistant.components.directv +directpy==0.5 + +# homeassistant.components.updater +distro==1.4.0 + # homeassistant.components.dsmr dsmr_parser==0.12 @@ -117,9 +174,6 @@ eebrightbox==0.0.4 # homeassistant.components.emulated_roku emulated_roku==0.1.8 -# homeassistant.components.enocean -enocean==0.50 - # homeassistant.components.season ephem==3.7.6.0 @@ -154,9 +208,15 @@ georss_qld_bushfire_alert_client==0.3 # homeassistant.components.nmap_tracker getmac==0.8.1 +# homeassistant.components.glances +glances_api==0.2.0 + # homeassistant.components.google google-api-python-client==1.6.4 +# homeassistant.components.google_pubsub +google-cloud-pubsub==0.39.1 + # homeassistant.components.ffmpeg ha-ffmpeg==2.0 @@ -170,7 +230,7 @@ hass-nabucasa==0.22 hbmqtt==0.9.5 # homeassistant.components.jewish_calendar -hdate==0.9.0 +hdate==0.9.1 # homeassistant.components.here_travel_time herepy==0.6.3.1 @@ -182,13 +242,16 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191002.2 +home-assistant-frontend==20191025.1 + +# homeassistant.components.zwave +homeassistant-pyozw==0.1.4 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 # homeassistant.components.homematicip_cloud -homematicip==0.10.12 +homematicip==0.10.13 # homeassistant.components.google # homeassistant.components.remember_the_milk @@ -206,12 +269,21 @@ influxdb==5.2.3 # homeassistant.components.verisure jsonpath==0.75 +# homeassistant.scripts.keyring +keyring==19.2.0 + +# homeassistant.scripts.keyring +keyrings.alt==3.1.1 + # homeassistant.components.dyson libpurecool==0.5.0 # homeassistant.components.soundtouch libsoundtouch==0.7.2 +# homeassistant.components.logi_circle +logi_circle==0.2.2 + # homeassistant.components.luftdaten luftdaten==0.6.3 @@ -224,15 +296,27 @@ mficlient==0.3.0 # homeassistant.components.minio minio==4.0.9 +# homeassistant.components.tts +mutagen==1.42.0 + +# homeassistant.components.ness_alarm +nessclient==0.9.15 + # homeassistant.components.discovery # homeassistant.components.ssdp netdisco==2.6.0 +# homeassistant.components.nsw_fuel_station +nsw-fuel-api-client==1.0.10 + +# homeassistant.components.nuheat +nuheat==0.3.0 + # homeassistant.components.iqvia # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.17.1 +numpy==1.17.3 # homeassistant.components.google oauth2client==4.0.0 @@ -253,18 +337,27 @@ pilight==0.1.1 # homeassistant.components.image_processing # homeassistant.components.proxy # homeassistant.components.qrcode -pillow==6.1.0 +pillow==6.2.0 # homeassistant.components.plex plexapi==3.0.6 # homeassistant.components.plex -plexauth==0.0.4 +plexauth==0.0.5 + +# homeassistant.components.plex +plexwebsocket==0.0.3 # homeassistant.components.mhz19 # homeassistant.components.serial_pm pmsensor==0.4 +# homeassistant.components.reddit +praw==6.3.1 + +# homeassistant.components.islamic_prayer_times +prayer_times_calculator==0.0.3 + # homeassistant.components.prometheus prometheus_client==0.7.1 @@ -277,6 +370,9 @@ pushbullet.py==0.11.0 # homeassistant.components.canary py-canary==0.5.0 +# homeassistant.components.melissa +py-melissa-climate==2.0.0 + # homeassistant.components.seventeentrack py17track==2.2.2 @@ -287,35 +383,86 @@ pyHS100==0.3.5 # homeassistant.components.norway_air pyMetno==0.4.6 +# homeassistant.components.rfxtrx +pyRFXtrx==0.23 + +# homeassistant.components.nextbus +py_nextbusnext==0.1.4 + +# homeassistant.components.arlo +pyarlo==0.2.3 + # homeassistant.components.blackbird pyblackbird==0.5 +# homeassistant.components.neato +pybotvac==0.0.17 + # homeassistant.components.cast pychromecast==4.0.1 +# homeassistant.components.coolmaster +pycoolmasternet==0.0.4 + +# homeassistant.components.daikin +pydaikin==1.6.1 + # homeassistant.components.deconz -pydeconz==63 +pydeconz==64 # homeassistant.components.zwave pydispatcher==2.0.5 +# homeassistant.components.everlights +pyeverlights==0.1.0 + +# homeassistant.components.fido +pyfido==2.1.1 + +# homeassistant.components.fritzbox +pyfritzhome==0.4.0 + +# homeassistant.components.ifttt +pyfttt==0.3 + +# homeassistant.components.version +pyhaversion==3.1.0 + # homeassistant.components.heos pyheos==0.6.0 # homeassistant.components.homematic -pyhomematic==0.1.60 +pyhomematic==0.1.61 + +# homeassistant.components.ipma +pyipma==1.2.1 # homeassistant.components.iqvia 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 # homeassistant.components.litejet pylitejet==0.1 +# homeassistant.components.mailgun +pymailgunner==1.4 + # homeassistant.components.somfy -pymfy==0.5.2 +pymfy==0.6.0 + +# homeassistant.components.mochad +pymochad==0.2.0 + +# homeassistant.components.modbus +pymodbus==1.5.2 # homeassistant.components.monoprice pymonoprice==0.3 @@ -329,13 +476,19 @@ pynx584==0.4 # homeassistant.components.openuv pyopenuv==1.0.9 +# homeassistant.components.opentherm_gw +pyotgw==0.5b0 + # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp # homeassistant.components.otp pyotp==2.3.0 +# homeassistant.components.point +pypoint==1.1.1 + # homeassistant.components.ps4 -pyps4-homeassistant==0.8.7 +pyps4-2ndscreen==1.0.1 # homeassistant.components.qwikswitch pyqwikswitch==0.93 @@ -353,7 +506,7 @@ pysmartthings==0.6.9 pysoma==0.0.10 # homeassistant.components.sonos -pysonos==0.0.23 +pysonos==0.0.24 # homeassistant.components.spc pyspcwebgw==0.4.0 @@ -367,6 +520,9 @@ python-forecastio==1.4.0 # homeassistant.components.izone python-izone==1.1.1 +# homeassistant.components.xiaomi_miio +python-miio==0.4.6 + # homeassistant.components.nest python-nest==4.1.0 @@ -376,6 +532,9 @@ python-velbus==2.0.27 # homeassistant.components.awair python_awair==0.0.4 +# homeassistant.components.traccar +pytraccar==0.9.0 + # homeassistant.components.tradfri pytradfri[async]==6.3.1 @@ -400,6 +559,9 @@ ring_doorbell==0.2.3 # homeassistant.components.yamaha rxv==0.6.0 +# homeassistant.components.samsungtv +samsungctl[websocket]==0.7.1 + # homeassistant.components.simplisafe simplisafe-python==5.0.1 @@ -417,11 +579,22 @@ somecomfort==0.5.2 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.3.8 +sqlalchemy==1.3.10 # homeassistant.components.statsd statsd==3.2.1 +# homeassistant.components.solaredge +# homeassistant.components.thermoworks_smoke +# homeassistant.components.traccar +stringcase==1.2.0 + +# homeassistant.components.solarlog +sunwatcher==0.2.1 + +# homeassistant.components.tellduslive +tellduslive==0.10.10 + # homeassistant.components.toon toonapilib==3.2.4 @@ -431,6 +604,9 @@ transmissionrpc==0.11 # homeassistant.components.twentemilieu twentemilieu==0.1.0 +# homeassistant.components.twilio +twilio==6.32.0 + # homeassistant.components.uvc uvcclient==0.11.0 @@ -445,11 +621,42 @@ vultr==0.1.2 # homeassistant.components.wake_on_lan wakeonlan==1.1.6 +# homeassistant.components.folder_watcher +watchdog==0.8.3 + +# homeassistant.components.webostv +websockets==6.0 + # homeassistant.components.withings withings-api==2.0.0b8 +# homeassistant.components.bluesound +# homeassistant.components.startca +# homeassistant.components.ted5000 +# homeassistant.components.yr +# homeassistant.components.zestimate +xmltodict==0.12.0 + +# homeassistant.components.yandex_transport +ya_ma==0.3.8 + +# homeassistant.components.yweather +yahooweather==0.10 + # homeassistant.components.zeroconf zeroconf==0.23.0 # homeassistant.components.zha -zigpy-homeassistant==0.9.0 +zha-quirks==0.0.26 + +# homeassistant.components.zha +zigpy-deconz==0.6.0 + +# homeassistant.components.zha +zigpy-homeassistant==0.10.0 + +# homeassistant.components.zha +zigpy-xbee-homeassistant==0.6.0 + +# homeassistant.components.zha +zigpy-zigate==0.5.0 diff --git a/script/bootstrap b/script/bootstrap index ed6cd55be36..211f1355b7d 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -6,5 +6,5 @@ set -e cd "$(dirname "$0")/.." -echo "Installing test dependencies..." -python3 -m pip install tox colorlog pre-commit +echo "Installing development dependencies..." +python3 -m pip install tox colorlog pre-commit $(grep mypy requirements_test.txt) diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index b1ad5240d68..930ffa11b5f 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -1,8 +1,9 @@ #!/usr/bin/env python3 """Generate an updated requirements_all.txt.""" +import difflib import importlib import os -import pathlib +from pathlib import Path import pkgutil import re import sys @@ -41,157 +42,8 @@ COMMENT_REQUIREMENTS = ( "VL53L1X2", ) -TEST_REQUIREMENTS = ( - "adguardhome", - "aio_geojson_geonetnz_quakes", - "aioambient", - "aioautomatic", - "aiobotocore", - "aioesphomeapi", - "aiohttp_cors", - "aiohue", - "aionotion", - "aioswitcher", - "aiounifi", - "aiowwlln", - "ambiclimate", - "androidtv", - "apns2", - "aprslib", - "av", - "axis", - "bellows-homeassistant", - "caldav", - "coinmarketcap", - "defusedxml", - "dsmr_parser", - "eebrightbox", - "emulated_roku", - "enocean", - "ephem", - "evohomeclient", - "feedparser-homeassistant", - "foobot_async", - "geojson_client", - "geopy", - "georss_generic_client", - "georss_ign_sismologia_client", - "georss_qld_bushfire_alert_client", - "getmac", - "google-api-python-client", - "gTTS-token", - "ha-ffmpeg", - "hangups", - "HAP-python", - "hass-nabucasa", - "haversine", - "hbmqtt", - "hdate", - "herepy", - "hole", - "holidays", - "home-assistant-frontend", - "homekit[IP]", - "homematicip", - "httplib2", - "huawei-lte-api", - "iaqualink", - "influxdb", - "jsonpath", - "libpurecool", - "libsoundtouch", - "luftdaten", - "mbddns", - "mficlient", - "minio", - "netdisco", - "numpy", - "oauth2client", - "paho-mqtt", - "pexpect", - "pilight", - "pillow", - "plexapi", - "plexauth", - "pmsensor", - "prometheus_client", - "ptvsd", - "pushbullet.py", - "py-canary", - "py17track", - "pyblackbird", - "pychromecast", - "pydeconz", - "pydispatcher", - "pyheos", - "pyhomematic", - "pyHS100", - "pyiqvia", - "pylinky", - "pylitejet", - "pyMetno", - "pymfy", - "pymonoprice", - "PyNaCl", - "pynws", - "pynx584", - "pyopenuv", - "pyotp", - "pyps4-homeassistant", - "pyqwikswitch", - "PyRMVtransport", - "pysma", - "pysmartapp", - "pysmartthings", - "pysoma", - "pysonos", - "pyspcwebgw", - "python_awair", - "python-ecobee-api", - "python-forecastio", - "python-izone", - "python-nest", - "python-velbus", - "pythonwhois", - "pytradfri[async]", - "PyTransportNSW", - "pyunifi", - "pyupnp-async", - "pyvesync", - "pywebpush", - "regenmaschine", - "restrictedpython", - "rflink", - "ring_doorbell", - "ruamel.yaml", - "rxv", - "simplisafe-python", - "sleepyq", - "smhi-pkg", - "solaredge", - "somecomfort", - "sqlalchemy", - "srpenergy", - "statsd", - "toonapilib", - "transmissionrpc", - "twentemilieu", - "uvcclient", - "vsure", - "vultr", - "wakeonlan", - "warrant", - "nokia", - "YesssSMS", - "zeroconf", - "zigpy-homeassistant", - "withings-api", -) - IGNORE_PIN = ("colorlog>2.1,<3", "keyring>=9.3,<10.0", "urllib3") -IGNORE_REQ = ("colorama<=1",) # Windows only requirement in check_config - URL_PIN = ( "https://developers.home-assistant.io/docs/" "creating_platform_code_review.html#1-requirements" @@ -209,12 +61,31 @@ enum34==1000000000.0.0 # This is a old unmaintained library and is replaced with pycryptodome pycrypto==1000000000.0.0 - -# Contains code to modify Home Assistant to work around our rules -python-systemair-savecair==1000000000.0.0 """ +def has_tests(module: str): + """Test if a module has tests. + + Module format: homeassistant.components.hue + Test if exists: tests/components/hue + """ + path = Path(module.replace(".", "/").replace("homeassistant", "tests")) + if not path.exists(): + return False + + if not path.is_dir(): + return True + + # Dev environments might have stale directories around + # from removed tests. Check for that. + content = [f.name for f in path.glob("*")] + + # Directories need to contain more than `__pycache__` + # to exist in Git and so be seen by CI. + return content != ["__pycache__"] + + def explore_module(package, explore_children): """Explore the modules.""" module = importlib.import_module(package) @@ -235,8 +106,9 @@ def explore_module(package, explore_children): def core_requirements(): """Gather core requirements out of setup.py.""" - with open("setup.py") as inp: - reqs_raw = re.search(r"REQUIRES = \[(.*?)\]", inp.read(), re.S).group(1) + reqs_raw = re.search( + r"REQUIRES = \[(.*?)\]", Path("setup.py").read_text(), re.S + ).group(1) return [x[1] for x in re.findall(r"(['\"])(.*?)\1", reqs_raw)] @@ -246,7 +118,7 @@ def gather_recursive_requirements(domain, seen=None): seen = set() seen.add(domain) - integration = Integration(pathlib.Path(f"homeassistant/components/{domain}")) + integration = Integration(Path(f"homeassistant/components/{domain}")) integration.load_manifest() reqs = set(integration.manifest["requirements"]) for dep_domain in integration.manifest["dependencies"]: @@ -281,7 +153,7 @@ def gather_modules(): def gather_requirements_from_manifests(errors, reqs): """Gather all of the requirements from manifests.""" - integrations = Integration.load_dir(pathlib.Path("homeassistant/components")) + integrations = Integration.load_dir(Path("homeassistant/components")) for domain in sorted(integrations): integration = integrations[domain] @@ -317,8 +189,6 @@ def gather_requirements_from_modules(errors, reqs): def process_requirements(errors, module_requirements, package, reqs): """Process all of the requirements.""" for req in module_requirements: - if req in IGNORE_REQ: - continue if "://" in req: errors.append(f"{package}[Only pypi dependencies are allowed: {req}]") if req.partition("==")[1] == "" and req not in IGNORE_PIN: @@ -357,15 +227,18 @@ def requirements_test_output(reqs): output = [] output.append("# Home Assistant test") output.append("\n") - with open("requirements_test.txt") as test_file: - output.append(test_file.read()) + output.append(Path("requirements_test.txt").read_text()) output.append("\n") + filtered = { - key: value - for key, value in reqs.items() + requirement: modules + for requirement, modules in reqs.items() if any( - re.search(r"(^|#){}($|[=><])".format(re.escape(ign)), key) is not None - for ign in TEST_REQUIREMENTS + # Always install requirements that are not part of integrations + not mdl.startswith("homeassistant.components.") or + # Install tests for integrations that have tests + has_tests(mdl) + for mdl in modules ) } output.append(generate_requirements_list(filtered)) @@ -375,48 +248,28 @@ def requirements_test_output(reqs): def gather_constraints(): """Construct output for constraint file.""" - return "\n".join( - sorted( - core_requirements() + list(gather_recursive_requirements("default_config")) + return ( + "\n".join( + sorted( + core_requirements() + + list(gather_recursive_requirements("default_config")) + ) + + [""] ) - + [""] + + CONSTRAINT_BASE ) -def write_requirements_file(data): - """Write the modules to the requirements_all.txt.""" - with open("requirements_all.txt", "w+", newline="\n") as req_file: - req_file.write(data) - - -def write_test_requirements_file(data): - """Write the modules to the requirements_test_all.txt.""" - with open("requirements_test_all.txt", "w+", newline="\n") as req_file: - req_file.write(data) - - -def write_constraints_file(data): - """Write constraints to a file.""" - with open(CONSTRAINT_PATH, "w+", newline="\n") as req_file: - req_file.write(data + CONSTRAINT_BASE) - - -def validate_requirements_file(data): - """Validate if requirements_all.txt is up to date.""" - with open("requirements_all.txt", "r") as req_file: - return data == req_file.read() - - -def validate_requirements_test_file(data): - """Validate if requirements_test_all.txt is up to date.""" - with open("requirements_test_all.txt", "r") as req_file: - return data == req_file.read() - - -def validate_constraints_file(data): - """Validate if constraints is up to date.""" - with open(CONSTRAINT_PATH, "r") as req_file: - return data + CONSTRAINT_BASE == req_file.read() +def diff_file(filename, content): + """Diff a file.""" + return list( + difflib.context_diff( + [line + "\n" for line in Path(filename).read_text().split("\n")], + [line + "\n" for line in content.split("\n")], + filename, + "generated", + ) + ) def main(validate): @@ -430,33 +283,38 @@ def main(validate): if data is None: return 1 - constraints = gather_constraints() - reqs_file = requirements_all_output(data) reqs_test_file = requirements_test_output(data) + constraints = gather_constraints() + + files = ( + ("requirements_all.txt", reqs_file), + ("requirements_test_all.txt", reqs_test_file), + ("homeassistant/package_constraints.txt", constraints), + ) if validate: errors = [] - if not validate_requirements_file(reqs_file): - errors.append("requirements_all.txt is not up to date") - if not validate_requirements_test_file(reqs_test_file): - errors.append("requirements_test_all.txt is not up to date") - - if not validate_constraints_file(constraints): - errors.append("home-assistant/package_constraints.txt is not up to date") + for filename, content in files: + diff = diff_file(filename, content) + if diff: + errors.append("".join(diff)) if errors: - print("******* ERROR") - print("\n".join(errors)) - print("Please run script/gen_requirements_all.py") + print("ERROR - FOUND THE FOLLOWING DIFFERENCES") + print() + print() + print("\n\n".join(errors)) + print() + print("Please run python3 -m script.gen_requirements_all") return 1 return 0 - write_requirements_file(reqs_file) - write_test_requirements_file(reqs_test_file) - write_constraints_file(constraints) + for filename, content in files: + Path(filename).write_text(content) + return 0 diff --git a/script/scaffold/docs.py b/script/scaffold/docs.py index ab87799d6b2..bb119c0e42e 100644 --- a/script/scaffold/docs.py +++ b/script/scaffold/docs.py @@ -38,6 +38,7 @@ that can occur in the state will cause the right service to be called. f""" Device trigger base has been added to the {info.domain} integration: - {info.integration_dir / "device_trigger.py"} + - {info.integration_dir / "strings.json"} (translations) - {info.tests_dir / "test_device_trigger.py"} You will now need to update the code to make sure that relevant triggers @@ -50,6 +51,7 @@ are exposed. f""" Device condition base has been added to the {info.domain} integration: - {info.integration_dir / "device_condition.py"} + - {info.integration_dir / "strings.json"} (translations) - {info.tests_dir / "test_device_condition.py"} You will now need to update the code to make sure that relevant condtions @@ -62,6 +64,7 @@ are exposed. f""" Device action base has been added to the {info.domain} integration: - {info.integration_dir / "device_action.py"} + - {info.integration_dir / "strings.json"} (translations) - {info.tests_dir / "test_device_action.py"} You will now need to update the code to make sure that relevant services diff --git a/script/scaffold/generate.py b/script/scaffold/generate.py index 6bccf6529fe..e16316fd76b 100644 --- a/script/scaffold/generate.py +++ b/script/scaffold/generate.py @@ -68,6 +68,39 @@ def _custom_tasks(template, info) -> None: info.update_manifest(**changes) + if template == "device_trigger": + info.update_strings( + device_automation={ + **info.strings().get("device_automation", {}), + "trigger_type": { + "turned_on": "{entity_name} turned on", + "turned_off": "{entity_name} turned off", + }, + } + ) + + if template == "device_condition": + info.update_strings( + device_automation={ + **info.strings().get("device_automation", {}), + "condtion_type": { + "is_on": "{entity_name} is on", + "is_off": "{entity_name} is off", + }, + } + ) + + if template == "device_action": + info.update_strings( + device_automation={ + **info.strings().get("device_automation", {}), + "action_type": { + "turn_on": "Turn on {entity_name}", + "turn_off": "Turn off {entity_name}", + }, + } + ) + if template == "config_flow": info.update_manifest(config_flow=True) info.update_strings( 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 f8a00bf1ec8..b65c8257531 100644 --- a/script/scaffold/templates/device_action/tests/test_device_action.py +++ b/script/scaffold/templates/device_action/tests/test_device_action.py @@ -8,6 +8,7 @@ from homeassistant.helpers import device_registry from tests.common import ( MockConfigEntry, + assert_lists_same, async_mock_service, mock_device_registry, mock_registry, @@ -28,7 +29,7 @@ def entity_reg(hass): async def test_get_actions(hass, device_reg, entity_reg): - """Test we get the expected actions from a switch.""" + """Test we get the expected actions from a NEW_DOMAIN.""" config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) device_entry = device_reg.async_get_or_create( @@ -51,7 +52,7 @@ async def test_get_actions(hass, device_reg, entity_reg): }, ] actions = await async_get_device_automations(hass, "action", device_entry.id) - assert actions == expected_actions + assert_lists_same(actions, expected_actions) async def test_action(hass): diff --git a/script/scaffold/templates/device_condition/integration/device_condition.py b/script/scaffold/templates/device_condition/integration/device_condition.py index d19fa8817a0..fa123cff8e0 100644 --- a/script/scaffold/templates/device_condition/integration/device_condition.py +++ b/script/scaffold/templates/device_condition/integration/device_condition.py @@ -1,31 +1,37 @@ """Provides device automations for NEW_NAME.""" -from typing import List +from typing import Dict, List import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, + CONF_CONDITION, CONF_DOMAIN, CONF_TYPE, - CONF_PLATFORM, CONF_DEVICE_ID, CONF_ENTITY_ID, + STATE_OFF, STATE_ON, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers import condition, entity_registry +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 # TODO specify your supported condition types. -CONDITION_TYPES = {"is_on"} +CONDITION_TYPES = {"is_on", "is_off"} CONDITION_SCHEMA = DEVICE_CONDITION_BASE_SCHEMA.extend( - {vol.Required(CONF_TYPE): vol.In(CONDITION_TYPES)} + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In(CONDITION_TYPES), + } ) -async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[str]: +async def async_get_conditions( + hass: HomeAssistant, device_id: str +) -> List[Dict[str, str]]: """List device conditions for NEW_NAME devices.""" registry = await entity_registry.async_get_registry(hass) conditions = [] @@ -39,13 +45,22 @@ async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[str] # TODO add your own conditions. conditions.append( { - CONF_PLATFORM: "device", + CONF_CONDITION: "device", CONF_DEVICE_ID: device_id, CONF_DOMAIN: DOMAIN, CONF_ENTITY_ID: entry.entity_id, CONF_TYPE: "is_on", } ) + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_off", + } + ) return conditions @@ -56,9 +71,13 @@ def async_condition_from_config( """Create a function to test a device condition.""" if config_validation: config = CONDITION_SCHEMA(config) + if config[CONF_TYPE] == "is_on": + state = STATE_ON + else: + state = STATE_OFF - def test_is_on(hass: HomeAssistant, variables: TemplateVarsType) -> bool: - """Test if an entity is on.""" - return condition.state(hass, config[ATTR_ENTITY_ID], STATE_ON) + def test_is_state(hass: HomeAssistant, variables: TemplateVarsType) -> bool: + """Test if an entity is a certain state.""" + return condition.state(hass, config[ATTR_ENTITY_ID], state) - return test_is_on + return test_is_state 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 d9cef083510..1ae4df5f1b7 100644 --- a/script/scaffold/templates/device_condition/tests/test_device_condition.py +++ b/script/scaffold/templates/device_condition/tests/test_device_condition.py @@ -1,7 +1,7 @@ """The tests for NEW_NAME device conditions.""" import pytest -from homeassistant.components.switch import DOMAIN +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 @@ -9,6 +9,7 @@ from homeassistant.helpers import device_registry from tests.common import ( MockConfigEntry, + assert_lists_same, async_mock_service, mock_device_registry, mock_registry, @@ -35,7 +36,7 @@ def calls(hass): async def test_get_conditions(hass, device_reg, entity_reg): - """Test we get the expected conditions from a switch.""" + """Test we get the expected conditions from a NEW_DOMAIN.""" config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) device_entry = device_reg.async_get_or_create( @@ -60,7 +61,7 @@ async def test_get_conditions(hass, device_reg, entity_reg): }, ] conditions = await async_get_device_automations(hass, "condition", device_entry.id) - assert conditions == expected_conditions + assert_lists_same(conditions, expected_conditions) async def test_if_state(hass, calls): diff --git a/script/scaffold/templates/device_trigger/integration/device_trigger.py b/script/scaffold/templates/device_trigger/integration/device_trigger.py index f7e9fc091f8..e0741734d5f 100644 --- a/script/scaffold/templates/device_trigger/integration/device_trigger.py +++ b/script/scaffold/templates/device_trigger/integration/device_trigger.py @@ -12,7 +12,7 @@ from homeassistant.const import ( STATE_OFF, ) from homeassistant.core import HomeAssistant, CALLBACK_TYPE -from homeassistant.helpers import entity_registry +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 @@ -22,7 +22,10 @@ from . import DOMAIN TRIGGER_TYPES = {"turned_on", "turned_off"} TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( - {vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES)} + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES), + } ) @@ -87,14 +90,13 @@ async def async_attach_trigger( from_state = STATE_ON to_state = STATE_OFF - return state.async_attach_trigger( - hass, - { - CONF_ENTITY_ID: config[CONF_ENTITY_ID], - state.CONF_FROM: from_state, - state.CONF_TO: to_state, - }, - action, - automation_info, - platform_type="device", + 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/script/scaffold/templates/device_trigger/tests/test_device_trigger.py b/script/scaffold/templates/device_trigger/tests/test_device_trigger.py index c22197bb136..99e1f8937af 100644 --- a/script/scaffold/templates/device_trigger/tests/test_device_trigger.py +++ b/script/scaffold/templates/device_trigger/tests/test_device_trigger.py @@ -1,7 +1,7 @@ """The tests for NEW_NAME device triggers.""" import pytest -from homeassistant.components.switch import DOMAIN +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 @@ -9,6 +9,7 @@ from homeassistant.helpers import device_registry from tests.common import ( MockConfigEntry, + assert_lists_same, async_mock_service, mock_device_registry, mock_registry, @@ -35,7 +36,7 @@ def calls(hass): async def test_get_triggers(hass, device_reg, entity_reg): - """Test we get the expected triggers from a switch.""" + """Test we get the expected triggers from a NEW_DOMAIN.""" config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) device_entry = device_reg.async_get_or_create( @@ -60,7 +61,7 @@ async def test_get_triggers(hass, device_reg, entity_reg): }, ] triggers = await async_get_device_automations(hass, "trigger", device_entry.id) - assert triggers == expected_triggers + assert_lists_same(triggers, expected_triggers) async def test_if_fires_on_state_change(hass, calls): diff --git a/setup.cfg b/setup.cfg index 4c9c892b93f..6d0e5378b44 100644 --- a/setup.cfg +++ b/setup.cfg @@ -57,17 +57,21 @@ combine_as_imports = true [mypy] python_version = 3.6 +ignore_errors = true +follow_imports = silent +ignore_missing_imports = true +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.*] +ignore_errors = false check_untyped_defs = true disallow_incomplete_defs = true disallow_untyped_calls = true disallow_untyped_defs = true -follow_imports = silent -ignore_missing_imports = true no_implicit_optional = true strict_equality = true -warn_incomplete_stub = true -warn_redundant_casts = true warn_return_any = true warn_unreachable = true -warn_unused_configs = true warn_unused_ignores = true diff --git a/setup.py b/setup.py index 23a8a808f43..d2c4934713b 100755 --- a/setup.py +++ b/setup.py @@ -36,16 +36,16 @@ REQUIRES = [ "async_timeout==3.0.1", "attrs==19.2.0", "bcrypt==3.1.7", - "certifi>=2019.6.16", + "certifi>=2019.9.11", 'contextvars==2.4;python_version<"3.7"', "importlib-metadata==0.23", "jinja2>=2.10.1", "PyJWT==1.7.1", # PyJWT has loose dependency. We want the latest one. - "cryptography==2.7", + "cryptography==2.8", "pip>=8.0.3", - "python-slugify==3.0.4", - "pytz>=2019.02", + "python-slugify==3.0.6", + "pytz>=2019.03", "pyyaml==5.1.2", "requests==2.22.0", "ruamel.yaml==0.15.100", diff --git a/tests/common.py b/tests/common.py index 0684e6daafc..f40019c5d24 100644 --- a/tests/common.py +++ b/tests/common.py @@ -27,6 +27,7 @@ from homeassistant.auth import ( ) from homeassistant.auth.permissions import system_policies from homeassistant.components import mqtt, recorder +from homeassistant.components.mqtt.models import Message from homeassistant.config import async_process_component_config from homeassistant.const import ( ATTR_DISCOVERED, @@ -230,7 +231,6 @@ def get_test_instance_port(): return _TEST_INSTANCE_PORT -@ha.callback def async_mock_service(hass, domain, service, schema=None): """Set up a fake service & return a calls log list to this service.""" calls = [] @@ -272,7 +272,7 @@ def async_fire_mqtt_message(hass, topic, payload, qos=0, retain=False): """Fire the MQTT message.""" if isinstance(payload, str): payload = payload.encode("utf-8") - msg = mqtt.Message(topic, payload, qos, retain) + msg = Message(topic, payload, qos, retain) hass.data["mqtt"]._mqtt_handle_message(msg) @@ -1015,14 +1015,23 @@ def mock_entity_platform(hass, platform_path, module): hue.light. """ domain, platform_name = platform_path.split(".") - integration_cache = hass.data.setdefault(loader.DATA_COMPONENTS, {}) + mock_platform(hass, f"{platform_name}.{domain}", module) + + +def mock_platform(hass, platform_path, module=None): + """Mock a platform. + + platform_path is in form hue.config_flow. + """ + domain, platform_name = platform_path.split(".") + integration_cache = hass.data.setdefault(loader.DATA_INTEGRATIONS, {}) module_cache = hass.data.setdefault(loader.DATA_COMPONENTS, {}) - if platform_name not in integration_cache: - mock_integration(hass, MockModule(platform_name)) + if domain not in integration_cache: + mock_integration(hass, MockModule(domain)) _LOGGER.info("Adding mock integration platform: %s", platform_path) - module_cache["{}.{}".format(platform_name, domain)] = module + module_cache[platform_path] = module or Mock() def async_capture_events(hass, event_name): diff --git a/tests/components/abode/__init__.py b/tests/components/abode/__init__.py new file mode 100644 index 00000000000..a34320c21de --- /dev/null +++ b/tests/components/abode/__init__.py @@ -0,0 +1 @@ +"""Tests for the Abode component.""" diff --git a/tests/components/abode/test_config_flow.py b/tests/components/abode/test_config_flow.py new file mode 100644 index 00000000000..c3f5d170767 --- /dev/null +++ b/tests/components/abode/test_config_flow.py @@ -0,0 +1,120 @@ +"""Tests for the Abode config flow.""" +from unittest.mock import patch + +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" + + +async def test_show_form(hass): + """Test that the form is served with no input.""" + flow = config_flow.AbodeFlowHandler() + 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_one_config_allowed(hass): + """Test that only one Abode configuration is allowed.""" + flow = config_flow.AbodeFlowHandler() + flow.hass = hass + + MockConfigEntry( + domain="abode", + data={CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"}, + ).add_to_hass(hass) + + step_user_result = await flow.async_step_user() + + assert step_user_result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert step_user_result["reason"] == "single_instance_allowed" + + conf = { + CONF_USERNAME: "user@email.com", + CONF_PASSWORD: "password", + CONF_POLLING: False, + } + + import_config_result = await flow.async_step_import(conf) + + assert import_config_result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert import_config_result["reason"] == "single_instance_allowed" + + +async def test_invalid_credentials(hass): + """Test that invalid credentials throws an error.""" + conf = {CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"} + + flow = config_flow.AbodeFlowHandler() + flow.hass = hass + + with patch( + "homeassistant.components.abode.config_flow.Abode", + side_effect=AbodeAuthenticationException((400, "auth error")), + ): + result = await flow.async_step_user(user_input=conf) + assert result["errors"] == {"base": "invalid_credentials"} + + +async def test_connection_error(hass): + """Test other than invalid credentials throws an error.""" + conf = {CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"} + + flow = config_flow.AbodeFlowHandler() + flow.hass = hass + + with patch( + "homeassistant.components.abode.config_flow.Abode", + side_effect=AbodeAuthenticationException((500, "connection error")), + ): + result = await flow.async_step_user(user_input=conf) + assert result["errors"] == {"base": "connection_error"} + + +async def test_step_import(hass): + """Test that the import step works.""" + conf = { + CONF_USERNAME: "user@email.com", + CONF_PASSWORD: "password", + CONF_POLLING: False, + } + + flow = config_flow.AbodeFlowHandler() + flow.hass = hass + + with patch("homeassistant.components.abode.config_flow.Abode"): + result = await flow.async_step_import(import_config=conf) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + result = await flow.async_step_user(user_input=result["data"]) + assert result["title"] == "user@email.com" + assert result["data"] == { + CONF_USERNAME: "user@email.com", + CONF_PASSWORD: "password", + CONF_POLLING: False, + } + + +async def test_step_user(hass): + """Test that the user step works.""" + conf = {CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"} + + flow = config_flow.AbodeFlowHandler() + flow.hass = hass + + with patch("homeassistant.components.abode.config_flow.Abode"): + result = await flow.async_step_user(user_input=conf) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "user@email.com" + assert result["data"] == { + CONF_USERNAME: "user@email.com", + CONF_PASSWORD: "password", + CONF_POLLING: False, + } diff --git a/tests/components/adguard/test_config_flow.py b/tests/components/adguard/test_config_flow.py index ea5e5ad2276..dbda1e99a48 100644 --- a/tests/components/adguard/test_config_flow.py +++ b/tests/components/adguard/test_config_flow.py @@ -3,9 +3,9 @@ from unittest.mock import patch import aiohttp -from homeassistant import data_entry_flow, config_entries +from homeassistant import config_entries, data_entry_flow from homeassistant.components.adguard import config_flow -from homeassistant.components.adguard.const import DOMAIN +from homeassistant.components.adguard.const import DOMAIN, MIN_ADGUARD_HOME_VERSION from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -65,7 +65,7 @@ async def test_full_flow_implementation(hass, aioclient_mock): FIXTURE_USER_INPUT[CONF_HOST], FIXTURE_USER_INPUT[CONF_PORT], ), - json={"version": "1.0"}, + json={"version": "v0.99.0"}, headers={"Content-Type": "application/json"}, ) @@ -133,8 +133,19 @@ async def test_hassio_update_instance_not_running(hass): assert result["reason"] == "existing_instance_updated" -async def test_hassio_update_instance_running(hass): +async def test_hassio_update_instance_running(hass, aioclient_mock): """Test we only allow a single config flow.""" + aioclient_mock.get( + "http://mock-adguard-updated:3000/control/status", + json={"version": "v0.99.0"}, + headers={"Content-Type": "application/json"}, + ) + aioclient_mock.get( + "http://mock-adguard:3000/control/status", + json={"version": "v0.99.0"}, + headers={"Content-Type": "application/json"}, + ) + entry = MockConfigEntry( domain="adguard", data={ @@ -187,7 +198,7 @@ async def test_hassio_confirm(hass, aioclient_mock): """Test we can finish a config flow.""" aioclient_mock.get( "http://mock-adguard:3000/control/status", - json={"version": "1.0"}, + json={"version": "v0.99.0"}, headers={"Content-Type": "application/json"}, ) @@ -228,3 +239,54 @@ async def test_hassio_connection_error(hass, aioclient_mock): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "hassio_confirm" assert result["errors"] == {"base": "connection_error"} + + +async def test_outdated_adguard_version(hass, aioclient_mock): + """Test we show abort when connecting with unsupported AdGuard version.""" + aioclient_mock.get( + "{}://{}:{}/control/status".format( + "https" if FIXTURE_USER_INPUT[CONF_SSL] else "http", + FIXTURE_USER_INPUT[CONF_HOST], + FIXTURE_USER_INPUT[CONF_PORT], + ), + json={"version": "v0.98.0"}, + headers={"Content-Type": "application/json"}, + ) + + flow = config_flow.AdGuardHomeFlowHandler() + 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" + + result = await flow.async_step_user(user_input=FIXTURE_USER_INPUT) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "adguard_home_outdated" + assert result["description_placeholders"] == { + "current_version": "v0.98.0", + "minimal_version": MIN_ADGUARD_HOME_VERSION, + } + + +async def test_outdated_adguard_addon_version(hass, aioclient_mock): + """Test we show abort when connecting with unsupported AdGuard add-on version.""" + aioclient_mock.get( + "http://mock-adguard:3000/control/status", + json={"version": "v0.98.0"}, + headers={"Content-Type": "application/json"}, + ) + + result = await hass.config_entries.flow.async_init( + "adguard", + data={"addon": "AdGuard Home Addon", "host": "mock-adguard", "port": 3000}, + context={"source": "hassio"}, + ) + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "adguard_home_addon_outdated" + assert result["description_placeholders"] == { + "current_version": "v0.98.0", + "minimal_version": MIN_ADGUARD_HOME_VERSION, + } diff --git a/tests/components/airly/__init__.py b/tests/components/airly/__init__.py new file mode 100644 index 00000000000..f31dfb7712d --- /dev/null +++ b/tests/components/airly/__init__.py @@ -0,0 +1 @@ +"""Tests for Airly.""" diff --git a/tests/components/airly/test_config_flow.py b/tests/components/airly/test_config_flow.py new file mode 100644 index 00000000000..8b615b34c2a --- /dev/null +++ b/tests/components/airly/test_config_flow.py @@ -0,0 +1,93 @@ +"""Define tests for the Airly config flow.""" +import json + +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 tests.common import load_fixture, MockConfigEntry + +CONFIG = { + CONF_NAME: "abcd", + CONF_API_KEY: "foo", + CONF_LATITUDE: 123, + CONF_LONGITUDE: 456, +} + + +async def test_show_form(hass): + """Test that the form is served with no input.""" + flow = config_flow.AirlyFlowHandler() + 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_api_key(hass): + """Test that errors are shown when API key is invalid.""" + with patch( + "airly._private._RequestsHandler.get", + side_effect=AirlyError(403, {"message": "Invalid authentication credentials"}), + ): + flow = config_flow.AirlyFlowHandler() + flow.hass = hass + + result = await flow.async_step_user(user_input=CONFIG) + + assert result["errors"] == {"base": "auth"} + + +async def test_invalid_location(hass): + """Test that errors are shown when location is invalid.""" + with patch( + "airly._private._RequestsHandler.get", + return_value=json.loads(load_fixture("airly_no_station.json")), + ): + flow = config_flow.AirlyFlowHandler() + flow.hass = hass + + result = await flow.async_step_user(user_input=CONFIG) + + assert result["errors"] == {"base": "wrong_location"} + + +async def test_duplicate_error(hass): + """Test that errors are shown when duplicates are added.""" + + with patch( + "airly._private._RequestsHandler.get", + return_value=json.loads(load_fixture("airly_valid_station.json")), + ): + MockConfigEntry(domain=DOMAIN, data=CONFIG).add_to_hass(hass) + flow = config_flow.AirlyFlowHandler() + flow.hass = hass + + result = await flow.async_step_user(user_input=CONFIG) + + assert result["errors"] == {CONF_NAME: "name_exists"} + + +async def test_create_entry(hass): + """Test that the user step works.""" + + with patch( + "airly._private._RequestsHandler.get", + return_value=json.loads(load_fixture("airly_valid_station.json")), + ): + flow = config_flow.AirlyFlowHandler() + flow.hass = hass + + result = await flow.async_step_user(user_input=CONFIG) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == CONFIG[CONF_NAME] + assert result["data"][CONF_LATITUDE] == CONFIG[CONF_LATITUDE] + assert result["data"][CONF_LONGITUDE] == CONFIG[CONF_LONGITUDE] + assert result["data"][CONF_API_KEY] == CONFIG[CONF_API_KEY] diff --git a/tests/components/alarm_control_panel/test_device_action.py b/tests/components/alarm_control_panel/test_device_action.py new file mode 100644 index 00000000000..c2dfcbd78b9 --- /dev/null +++ b/tests/components/alarm_control_panel/test_device_action.py @@ -0,0 +1,274 @@ +"""The tests for Alarm control panel device actions.""" +import pytest + +from homeassistant.components.alarm_control_panel import DOMAIN +from homeassistant.const import ( + CONF_PLATFORM, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, + 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 tests.common import ( + MockConfigEntry, + assert_lists_same, + mock_device_registry, + mock_registry, + async_get_device_automations, + async_get_device_automation_capabilities, +) + + +@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) + + +async def test_get_actions(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) + expected_actions = [ + { + "domain": DOMAIN, + "type": "arm_away", + "device_id": device_entry.id, + "entity_id": "alarm_control_panel.test_5678", + }, + { + "domain": DOMAIN, + "type": "arm_home", + "device_id": device_entry.id, + "entity_id": "alarm_control_panel.test_5678", + }, + { + "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", + }, + { + "domain": DOMAIN, + "type": "trigger", + "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}") + platform.init() + + 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", + platform.ENTITIES["no_arm_code"].unique_id, + device_id=device_entry.id, + ) + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + expected_capabilities = { + "arm_away": {"extra_fields": []}, + "arm_home": {"extra_fields": []}, + "arm_night": {"extra_fields": []}, + "disarm": { + "extra_fields": [{"name": "code", "optional": True, "type": "string"}] + }, + "trigger": {"extra_fields": []}, + } + actions = await async_get_device_automations(hass, "action", device_entry.id) + assert len(actions) == 5 + for action in actions: + capabilities = await async_get_device_automation_capabilities( + hass, "action", action + ) + assert capabilities == expected_capabilities[action["type"]] + + +async def test_get_action_capabilities_arm_code(hass, device_reg, entity_reg): + """Test we get the expected capabilities from a sensor trigger.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + 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", + platform.ENTITIES["arm_code"].unique_id, + device_id=device_entry.id, + ) + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + expected_capabilities = { + "arm_away": { + "extra_fields": [{"name": "code", "optional": True, "type": "string"}] + }, + "arm_home": { + "extra_fields": [{"name": "code", "optional": True, "type": "string"}] + }, + "arm_night": { + "extra_fields": [{"name": "code", "optional": True, "type": "string"}] + }, + "disarm": { + "extra_fields": [{"name": "code", "optional": True, "type": "string"}] + }, + "trigger": {"extra_fields": []}, + } + actions = await async_get_device_automations(hass, "action", device_entry.id) + assert len(actions) == 5 + for action in actions: + capabilities = await async_get_device_automation_capabilities( + hass, "action", action + ) + assert capabilities == expected_capabilities[action["type"]] + + +async def test_action(hass): + """Test for turn_on and turn_off actions.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "event", + "event_type": "test_event_arm_away", + }, + "action": { + "domain": DOMAIN, + "device_id": "abcdefgh", + "entity_id": "alarm_control_panel.alarm_no_arm_code", + "type": "arm_away", + }, + }, + { + "trigger": { + "platform": "event", + "event_type": "test_event_arm_home", + }, + "action": { + "domain": DOMAIN, + "device_id": "abcdefgh", + "entity_id": "alarm_control_panel.alarm_no_arm_code", + "type": "arm_home", + }, + }, + { + "trigger": { + "platform": "event", + "event_type": "test_event_arm_night", + }, + "action": { + "domain": DOMAIN, + "device_id": "abcdefgh", + "entity_id": "alarm_control_panel.alarm_no_arm_code", + "type": "arm_night", + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event_disarm"}, + "action": { + "domain": DOMAIN, + "device_id": "abcdefgh", + "entity_id": "alarm_control_panel.alarm_no_arm_code", + "type": "disarm", + "code": "1234", + }, + }, + { + "trigger": { + "platform": "event", + "event_type": "test_event_trigger", + }, + "action": { + "domain": DOMAIN, + "device_id": "abcdefgh", + "entity_id": "alarm_control_panel.alarm_no_arm_code", + "type": "trigger", + }, + }, + ] + }, + ) + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + assert ( + hass.states.get("alarm_control_panel.alarm_no_arm_code").state == STATE_UNKNOWN + ) + + hass.bus.async_fire("test_event_arm_away") + await hass.async_block_till_done() + assert ( + hass.states.get("alarm_control_panel.alarm_no_arm_code").state + == STATE_ALARM_ARMED_AWAY + ) + + hass.bus.async_fire("test_event_arm_home") + await hass.async_block_till_done() + assert ( + hass.states.get("alarm_control_panel.alarm_no_arm_code").state + == STATE_ALARM_ARMED_HOME + ) + + hass.bus.async_fire("test_event_arm_night") + await hass.async_block_till_done() + assert ( + hass.states.get("alarm_control_panel.alarm_no_arm_code").state + == STATE_ALARM_ARMED_NIGHT + ) + + hass.bus.async_fire("test_event_disarm") + await hass.async_block_till_done() + assert ( + hass.states.get("alarm_control_panel.alarm_no_arm_code").state + == STATE_ALARM_DISARMED + ) + + hass.bus.async_fire("test_event_trigger") + await hass.async_block_till_done() + assert ( + hass.states.get("alarm_control_panel.alarm_no_arm_code").state + == STATE_ALARM_TRIGGERED + ) diff --git a/tests/components/alexa/__init__.py b/tests/components/alexa/__init__.py index 48406a11aef..0fa1961ad61 100644 --- a/tests/components/alexa/__init__.py +++ b/tests/components/alexa/__init__.py @@ -13,7 +13,7 @@ TEST_TOKEN_URL = "https://api.amazon.com/auth/o2/token" class MockConfig(config.AbstractConfig): """Mock Alexa config.""" - entity_config = {} + entity_config = {"binary_sensor.test_doorbell": {"display_categories": "DOORBELL"}} @property def supports_auth(self): @@ -67,13 +67,22 @@ def get_new_request(namespace, name, endpoint=None): async def assert_request_calls_service( - namespace, name, endpoint, service, hass, response_type="Response", payload=None + namespace, + name, + endpoint, + service, + hass, + response_type="Response", + payload=None, + instance=None, ): """Assert an API request calls a hass service.""" context = Context() request = get_new_request(namespace, name, endpoint) if payload: request["directive"]["payload"] = payload + if instance: + request["directive"]["header"]["instance"] = instance domain, service_name = service.split(".") calls = async_mock_service(hass, domain, service_name) diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py index d53f145e6ff..be4a2ba4806 100644 --- a/tests/components/alexa/test_capabilities.py +++ b/tests/components/alexa/test_capabilities.py @@ -8,6 +8,11 @@ from homeassistant.const import ( 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 @@ -300,7 +305,7 @@ async def test_report_colored_temp_light_state(hass): async def test_report_fan_speed_state(hass): - """Test PercentageController reports fan speed correctly.""" + """Test PercentageController, PowerLevelController, RangeController reports fan speed correctly.""" hass.states.async_set( "fan.off", "off", @@ -328,15 +333,82 @@ async def test_report_fan_speed_state(hass): properties = await reported_properties(hass, "fan.off") properties.assert_equal("Alexa.PercentageController", "percentage", 0) + properties.assert_equal("Alexa.PowerLevelController", "powerLevel", 0) + properties.assert_equal("Alexa.RangeController", "rangeValue", 0) properties = await reported_properties(hass, "fan.low_speed") properties.assert_equal("Alexa.PercentageController", "percentage", 33) + properties.assert_equal("Alexa.PowerLevelController", "powerLevel", 33) + properties.assert_equal("Alexa.RangeController", "rangeValue", 1) properties = await reported_properties(hass, "fan.medium_speed") properties.assert_equal("Alexa.PercentageController", "percentage", 66) + properties.assert_equal("Alexa.PowerLevelController", "powerLevel", 66) + properties.assert_equal("Alexa.RangeController", "rangeValue", 2) properties = await reported_properties(hass, "fan.high_speed") properties.assert_equal("Alexa.PercentageController", "percentage", 100) + properties.assert_equal("Alexa.PowerLevelController", "powerLevel", 100) + properties.assert_equal("Alexa.RangeController", "rangeValue", 3) + + +async def test_report_fan_oscillating(hass): + """Test ToggleController reports fan oscillating correctly.""" + hass.states.async_set( + "fan.off", + "off", + {"friendly_name": "Off fan", "speed": "off", "supported_features": 3}, + ) + hass.states.async_set( + "fan.low_speed", + "on", + { + "friendly_name": "Low speed fan", + "speed": "low", + "oscillating": True, + "supported_features": 3, + }, + ) + + properties = await reported_properties(hass, "fan.off") + properties.assert_equal("Alexa.ToggleController", "toggleState", "OFF") + + properties = await reported_properties(hass, "fan.low_speed") + properties.assert_equal("Alexa.ToggleController", "toggleState", "ON") + + +async def test_report_fan_direction(hass): + """Test ModeController reports fan direction correctly.""" + hass.states.async_set( + "fan.off", "off", {"friendly_name": "Off fan", "supported_features": 4} + ) + hass.states.async_set( + "fan.reverse", + "on", + { + "friendly_name": "Fan Reverse", + "direction": "reverse", + "supported_features": 4, + }, + ) + hass.states.async_set( + "fan.forward", + "on", + { + "friendly_name": "Fan Forward", + "direction": "forward", + "supported_features": 4, + }, + ) + + properties = await reported_properties(hass, "fan.off") + properties.assert_not_has_property("Alexa.ModeController", "mode") + + properties = await reported_properties(hass, "fan.reverse") + properties.assert_equal("Alexa.ModeController", "mode", "reverse") + + properties = await reported_properties(hass, "fan.forward") + properties.assert_equal("Alexa.ModeController", "mode", "forward") async def test_report_cover_percentage_state(hass): @@ -527,3 +599,33 @@ async def test_temperature_sensor_climate(hass): properties.assert_equal( "Alexa.TemperatureSensor", "temperature", {"value": 34.0, "scale": "CELSIUS"} ) + + +async def test_report_alarm_control_panel_state(hass): + """Test SecurityPanelController implements armState property.""" + hass.states.async_set("alarm_control_panel.armed_away", STATE_ALARM_ARMED_AWAY, {}) + hass.states.async_set( + "alarm_control_panel.armed_custom_bypass", STATE_ALARM_ARMED_CUSTOM_BYPASS, {} + ) + hass.states.async_set("alarm_control_panel.armed_home", STATE_ALARM_ARMED_HOME, {}) + hass.states.async_set( + "alarm_control_panel.armed_night", STATE_ALARM_ARMED_NIGHT, {} + ) + hass.states.async_set("alarm_control_panel.disarmed", STATE_ALARM_DISARMED, {}) + + properties = await reported_properties(hass, "alarm_control_panel.armed_away") + properties.assert_equal("Alexa.SecurityPanelController", "armState", "ARMED_AWAY") + + properties = await reported_properties( + hass, "alarm_control_panel.armed_custom_bypass" + ) + properties.assert_equal("Alexa.SecurityPanelController", "armState", "ARMED_STAY") + + properties = await reported_properties(hass, "alarm_control_panel.armed_home") + properties.assert_equal("Alexa.SecurityPanelController", "armState", "ARMED_STAY") + + properties = await reported_properties(hass, "alarm_control_panel.armed_night") + properties.assert_equal("Alexa.SecurityPanelController", "armState", "ARMED_NIGHT") + + properties = await reported_properties(hass, "alarm_control_panel.disarmed") + properties.assert_equal("Alexa.SecurityPanelController", "armState", "DISARMED") diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index e5e5b8ab7ae..c50c0748147 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -4,6 +4,19 @@ 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.media_player.const import ( + SUPPORT_NEXT_TRACK, + SUPPORT_PAUSE, + SUPPORT_PLAY, + SUPPORT_PLAY_MEDIA, + SUPPORT_PREVIOUS_TRACK, + SUPPORT_SELECT_SOURCE, + SUPPORT_STOP, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, + SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_SET, +) from homeassistant.helpers import entityfilter from tests.common import async_mock_service @@ -310,10 +323,14 @@ async def test_fan(hass): assert appliance["endpointId"] == "fan#test_1" assert appliance["displayCategories"][0] == "FAN" assert appliance["friendlyName"] == "Test fan 1" - assert_endpoint_capabilities( + capabilities = assert_endpoint_capabilities( appliance, "Alexa.PowerController", "Alexa.EndpointHealth" ) + power_capability = get_capability(capabilities, "Alexa.PowerController") + assert "capabilityResources" not in power_capability + assert "configuration" not in power_capability + async def test_variable_fan(hass): """Test fan discovery. @@ -336,13 +353,33 @@ async def test_variable_fan(hass): assert appliance["displayCategories"][0] == "FAN" assert appliance["friendlyName"] == "Test fan 2" - assert_endpoint_capabilities( + capabilities = assert_endpoint_capabilities( appliance, "Alexa.PercentageController", "Alexa.PowerController", + "Alexa.PowerLevelController", + "Alexa.RangeController", "Alexa.EndpointHealth", ) + range_capability = get_capability(capabilities, "Alexa.RangeController") + assert range_capability is not None + assert range_capability["instance"] == "fan.speed" + + 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": "asset", + "value": {"assetId": "Alexa.Setting.FanSpeed"}, + } in capability_resources["friendlyNames"] + + configuration = range_capability["configuration"] + assert configuration is not None + call, _ = await assert_request_calls_service( "Alexa.PercentageController", "SetPercentage", @@ -364,6 +401,272 @@ 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": "50"}, + ) + assert call.data["speed"] == "medium" + + await assert_percentage_changes( + hass, + [("high", "-5"), ("medium", "-50"), ("low", "-80")], + "Alexa.PowerLevelController", + "AdjustPowerLevel", + "fan#test_2", + "powerLevelDelta", + "fan.set_speed", + "speed", + ) + + +async def test_oscillating_fan(hass): + """Test oscillating fan discovery.""" + device = ( + "fan.test_3", + "off", + {"friendly_name": "Test fan 3", "supported_features": 3}, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "fan#test_3" + assert appliance["displayCategories"][0] == "FAN" + assert appliance["friendlyName"] == "Test fan 3" + capabilities = assert_endpoint_capabilities( + appliance, + "Alexa.PercentageController", + "Alexa.PowerController", + "Alexa.PowerLevelController", + "Alexa.RangeController", + "Alexa.ToggleController", + "Alexa.EndpointHealth", + ) + + toggle_capability = get_capability(capabilities, "Alexa.ToggleController") + assert toggle_capability is not None + assert toggle_capability["instance"] == "fan.oscillating" + + properties = toggle_capability["properties"] + assert properties["nonControllable"] is False + assert {"name": "toggleState"} in properties["supported"] + + capability_resources = toggle_capability["capabilityResources"] + assert capability_resources is not None + assert { + "@type": "asset", + "value": {"assetId": "Alexa.Setting.Oscillate"}, + } in capability_resources["friendlyNames"] + + call, _ = await assert_request_calls_service( + "Alexa.ToggleController", + "TurnOn", + "fan#test_3", + "fan.oscillate", + hass, + payload={}, + instance="fan.oscillating", + ) + assert call.data["oscillating"] + + call, _ = await assert_request_calls_service( + "Alexa.ToggleController", + "TurnOff", + "fan#test_3", + "fan.oscillate", + hass, + payload={}, + instance="fan.oscillating", + ) + assert not call.data["oscillating"] + + +async def test_direction_fan(hass): + """Test direction fan discovery.""" + device = ( + "fan.test_4", + "on", + { + "friendly_name": "Test fan 4", + "supported_features": 5, + "direction": "forward", + }, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "fan#test_4" + assert appliance["displayCategories"][0] == "FAN" + assert appliance["friendlyName"] == "Test fan 4" + capabilities = assert_endpoint_capabilities( + appliance, + "Alexa.PercentageController", + "Alexa.PowerController", + "Alexa.PowerLevelController", + "Alexa.RangeController", + "Alexa.ModeController", + "Alexa.EndpointHealth", + ) + + mode_capability = get_capability(capabilities, "Alexa.ModeController") + assert mode_capability is not None + assert mode_capability["instance"] == "fan.direction" + + 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": "asset", + "value": {"assetId": "Alexa.Setting.Direction"}, + } 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": "direction.forward", + "modeResources": { + "friendlyNames": [ + {"@type": "text", "value": {"text": "forward", "locale": "en-US"}} + ] + }, + } in supported_modes + assert { + "value": "direction.reverse", + "modeResources": { + "friendlyNames": [ + {"@type": "text", "value": {"text": "reverse", "locale": "en-US"}} + ] + }, + } in supported_modes + + call, _ = await assert_request_calls_service( + "Alexa.ModeController", + "SetMode", + "fan#test_4", + "fan.set_direction", + hass, + payload={"mode": "direction.reverse"}, + instance="fan.direction", + ) + assert call.data["direction"] == "reverse" + + # Test for AdjustMode instance=None Error coverage + with pytest.raises(AssertionError): + call, _ = await assert_request_calls_service( + "Alexa.ModeController", + "AdjustMode", + "fan#test_4", + "fan.set_direction", + hass, + payload={}, + instance=None, + ) + assert call.data + + +async def test_fan_range(hass): + """Test fan discovery with range controller. + + This one has variable speed. + """ + device = ( + "fan.test_5", + "off", + { + "friendly_name": "Test fan 5", + "supported_features": 1, + "speed_list": ["low", "medium", "high"], + "speed": "medium", + }, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "fan#test_5" + assert appliance["displayCategories"][0] == "FAN" + assert appliance["friendlyName"] == "Test fan 5" + + capabilities = assert_endpoint_capabilities( + appliance, + "Alexa.PercentageController", + "Alexa.PowerController", + "Alexa.PowerLevelController", + "Alexa.RangeController", + "Alexa.EndpointHealth", + ) + + range_capability = get_capability(capabilities, "Alexa.RangeController") + assert range_capability is not None + assert range_capability["instance"] == "fan.speed" + + call, _ = await assert_request_calls_service( + "Alexa.RangeController", + "SetRangeValue", + "fan#test_5", + "fan.set_speed", + hass, + payload={"rangeValue": "1"}, + instance="fan.speed", + ) + assert call.data["speed"] == "low" + + await assert_range_changes( + hass, + [("low", "-1"), ("high", "1"), ("medium", "0")], + "Alexa.RangeController", + "AdjustRangeValue", + "fan#test_5", + False, + "fan.set_speed", + "speed", + instance="fan.speed", + ) + + +async def test_fan_range_off(hass): + """Test fan range controller 0 turns_off fan.""" + device = ( + "fan.test_6", + "off", + { + "friendly_name": "Test fan 6", + "supported_features": 1, + "speed_list": ["low", "medium", "high"], + "speed": "high", + }, + ) + await discovery_test(device, hass) + + call, _ = await assert_request_calls_service( + "Alexa.RangeController", + "SetRangeValue", + "fan#test_6", + "fan.turn_off", + hass, + payload={"rangeValue": "0"}, + instance="fan.speed", + ) + assert call.data["speed"] == "off" + + await assert_range_changes( + hass, + [("off", "-3")], + "Alexa.RangeController", + "AdjustRangeValue", + "fan#test_6", + False, + "fan.turn_off", + "speed", + instance="fan.speed", + ) + async def test_lock(hass): """Test lock discovery.""" @@ -381,12 +684,20 @@ async def test_lock(hass): "Alexa.LockController", "Lock", "lock#test", "lock.lock", hass ) - # always return LOCKED for now properties = msg["context"]["properties"][0] assert properties["name"] == "lockState" assert properties["namespace"] == "Alexa.LockController" assert properties["value"] == "LOCKED" + _, msg = await assert_request_calls_service( + "Alexa.LockController", "Unlock", "lock#test", "lock.unlock", hass + ) + + properties = msg["context"]["properties"][0] + assert properties["name"] == "lockState" + assert properties["namespace"] == "Alexa.LockController" + assert properties["value"] == "UNLOCKED" + async def test_media_player(hass): """Test media player discovery.""" @@ -395,7 +706,17 @@ async def test_media_player(hass): "off", { "friendly_name": "Test media player", - "supported_features": 0x59BD, + "supported_features": SUPPORT_NEXT_TRACK + | SUPPORT_PAUSE + | SUPPORT_PLAY + | SUPPORT_PLAY_MEDIA + | SUPPORT_PREVIOUS_TRACK + | SUPPORT_SELECT_SOURCE + | SUPPORT_STOP + | SUPPORT_TURN_OFF + | SUPPORT_TURN_ON + | SUPPORT_VOLUME_MUTE + | SUPPORT_VOLUME_SET, "volume_level": 0.75, }, ) @@ -413,6 +734,7 @@ async def test_media_player(hass): "Alexa.StepSpeaker", "Alexa.PlaybackController", "Alexa.EndpointHealth", + "Alexa.ChannelController", ) await assert_power_controller_works( @@ -526,7 +848,7 @@ async def test_media_player(hass): "media_player#test", "media_player.volume_up", hass, - payload={"volumeSteps": 20}, + payload={"volumeSteps": 1, "volumeStepsDefault": False}, ) call, _ = await assert_request_calls_service( @@ -535,7 +857,69 @@ async def test_media_player(hass): "media_player#test", "media_player.volume_down", hass, - payload={"volumeSteps": -20}, + payload={"volumeSteps": -1, "volumeStepsDefault": False}, + ) + + call, _ = await assert_request_calls_service( + "Alexa.StepSpeaker", + "AdjustVolume", + "media_player#test", + "media_player.volume_up", + hass, + payload={"volumeSteps": 10, "volumeStepsDefault": True}, + ) + call, _ = await assert_request_calls_service( + "Alexa.ChannelController", + "ChangeChannel", + "media_player#test", + "media_player.play_media", + hass, + payload={"channel": {"number": 24}}, + ) + + call, _ = await assert_request_calls_service( + "Alexa.ChannelController", + "ChangeChannel", + "media_player#test", + "media_player.play_media", + hass, + payload={"channel": {"callSign": "ABC"}}, + ) + + call, _ = await assert_request_calls_service( + "Alexa.ChannelController", + "ChangeChannel", + "media_player#test", + "media_player.play_media", + hass, + payload={"channel": {"affiliateCallSign": "ABC"}}, + ) + + call, _ = await assert_request_calls_service( + "Alexa.ChannelController", + "ChangeChannel", + "media_player#test", + "media_player.play_media", + hass, + payload={"channel": {"uri": "ABC"}}, + ) + + call, _ = await assert_request_calls_service( + "Alexa.ChannelController", + "SkipChannels", + "media_player#test", + "media_player.media_next_track", + hass, + payload={"channelCount": 1}, + ) + + call, _ = await assert_request_calls_service( + "Alexa.ChannelController", + "SkipChannels", + "media_player#test", + "media_player.media_previous_track", + hass, + payload={"channelCount": -1}, ) @@ -564,6 +948,7 @@ async def test_media_player_power(hass): "Alexa.StepSpeaker", "Alexa.PlaybackController", "Alexa.EndpointHealth", + "Alexa.ChannelController", ) await assert_request_calls_service( @@ -699,6 +1084,33 @@ async def assert_percentage_changes( assert call.data[changed_parameter] == result_volume +async def assert_range_changes( + hass, + adjustments, + namespace, + name, + endpoint, + delta_default, + service, + changed_parameter, + instance, +): + """Assert an API request making range changes works. + + AdjustRangeValue are examples of such requests. + """ + for result_range, adjustment in adjustments: + payload = { + "rangeValueDelta": adjustment, + "rangeValueDeltaDefault": delta_default, + } + + call, _ = await assert_request_calls_service( + namespace, name, endpoint, service, hass, payload=payload, instance=instance + ) + assert call.data[changed_parameter] == result_range + + async def test_temp_sensor(hass): """Test temperature sensor discovery.""" device = ( @@ -784,6 +1196,28 @@ async def test_motion_sensor(hass): properties.assert_equal("Alexa.MotionSensor", "detectionState", "DETECTED") +async def test_doorbell_sensor(hass): + """Test doorbell sensor discovery.""" + device = ( + "binary_sensor.test_doorbell", + "off", + {"friendly_name": "Test Doorbell Sensor", "device_class": "occupancy"}, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "binary_sensor#test_doorbell" + assert appliance["displayCategories"][0] == "DOORBELL" + assert appliance["friendlyName"] == "Test Doorbell Sensor" + + capabilities = assert_endpoint_capabilities( + appliance, "Alexa.DoorbellEventSource", "Alexa.EndpointHealth" + ) + + doorbell_capability = get_capability(capabilities, "Alexa.DoorbellEventSource") + assert doorbell_capability is not None + assert doorbell_capability["proactivelyReported"] is True + + async def test_unknown_sensor(hass): """Test sensors of unknown quantities are not discovered.""" device = ( @@ -1284,3 +1718,165 @@ async def test_endpoint_bad_health(hass): properties.assert_equal( "Alexa.EndpointHealth", "connectivity", {"value": "UNREACHABLE"} ) + + +async def test_alarm_control_panel_disarmed(hass): + """Test alarm_control_panel discovery.""" + device = ( + "alarm_control_panel.test_1", + "disarmed", + { + "friendly_name": "Test Alarm Control Panel 1", + "code_arm_required": False, + "code_format": "number", + "code": "1234", + }, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "alarm_control_panel#test_1" + assert appliance["displayCategories"][0] == "SECURITY_PANEL" + assert appliance["friendlyName"] == "Test Alarm Control Panel 1" + capabilities = assert_endpoint_capabilities( + appliance, "Alexa.SecurityPanelController", "Alexa.EndpointHealth" + ) + security_panel_capability = get_capability( + capabilities, "Alexa.SecurityPanelController" + ) + assert security_panel_capability is not None + configuration = security_panel_capability["configuration"] + assert {"type": "FOUR_DIGIT_PIN"} in configuration["supportedAuthorizationTypes"] + + properties = await reported_properties(hass, "alarm_control_panel#test_1") + properties.assert_equal("Alexa.SecurityPanelController", "armState", "DISARMED") + + call, msg = await assert_request_calls_service( + "Alexa.SecurityPanelController", + "Arm", + "alarm_control_panel#test_1", + "alarm_control_panel.alarm_arm_home", + hass, + response_type="Arm.Response", + payload={"armState": "ARMED_STAY"}, + ) + properties = ReportedProperties(msg["context"]["properties"]) + properties.assert_equal("Alexa.SecurityPanelController", "armState", "ARMED_STAY") + + call, msg = await assert_request_calls_service( + "Alexa.SecurityPanelController", + "Arm", + "alarm_control_panel#test_1", + "alarm_control_panel.alarm_arm_away", + hass, + response_type="Arm.Response", + payload={"armState": "ARMED_AWAY"}, + ) + properties = ReportedProperties(msg["context"]["properties"]) + properties.assert_equal("Alexa.SecurityPanelController", "armState", "ARMED_AWAY") + + call, msg = await assert_request_calls_service( + "Alexa.SecurityPanelController", + "Arm", + "alarm_control_panel#test_1", + "alarm_control_panel.alarm_arm_night", + hass, + response_type="Arm.Response", + payload={"armState": "ARMED_NIGHT"}, + ) + properties = ReportedProperties(msg["context"]["properties"]) + properties.assert_equal("Alexa.SecurityPanelController", "armState", "ARMED_NIGHT") + + +async def test_alarm_control_panel_armed(hass): + """Test alarm_control_panel discovery.""" + device = ( + "alarm_control_panel.test_2", + "armed_away", + { + "friendly_name": "Test Alarm Control Panel 2", + "code_arm_required": False, + "code_format": "FORMAT_NUMBER", + "code": "1234", + }, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "alarm_control_panel#test_2" + assert appliance["displayCategories"][0] == "SECURITY_PANEL" + assert appliance["friendlyName"] == "Test Alarm Control Panel 2" + assert_endpoint_capabilities( + appliance, "Alexa.SecurityPanelController", "Alexa.EndpointHealth" + ) + + properties = await reported_properties(hass, "alarm_control_panel#test_2") + properties.assert_equal("Alexa.SecurityPanelController", "armState", "ARMED_AWAY") + + call, msg = await assert_request_calls_service( + "Alexa.SecurityPanelController", + "Disarm", + "alarm_control_panel#test_2", + "alarm_control_panel.alarm_disarm", + hass, + payload={"authorization": {"type": "FOUR_DIGIT_PIN", "value": "1234"}}, + ) + assert call.data["code"] == "1234" + properties = ReportedProperties(msg["context"]["properties"]) + properties.assert_equal("Alexa.SecurityPanelController", "armState", "DISARMED") + + msg = await assert_request_fails( + "Alexa.SecurityPanelController", + "Arm", + "alarm_control_panel#test_2", + "alarm_control_panel.alarm_arm_home", + hass, + payload={"armState": "ARMED_STAY"}, + ) + assert msg["event"]["payload"]["type"] == "AUTHORIZATION_REQUIRED" + + +async def test_alarm_control_panel_code_arm_required(hass): + """Test alarm_control_panel with code_arm_required discovery.""" + device = ( + "alarm_control_panel.test_3", + "disarmed", + {"friendly_name": "Test Alarm Control Panel 3", "code_arm_required": True}, + ) + await discovery_test(device, hass, expected_endpoints=0) + + +async def test_range_unsupported_domain(hass): + """Test rangeController with unsupported domain.""" + device = ("switch.test", "on", {"friendly_name": "Test switch"}) + await discovery_test(device, hass) + + context = Context() + request = get_new_request("Alexa.RangeController", "SetRangeValue", "switch#test") + request["directive"]["payload"] = {"rangeValue": "1"} + request["directive"]["header"]["instance"] = "switch.speed" + + 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" + + +async def test_mode_unsupported_domain(hass): + """Test modeController with unsupported domain.""" + device = ("switch.test", "on", {"friendly_name": "Test switch"}) + await discovery_test(device, hass) + + context = Context() + request = get_new_request("Alexa.ModeController", "SetMode", "switch#test") + request["directive"]["payload"] = {"mode": "testMode"} + request["directive"]["header"]["instance"] = "switch.direction" + + 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_state_report.py b/tests/components/alexa/test_state_report.py index c05eed2a89b..2c58d1ed45e 100644 --- a/tests/components/alexa/test_state_report.py +++ b/tests/components/alexa/test_state_report.py @@ -37,6 +37,54 @@ async def test_report_state(hass, aioclient_mock): assert call_json["event"]["endpoint"]["endpointId"] == "binary_sensor#test_contact" +async def test_report_state_instance(hass, aioclient_mock): + """Test proactive state reports with instance.""" + aioclient_mock.post(TEST_URL, text="", status=202) + + hass.states.async_set( + "fan.test_fan", + "off", + { + "friendly_name": "Test fan", + "supported_features": 3, + "speed": "off", + "oscillating": False, + }, + ) + + await state_report.async_enable_proactive_mode(hass, DEFAULT_CONFIG) + + hass.states.async_set( + "fan.test_fan", + "on", + { + "friendly_name": "Test fan", + "supported_features": 3, + "speed": "high", + "oscillating": True, + }, + ) + + # To trigger event listener + await hass.async_block_till_done() + + assert len(aioclient_mock.mock_calls) == 1 + call = aioclient_mock.mock_calls + + call_json = call[0][2] + assert call_json["event"]["header"]["namespace"] == "Alexa" + assert call_json["event"]["header"]["name"] == "ChangeReport" + + change_reports = call_json["event"]["payload"]["change"]["properties"] + for report in change_reports: + if report["name"] == "toggleState": + assert report["value"] == "ON" + assert report["instance"] == "fan.oscillating" + assert report["namespace"] == "Alexa.ToggleController" + + assert call_json["event"]["endpoint"]["endpointId"] == "fan#test_fan" + + async def test_send_add_or_update_message(hass, aioclient_mock): """Test sending an AddOrUpdateReport message.""" aioclient_mock.post(TEST_URL, text="") @@ -89,3 +137,34 @@ async def test_send_delete_message(hass, aioclient_mock): call_json["event"]["payload"]["endpoints"][0]["endpointId"] == "binary_sensor#test_contact" ) + + +async def test_doorbell_event(hass, aioclient_mock): + """Test doorbell press reports.""" + aioclient_mock.post(TEST_URL, text="", status=202) + + hass.states.async_set( + "binary_sensor.test_doorbell", + "off", + {"friendly_name": "Test Doorbell Sensor", "device_class": "occupancy"}, + ) + + await state_report.async_enable_proactive_mode(hass, DEFAULT_CONFIG) + + hass.states.async_set( + "binary_sensor.test_doorbell", + "on", + {"friendly_name": "Test Doorbell Sensor", "device_class": "occupancy"}, + ) + + # To trigger event listener + await hass.async_block_till_done() + + assert len(aioclient_mock.mock_calls) == 1 + call = aioclient_mock.mock_calls + + call_json = call[0][2] + assert call_json["event"]["header"]["namespace"] == "Alexa.DoorbellEventSource" + assert call_json["event"]["header"]["name"] == "DoorbellPress" + assert call_json["event"]["payload"]["cause"]["type"] == "PHYSICAL_INTERACTION" + assert call_json["event"]["endpoint"]["endpointId"] == "binary_sensor#test_doorbell" diff --git a/tests/components/androidtv/patchers.py b/tests/components/androidtv/patchers.py index 73aa5225989..5fc6bc754fa 100644 --- a/tests/components/androidtv/patchers.py +++ b/tests/components/androidtv/patchers.py @@ -1,7 +1,7 @@ """Define patches used for androidtv tests.""" from socket import error as socket_error -from unittest.mock import patch +from unittest.mock import mock_open, patch class AdbDeviceFake: @@ -128,3 +128,15 @@ def patch_shell(response=None, error=False): PATCH_ADB_DEVICE = patch("androidtv.adb_manager.AdbDevice", AdbDeviceFake) +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") + + +def isfile(filepath): + """Mock `os.path.isfile`.""" + return filepath.endswith("adbkey") + + +PATCH_ISFILE = patch("os.path.isfile", isfile) +PATCH_ACCESS = patch("os.access", return_value=True) diff --git a/tests/components/androidtv/test_media_player.py b/tests/components/androidtv/test_media_player.py index feffc70d841..85f562a3500 100644 --- a/tests/components/androidtv/test_media_player.py +++ b/tests/components/androidtv/test_media_player.py @@ -5,6 +5,7 @@ from homeassistant.setup import async_setup_component from homeassistant.components.androidtv.media_player import ( ANDROIDTV_DOMAIN, CONF_ADB_SERVER_IP, + CONF_ADBKEY, ) from homeassistant.components.media_player.const import DOMAIN from homeassistant.const import ( @@ -61,14 +62,8 @@ CONFIG_FIRETV_ADB_SERVER = { } -async def _test_reconnect(hass, caplog, config): - """Test that the error and reconnection attempts are logged correctly. - - "Handles device/service unavailable. Log a warning once when - unavailable, log once when reconnected." - - https://developers.home-assistant.io/docs/en/integration_quality_scale_index.html - """ +def _setup(hass, config): + """Perform common setup tasks for the tests.""" if CONF_ADB_SERVER_IP not in config[DOMAIN]: patch_key = "python" else: @@ -79,10 +74,26 @@ async def _test_reconnect(hass, caplog, config): else: entity_id = "media_player.fire_tv" + return patch_key, entity_id + + +async def _test_reconnect(hass, caplog, config): + """Test that the error and reconnection attempts are logged correctly. + + "Handles device/service unavailable. Log a warning once when + unavailable, log once when reconnected." + + https://developers.home-assistant.io/docs/en/integration_quality_scale_index.html + """ + patch_key, entity_id = _setup(hass, config) + with patchers.PATCH_ADB_DEVICE, patchers.patch_connect(True)[ patch_key - ], patchers.patch_shell("")[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 not None @@ -93,7 +104,7 @@ async def _test_reconnect(hass, caplog, config): with patchers.patch_connect(False)[patch_key], patchers.patch_shell(error=True)[ patch_key - ]: + ], patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER: for _ in range(5): await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) @@ -105,7 +116,9 @@ async def _test_reconnect(hass, caplog, config): assert caplog.record_tuples[1][1] == logging.WARNING caplog.set_level(logging.DEBUG) - with patchers.patch_connect(True)[patch_key], patchers.patch_shell("1")[patch_key]: + with patchers.patch_connect(True)[patch_key], patchers.patch_shell("1")[ + patch_key + ], patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER: # Update 1 will reconnect await hass.helpers.entity_component.async_update_entity(entity_id) @@ -143,19 +156,13 @@ async def _test_adb_shell_returns_none(hass, config): The state should be `None` and the device should be unavailable. """ - if CONF_ADB_SERVER_IP not in config[DOMAIN]: - patch_key = "python" - else: - patch_key = "server" - - if config[DOMAIN].get(CONF_DEVICE_CLASS) != "firetv": - entity_id = "media_player.android_tv" - else: - entity_id = "media_player.fire_tv" + patch_key, entity_id = _setup(hass, config) with patchers.PATCH_ADB_DEVICE, patchers.patch_connect(True)[ patch_key - ], patchers.patch_shell("")[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) @@ -164,7 +171,7 @@ async def _test_adb_shell_returns_none(hass, config): with patchers.patch_shell(None)[patch_key], patchers.patch_shell(error=True)[ patch_key - ]: + ], patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER: await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) assert state is not None @@ -251,3 +258,21 @@ async def test_adb_shell_returns_none_firetv_adb_server(hass): """ assert await _test_adb_shell_returns_none(hass, CONFIG_FIRETV_ADB_SERVER) + + +async def test_setup_with_adbkey(hass): + """Test that setup succeeds when using an ADB key.""" + config = CONFIG_ANDROIDTV_PYTHON_ADB.copy() + 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)[ + patch_key + ], patchers.patch_shell("")[ + patch_key + ], patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER, patchers.PATCH_ISFILE, patchers.PATCH_ACCESS: + 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 not None + assert state.state == STATE_OFF diff --git a/tests/components/apns/test_notify.py b/tests/components/apns/test_notify.py index a4202f74d39..78f597c58ad 100644 --- a/tests/components/apns/test_notify.py +++ b/tests/components/apns/test_notify.py @@ -239,7 +239,7 @@ class TestApns(unittest.TestCase): assert "tracking123" == test_device_1.tracking_device_id assert "tracking456" == test_device_2.tracking_device_id - @patch("apns2.client.APNsClient") + @patch("homeassistant.components.apns.notify.APNsClient") def test_send(self, mock_client): """Test updating an existing device.""" send = mock_client.return_value.send_notification @@ -274,7 +274,7 @@ class TestApns(unittest.TestCase): assert "test.mp3" == payload.sound assert "testing" == payload.category - @patch("apns2.client.APNsClient") + @patch("homeassistant.components.apns.notify.APNsClient") def test_send_when_disabled(self, mock_client): """Test updating an existing device.""" send = mock_client.return_value.send_notification @@ -299,7 +299,7 @@ class TestApns(unittest.TestCase): assert not send.called - @patch("apns2.client.APNsClient") + @patch("homeassistant.components.apns.notify.APNsClient") def test_send_with_state(self, mock_client): """Test updating an existing device.""" send = mock_client.return_value.send_notification @@ -334,7 +334,7 @@ class TestApns(unittest.TestCase): assert "5678" == target assert "Hello" == payload.alert - @patch("apns2.client.APNsClient") + @patch("homeassistant.components.apns.notify.APNsClient") @patch("homeassistant.components.apns.notify._write_device") def test_disable_when_unregistered(self, mock_write, mock_client): """Test disabling a device when it is unregistered.""" diff --git a/tests/components/apprise/__init__.py b/tests/components/apprise/__init__.py new file mode 100644 index 00000000000..ffebc35b4e1 --- /dev/null +++ b/tests/components/apprise/__init__.py @@ -0,0 +1 @@ +"""Tests for the apprise component.""" diff --git a/tests/components/apprise/test_notify.py b/tests/components/apprise/test_notify.py new file mode 100644 index 00000000000..237f99de676 --- /dev/null +++ b/tests/components/apprise/test_notify.py @@ -0,0 +1,148 @@ +"""The tests for the apprise notification platform.""" +from unittest.mock import patch +from unittest.mock import MagicMock + +from homeassistant.setup import async_setup_component + +BASE_COMPONENT = "notify" + + +async def test_apprise_config_load_fail01(hass): + """Test apprise configuration failures 1.""" + + config = { + BASE_COMPONENT: {"name": "test", "platform": "apprise", "config": "/path/"} + } + + with patch("apprise.AppriseConfig.add", return_value=False): + assert await async_setup_component(hass, BASE_COMPONENT, config) + await hass.async_block_till_done() + + # Test that our service failed to load + assert not hass.services.has_service(BASE_COMPONENT, "test") + + +async def test_apprise_config_load_fail02(hass): + """Test apprise configuration failures 2.""" + + config = { + BASE_COMPONENT: {"name": "test", "platform": "apprise", "config": "/path/"} + } + + with patch("apprise.Apprise.add", return_value=False): + with patch("apprise.AppriseConfig.add", return_value=True): + assert await async_setup_component(hass, BASE_COMPONENT, config) + await hass.async_block_till_done() + + # Test that our service failed to load + assert not hass.services.has_service(BASE_COMPONENT, "test") + + +async def test_apprise_config_load_okay(hass, tmp_path): + """Test apprise configuration failures.""" + + # Test cases where our URL is invalid + d = tmp_path / "apprise-config" + d.mkdir() + f = d / "apprise" + f.write_text("mailto://user:pass@example.com/") + + config = {BASE_COMPONENT: {"name": "test", "platform": "apprise", "config": str(f)}} + + assert await async_setup_component(hass, BASE_COMPONENT, config) + await hass.async_block_till_done() + + # Valid configuration was loaded; our service is good + assert hass.services.has_service(BASE_COMPONENT, "test") + + +async def test_apprise_url_load_fail(hass): + """Test apprise url failure.""" + + config = { + BASE_COMPONENT: { + "name": "test", + "platform": "apprise", + "url": "mailto://user:pass@example.com", + } + } + with patch("apprise.Apprise.add", return_value=False): + assert await async_setup_component(hass, BASE_COMPONENT, config) + await hass.async_block_till_done() + + # Test that our service failed to load + assert not hass.services.has_service(BASE_COMPONENT, "test") + + +async def test_apprise_notification(hass): + """Test apprise notification.""" + + config = { + BASE_COMPONENT: { + "name": "test", + "platform": "apprise", + "url": "mailto://user:pass@example.com", + } + } + + # Our Message + data = {"title": "Test Title", "message": "Test Message"} + + with patch("apprise.Apprise") as mock_apprise: + obj = MagicMock() + obj.add.return_value = True + obj.notify.return_value = True + mock_apprise.return_value = obj + assert await async_setup_component(hass, BASE_COMPONENT, config) + await hass.async_block_till_done() + + # Test the existance of our service + assert hass.services.has_service(BASE_COMPONENT, "test") + + # Test the call to our underlining notify() call + await hass.services.async_call(BASE_COMPONENT, "test", data) + await hass.async_block_till_done() + + # Validate calls were made under the hood correctly + obj.add.assert_called_once_with([config[BASE_COMPONENT]["url"]]) + obj.notify.assert_called_once_with( + **{"body": data["message"], "title": data["title"], "tag": None} + ) + + +async def test_apprise_notification_with_target(hass, tmp_path): + """Test apprise notification with a target.""" + + # Test cases where our URL is invalid + d = tmp_path / "apprise-config" + d.mkdir() + f = d / "apprise" + + # Write 2 config entries each assigned to different tags + f.write_text("devops=mailto://user:pass@example.com/\r\n") + f.write_text("system,alert=syslog://\r\n") + + config = {BASE_COMPONENT: {"name": "test", "platform": "apprise", "config": str(f)}} + + # Our Message, only notify the services tagged with "devops" + data = {"title": "Test Title", "message": "Test Message", "target": ["devops"]} + + with patch("apprise.Apprise") as mock_apprise: + apprise_obj = MagicMock() + apprise_obj.add.return_value = True + apprise_obj.notify.return_value = True + mock_apprise.return_value = apprise_obj + assert await async_setup_component(hass, BASE_COMPONENT, config) + await hass.async_block_till_done() + + # Test the existance of our service + assert hass.services.has_service(BASE_COMPONENT, "test") + + # Test the call to our underlining notify() call + await hass.services.async_call(BASE_COMPONENT, "test", data) + await hass.async_block_till_done() + + # Validate calls were made under the hood correctly + apprise_obj.notify.assert_called_once_with( + **{"body": data["message"], "title": data["title"], "tag": data["target"]} + ) diff --git a/tests/components/auth/__init__.py b/tests/components/auth/__init__.py index e79d8a67845..5114e18889b 100644 --- a/tests/components/auth/__init__.py +++ b/tests/components/auth/__init__.py @@ -30,7 +30,7 @@ async def async_setup_auth( hass, provider_configs, module_configs ) ensure_auth_manager_loaded(hass.auth) - await async_setup_component(hass, "auth", {"http": {"api_password": "bla"}}) + await async_setup_component(hass, "auth", {}) if setup_api: await async_setup_component(hass, "api", {}) return await aiohttp_client(hass.http.app) diff --git a/tests/components/auth/test_init.py b/tests/components/auth/test_init.py index 80527c2636b..de91613b74b 100644 --- a/tests/components/auth/test_init.py +++ b/tests/components/auth/test_init.py @@ -103,7 +103,7 @@ def test_auth_code_store_expiration(): async def test_ws_current_user(hass, hass_ws_client, hass_access_token): """Test the current user command with homeassistant creds.""" - assert await async_setup_component(hass, "auth", {"http": {"api_password": "bla"}}) + assert await async_setup_component(hass, "auth", {}) refresh_token = await hass.auth.async_validate_access_token(hass_access_token) user = refresh_token.user diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 6acb40cec88..a0573ce7c1b 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -842,6 +842,25 @@ async def test_automation_with_error_in_script(hass, caplog): assert "Service not found" in caplog.text +async def test_automation_with_error_in_script_2(hass, caplog): + """Test automation with an error in script.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "alias": "hello", + "trigger": {"platform": "event", "event_type": "test_event"}, + "action": {"service": None, "entity_id": "hello.world"}, + } + }, + ) + + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert "string value is None" in caplog.text + + async def test_automation_restore_last_triggered_with_initial_state(hass): """Ensure last_triggered is restored, even when initial state is set.""" time = dt_util.utcnow() diff --git a/tests/components/automation/test_reproduce_state.py b/tests/components/automation/test_reproduce_state.py new file mode 100644 index 00000000000..4f3fd735fc5 --- /dev/null +++ b/tests/components/automation/test_reproduce_state.py @@ -0,0 +1,50 @@ +"""Test reproduce state for Automation.""" +from homeassistant.core import State + +from tests.common import async_mock_service + + +async def test_reproducing_states(hass, caplog): + """Test reproducing Automation states.""" + hass.states.async_set("automation.entity_off", "off", {}) + hass.states.async_set("automation.entity_on", "on", {}) + + turn_on_calls = async_mock_service(hass, "automation", "turn_on") + turn_off_calls = async_mock_service(hass, "automation", "turn_off") + + # These calls should do nothing as entities already in desired state + await hass.helpers.state.async_reproduce_state( + [State("automation.entity_off", "off"), State("automation.entity_on", "on")], + blocking=True, + ) + + assert len(turn_on_calls) == 0 + assert len(turn_off_calls) == 0 + + # Test invalid state is handled + await hass.helpers.state.async_reproduce_state( + [State("automation.entity_off", "not_supported")], blocking=True + ) + + assert "not_supported" in caplog.text + assert len(turn_on_calls) == 0 + assert len(turn_off_calls) == 0 + + # Make sure correct services are called + await hass.helpers.state.async_reproduce_state( + [ + State("automation.entity_on", "off"), + State("automation.entity_off", "on"), + # Should not raise + State("automation.non_existing", "on"), + ], + blocking=True, + ) + + assert len(turn_on_calls) == 1 + assert turn_on_calls[0].domain == "automation" + assert turn_on_calls[0].data == {"entity_id": "automation.entity_off"} + + assert len(turn_off_calls) == 1 + assert turn_off_calls[0].domain == "automation" + assert turn_off_calls[0].data == {"entity_id": "automation.entity_on"} diff --git a/tests/components/axis/test_config_flow.py b/tests/components/axis/test_config_flow.py index 5ec3f933e9e..5aec416961d 100644 --- a/tests/components/axis/test_config_flow.py +++ b/tests/components/axis/test_config_flow.py @@ -186,6 +186,7 @@ async def test_zeroconf_flow(hass): data={ config_flow.CONF_HOST: "1.2.3.4", config_flow.CONF_PORT: 80, + "hostname": "name", "properties": {"macaddress": "00408C12345"}, }, context={"source": "zeroconf"}, @@ -319,6 +320,7 @@ async def test_zeroconf_flow_bad_config_file(hass): config_flow.DOMAIN, data={ config_flow.CONF_HOST: "1.2.3.4", + "hostname": "name", "properties": {"macaddress": "00408C12345"}, }, context={"source": "zeroconf"}, diff --git a/tests/components/binary_sensor/test_device_condition.py b/tests/components/binary_sensor/test_device_condition.py index b5502d8fe3d..34cf4030a50 100644 --- a/tests/components/binary_sensor/test_device_condition.py +++ b/tests/components/binary_sensor/test_device_condition.py @@ -1,5 +1,7 @@ """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 @@ -7,6 +9,7 @@ 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.helpers import device_registry +import homeassistant.util.dt as dt_util from tests.common import ( MockConfigEntry, @@ -14,6 +17,7 @@ from tests.common import ( mock_device_registry, mock_registry, async_get_device_automations, + async_get_device_automation_capabilities, ) @@ -71,6 +75,28 @@ async def test_get_conditions(hass, device_reg, entity_reg): assert conditions == expected_conditions +async def test_get_condition_capabilities(hass, device_reg, entity_reg): + """Test we get the expected capabilities from a binary_sensor condition.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_capabilities = { + "extra_fields": [ + {"name": "for", "optional": True, "type": "positive_time_period_dict"} + ] + } + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + for condition in conditions: + capabilities = await async_get_device_automation_capabilities( + hass, "condition", condition + ) + assert capabilities == expected_capabilities + + async def test_if_state(hass, calls): """Test for turn_on and turn_off conditions.""" platform = getattr(hass.components, f"test.{DOMAIN}") @@ -131,7 +157,6 @@ async def test_if_state(hass, calls): assert len(calls) == 0 hass.bus.async_fire("test_event1") - hass.bus.async_fire("test_event2") await hass.async_block_till_done() assert len(calls) == 1 assert calls[0].data["some"] == "is_on event - test_event1" @@ -142,3 +167,73 @@ async def test_if_state(hass, calls): await hass.async_block_till_done() assert len(calls) == 2 assert calls[1].data["some"] == "is_off event - test_event2" + + +async def test_if_fires_on_for_condition(hass, calls): + """Test for firing if condition is on with delay.""" + point1 = dt_util.utcnow() + point2 = point1 + timedelta(seconds=10) + point3 = point2 + timedelta(seconds=10) + + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + sensor1 = platform.ENTITIES["battery"] + + with patch("homeassistant.core.dt_util.utcnow") as mock_utcnow: + mock_utcnow.return_value = point1 + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": sensor1.entity_id, + "type": "is_not_bat_low", + "for": {"seconds": 5}, + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "is_off {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ("platform", "event.event_type") + ) + }, + }, + } + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(sensor1.entity_id).state == STATE_ON + assert len(calls) == 0 + + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 0 + + # Time travel 10 secs into the future + mock_utcnow.return_value = point2 + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 0 + + hass.states.async_set(sensor1.entity_id, STATE_OFF) + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 0 + + # Time travel 20 secs into the future + mock_utcnow.return_value = point3 + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "is_off event - test_event1" diff --git a/tests/components/blackbird/test_media_player.py b/tests/components/blackbird/test_media_player.py index c41d5dcef41..34309fdbcf3 100644 --- a/tests/components/blackbird/test_media_player.py +++ b/tests/components/blackbird/test_media_player.py @@ -180,7 +180,10 @@ class TestBlackbirdMediaPlayer(unittest.TestCase): self.hass = tests.common.get_test_home_assistant() self.hass.start() # Note, source dictionary is unsorted! - with mock.patch("pyblackbird.get_blackbird", new=lambda *a: self.blackbird): + with mock.patch( + "homeassistant.components.blackbird.media_player.get_blackbird", + new=lambda *a: self.blackbird, + ): setup_platform( self.hass, { diff --git a/tests/components/buienradar/__init__.py b/tests/components/buienradar/__init__.py new file mode 100644 index 00000000000..15cdd8646d2 --- /dev/null +++ b/tests/components/buienradar/__init__.py @@ -0,0 +1 @@ +"""Tests for the buienradar component.""" diff --git a/tests/components/buienradar/test_sensor.py b/tests/components/buienradar/test_sensor.py new file mode 100644 index 00000000000..c1569e4576b --- /dev/null +++ b/tests/components/buienradar/test_sensor.py @@ -0,0 +1,26 @@ +"""The tests for the Buienradar sensor platform.""" +from homeassistant.setup import async_setup_component +from homeassistant.components import sensor + + +CONDITIONS = ["stationname", "temperature"] +BASE_CONFIG = { + "sensor": [ + { + "platform": "buienradar", + "name": "volkel", + "latitude": 51.65, + "longitude": 5.7, + "monitored_conditions": CONDITIONS, + } + ] +} + + +async def test_smoke_test_setup_component(hass): + """Smoke test for successfully set-up with default config.""" + assert await async_setup_component(hass, sensor.DOMAIN, BASE_CONFIG) + + for cond in CONDITIONS: + state = hass.states.get(f"sensor.volkel_{cond}") + assert state.state == "unknown" diff --git a/tests/components/buienradar/test_weather.py b/tests/components/buienradar/test_weather.py new file mode 100644 index 00000000000..1a8c94e1712 --- /dev/null +++ b/tests/components/buienradar/test_weather.py @@ -0,0 +1,25 @@ +"""The tests for the buienradar weather component.""" +from homeassistant.components import weather +from homeassistant.setup import async_setup_component + + +# Example config snippet from documentation. +BASE_CONFIG = { + "weather": [ + { + "platform": "buienradar", + "name": "volkel", + "latitude": 51.65, + "longitude": 5.7, + "forecast": True, + } + ] +} + + +async def test_smoke_test_setup_component(hass): + """Smoke test for successfully set-up with default config.""" + assert await async_setup_component(hass, weather.DOMAIN, BASE_CONFIG) + + state = hass.states.get("weather.volkel") + assert state.state == "unknown" diff --git a/tests/components/caldav/test_calendar.py b/tests/components/caldav/test_calendar.py index 209ab780265..c0be635988a 100644 --- a/tests/components/caldav/test_calendar.py +++ b/tests/components/caldav/test_calendar.py @@ -111,6 +111,19 @@ LOCATION:San Francisco DESCRIPTION:Sunny day END:VEVENT END:VCALENDAR +""", + """BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Global Corp.//CalDAV Client//EN +BEGIN:VEVENT +UID:8 +DTSTART:20171127T190000 +DTEND:20171127T200000 +SUMMARY:This is a floating Event +LOCATION:Hamburg +DESCRIPTION:What a day +END:VEVENT +END:VCALENDAR """, ] @@ -292,6 +305,29 @@ async def test_ongoing_event_different_tz(mock_now, hass, calendar): } +@patch("homeassistant.util.dt.now", return_value=_local_datetime(19, 10)) +async def test_ongoing_floating_event_returned(mock_now, hass, calendar): + """Test that floating events without timezones work.""" + assert await async_setup_component(hass, "calendar", {"calendar": CALDAV_CONFIG}) + await hass.async_block_till_done() + + state = hass.states.get("calendar.private") + print(dt.DEFAULT_TIME_ZONE) + print(state) + assert state.name == calendar.name + assert state.state == STATE_ON + assert dict(state.attributes) == { + "friendly_name": "Private", + "message": "This is a floating Event", + "all_day": False, + "offset_reached": False, + "start_time": "2017-11-27 19:00:00", + "end_time": "2017-11-27 20:00:00", + "location": "Hamburg", + "description": "What a day", + } + + @patch("homeassistant.util.dt.now", return_value=_local_datetime(8, 30)) async def test_ongoing_event_with_offset(mock_now, hass, calendar): """Test that the offset is taken into account.""" diff --git a/tests/components/cert_expiry/test_config_flow.py b/tests/components/cert_expiry/test_config_flow.py index f44e65512e3..3754551c230 100644 --- a/tests/components/cert_expiry/test_config_flow.py +++ b/tests/components/cert_expiry/test_config_flow.py @@ -1,11 +1,12 @@ """Tests for the Cert Expiry config flow.""" import pytest +import ssl import socket from unittest.mock import patch from homeassistant import data_entry_flow from homeassistant.components.cert_expiry import config_flow -from homeassistant.components.cert_expiry.const import DEFAULT_PORT +from homeassistant.components.cert_expiry.const import DEFAULT_NAME, DEFAULT_PORT from homeassistant.const import CONF_PORT, CONF_NAME, CONF_HOST from tests.common import MockConfigEntry, mock_coro @@ -45,7 +46,7 @@ async def test_user(hass, test_connect): {CONF_NAME: NAME, CONF_HOST: HOST, CONF_PORT: PORT} ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "cert_expiry_test_1_2_3" + assert result["title"] == NAME assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_PORT] == PORT @@ -57,21 +58,21 @@ async def test_import(hass, test_connect): # import with only host result = await flow.async_step_import({CONF_HOST: HOST}) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "ssl_certificate_expiry" + assert result["title"] == DEFAULT_NAME assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_PORT] == DEFAULT_PORT # import with host and name result = await flow.async_step_import({CONF_HOST: HOST, CONF_NAME: NAME}) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "cert_expiry_test_1_2_3" + assert result["title"] == NAME assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_PORT] == DEFAULT_PORT # improt with host and port result = await flow.async_step_import({CONF_HOST: HOST, CONF_PORT: PORT}) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "ssl_certificate_expiry" + assert result["title"] == DEFAULT_NAME assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_PORT] == PORT @@ -80,7 +81,7 @@ async def test_import(hass, test_connect): {CONF_HOST: HOST, CONF_PORT: PORT, CONF_NAME: NAME} ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "cert_expiry_test_1_2_3" + assert result["title"] == NAME assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_PORT] == PORT @@ -112,7 +113,7 @@ async def test_abort_if_already_setup(hass, test_connect): {CONF_HOST: HOST, CONF_NAME: NAME, CONF_PORT: 888} ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "cert_expiry_test_1_2_3" + assert result["title"] == NAME assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_PORT] == 888 @@ -131,7 +132,22 @@ async def test_abort_on_socket_failed(hass): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {CONF_HOST: "connection_timeout"} - with patch("socket.create_connection", side_effect=OSError()): + with patch( + "socket.create_connection", + side_effect=ssl.CertificateError(f"{HOST} doesn't match somethingelse.com"), + ): result = await flow.async_step_user({CONF_HOST: HOST}) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {CONF_HOST: "certificate_fetch_failed"} + assert result["errors"] == {CONF_HOST: "wrong_host"} + + with patch( + "socket.create_connection", side_effect=ssl.CertificateError("different error") + ): + result = await flow.async_step_user({CONF_HOST: HOST}) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {CONF_HOST: "certificate_error"} + + with patch("socket.create_connection", side_effect=ssl.SSLError()): + result = await flow.async_step_user({CONF_HOST: HOST}) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {CONF_HOST: "certificate_error"} diff --git a/tests/components/cloud/__init__.py b/tests/components/cloud/__init__.py index a0196cae32a..45ea4e43ee4 100644 --- a/tests/components/cloud/__init__.py +++ b/tests/components/cloud/__init__.py @@ -25,4 +25,4 @@ def mock_cloud_prefs(hass, prefs={}): } prefs_to_set.update(prefs) hass.data[cloud.DOMAIN].client._prefs._prefs = prefs_to_set - return prefs_to_set + return hass.data[cloud.DOMAIN].client._prefs diff --git a/tests/components/cloud/test_client.py b/tests/components/cloud/test_client.py index b7ac5f4cffd..054b38daffc 100644 --- a/tests/components/cloud/test_client.py +++ b/tests/components/cloud/test_client.py @@ -61,7 +61,7 @@ async def test_handler_alexa(hass): async def test_handler_alexa_disabled(hass, mock_cloud_fixture): """Test handler Alexa when user has disabled it.""" - mock_cloud_fixture[PREF_ENABLE_ALEXA] = False + mock_cloud_fixture._prefs[PREF_ENABLE_ALEXA] = False cloud = hass.data["cloud"] resp = await cloud.client.async_alexa_message( @@ -125,7 +125,7 @@ async def test_handler_google_actions(hass): async def test_handler_google_actions_disabled(hass, mock_cloud_fixture): """Test handler Google Actions when user has disabled it.""" - mock_cloud_fixture[PREF_ENABLE_GOOGLE] = False + mock_cloud_fixture._prefs[PREF_ENABLE_GOOGLE] = False with patch("hass_nabucasa.Cloud.start", return_value=mock_coro()): assert await async_setup_component(hass, "cloud", {}) diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 8e03fb82b2c..314db3a9e88 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -10,13 +10,7 @@ from hass_nabucasa.const import STATE_CONNECTED from homeassistant.core import State from homeassistant.auth.providers import trusted_networks as tn_auth -from homeassistant.components.cloud.const import ( - PREF_ENABLE_GOOGLE, - PREF_ENABLE_ALEXA, - PREF_GOOGLE_SECURE_DEVICES_PIN, - DOMAIN, - RequireRelink, -) +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 @@ -474,9 +468,9 @@ async def test_websocket_update_preferences( hass, hass_ws_client, aioclient_mock, setup_api, mock_cloud_login ): """Test updating preference.""" - assert setup_api[PREF_ENABLE_GOOGLE] - assert setup_api[PREF_ENABLE_ALEXA] - assert setup_api[PREF_GOOGLE_SECURE_DEVICES_PIN] is None + assert setup_api.google_enabled + assert setup_api.alexa_enabled + assert setup_api.google_secure_devices_pin is None client = await hass_ws_client(hass) await client.send_json( { @@ -490,9 +484,9 @@ async def test_websocket_update_preferences( response = await client.receive_json() assert response["success"] - assert not setup_api[PREF_ENABLE_GOOGLE] - assert not setup_api[PREF_ENABLE_ALEXA] - assert setup_api[PREF_GOOGLE_SECURE_DEVICES_PIN] == "1234" + assert not setup_api.google_enabled + assert not setup_api.alexa_enabled + assert setup_api.google_secure_devices_pin == "1234" async def test_websocket_update_preferences_require_relink( diff --git a/tests/components/conftest.py b/tests/components/conftest.py index 8c2515939f3..4f1f3e64e02 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -40,7 +40,9 @@ def hass_ws_client(aiohttp_client, hass_access_token): assert auth_resp["type"] == TYPE_AUTH_REQUIRED if access_token is None: - await websocket.send_json({"type": TYPE_AUTH, "api_password": "bla"}) + await websocket.send_json( + {"type": TYPE_AUTH, "access_token": "incorrect"} + ) else: await websocket.send_json( {"type": TYPE_AUTH, "access_token": access_token} diff --git a/tests/components/conversation/test_init.py b/tests/components/conversation/test_init.py index d4142f2ce5f..a9116ac0d98 100644 --- a/tests/components/conversation/test_init.py +++ b/tests/components/conversation/test_init.py @@ -263,54 +263,27 @@ async def test_http_api_wrong_data(hass, hass_client): assert resp.status == 400 -def test_create_matcher(): - """Test the create matcher method.""" - # Basic sentence - pattern = conversation.create_matcher("Hello world") - assert pattern.match("Hello world") is not None +async def test_custom_agent(hass, hass_client): + """Test a custom conversation agent.""" - # Match a part - pattern = conversation.create_matcher("Hello {name}") - match = pattern.match("hello world") - assert match is not None - assert match.groupdict()["name"] == "world" - no_match = pattern.match("Hello world, how are you?") - assert no_match is None + class MyAgent(conversation.AbstractConversationAgent): + """Test Agent.""" - # Optional and matching part - pattern = conversation.create_matcher("Turn on [the] {name}") - match = pattern.match("turn on the kitchen lights") - assert match is not None - assert match.groupdict()["name"] == "kitchen lights" - match = pattern.match("turn on kitchen lights") - assert match is not None - assert match.groupdict()["name"] == "kitchen lights" - match = pattern.match("turn off kitchen lights") - assert match is None + async def async_process(self, text): + """Process some text.""" + response = intent.IntentResponse() + response.async_set_speech("Test response") + return response - # Two different optional parts, 1 matching part - pattern = conversation.create_matcher("Turn on [the] [a] {name}") - match = pattern.match("turn on the kitchen lights") - assert match is not None - assert match.groupdict()["name"] == "kitchen lights" - match = pattern.match("turn on kitchen lights") - assert match is not None - assert match.groupdict()["name"] == "kitchen lights" - match = pattern.match("turn on a kitchen light") - assert match is not None - assert match.groupdict()["name"] == "kitchen light" + conversation.async_set_agent(hass, MyAgent()) - # Strip plural - pattern = conversation.create_matcher("Turn {name}[s] on") - match = pattern.match("turn kitchen lights on") - assert match is not None - assert match.groupdict()["name"] == "kitchen light" + assert await async_setup_component(hass, "conversation", {}) - # Optional 2 words - pattern = conversation.create_matcher("Turn [the great] {name} on") - match = pattern.match("turn the great kitchen lights on") - assert match is not None - assert match.groupdict()["name"] == "kitchen lights" - match = pattern.match("turn kitchen lights on") - assert match is not None - assert match.groupdict()["name"] == "kitchen lights" + client = await hass_client() + + resp = await client.post("/api/conversation/process", json={"text": "Test Text"}) + assert resp.status == 200 + assert await resp.json() == { + "card": {}, + "speech": {"plain": {"extra_data": None, "speech": "Test response"}}, + } diff --git a/tests/components/conversation/test_util.py b/tests/components/conversation/test_util.py new file mode 100644 index 00000000000..2fa4527e9b1 --- /dev/null +++ b/tests/components/conversation/test_util.py @@ -0,0 +1,55 @@ +"""Test the conversation utils.""" +from homeassistant.components.conversation.util import create_matcher + + +def test_create_matcher(): + """Test the create matcher method.""" + # Basic sentence + pattern = create_matcher("Hello world") + assert pattern.match("Hello world") is not None + + # Match a part + pattern = create_matcher("Hello {name}") + match = pattern.match("hello world") + assert match is not None + assert match.groupdict()["name"] == "world" + no_match = pattern.match("Hello world, how are you?") + assert no_match is None + + # Optional and matching part + pattern = create_matcher("Turn on [the] {name}") + match = pattern.match("turn on the kitchen lights") + assert match is not None + assert match.groupdict()["name"] == "kitchen lights" + match = pattern.match("turn on kitchen lights") + assert match is not None + assert match.groupdict()["name"] == "kitchen lights" + match = pattern.match("turn off kitchen lights") + assert match is None + + # Two different optional parts, 1 matching part + pattern = create_matcher("Turn on [the] [a] {name}") + match = pattern.match("turn on the kitchen lights") + assert match is not None + assert match.groupdict()["name"] == "kitchen lights" + match = pattern.match("turn on kitchen lights") + assert match is not None + assert match.groupdict()["name"] == "kitchen lights" + match = pattern.match("turn on a kitchen light") + assert match is not None + assert match.groupdict()["name"] == "kitchen light" + + # Strip plural + pattern = create_matcher("Turn {name}[s] on") + match = pattern.match("turn kitchen lights on") + assert match is not None + assert match.groupdict()["name"] == "kitchen light" + + # Optional 2 words + pattern = create_matcher("Turn [the great] {name} on") + match = pattern.match("turn the great kitchen lights on") + assert match is not None + assert match.groupdict()["name"] == "kitchen lights" + match = pattern.match("turn kitchen lights on") + assert match is not None + assert match.groupdict()["name"] == "kitchen lights" diff --git a/tests/components/coolmaster/__init__.py b/tests/components/coolmaster/__init__.py new file mode 100644 index 00000000000..a7e1bf08c99 --- /dev/null +++ b/tests/components/coolmaster/__init__.py @@ -0,0 +1 @@ +"""Tests for the Coolmaster component.""" diff --git a/tests/components/coolmaster/test_config_flow.py b/tests/components/coolmaster/test_config_flow.py new file mode 100644 index 00000000000..d49858fcf05 --- /dev/null +++ b/tests/components/coolmaster/test_config_flow.py @@ -0,0 +1,104 @@ +"""Test the Coolmaster config flow.""" +from unittest.mock import patch + +from homeassistant import config_entries, setup +from homeassistant.components.coolmaster.const import DOMAIN, AVAILABLE_MODES + +# from homeassistant.components.coolmaster.config_flow import validate_connection + +from tests.common import mock_coro + + +def _flow_data(): + options = {"host": "1.1.1.1"} + for mode in AVAILABLE_MODES: + options[mode] = True + return options + + +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"] is None + + with patch( + "homeassistant.components.coolmaster.config_flow.validate_connection", + return_value=mock_coro(True), + ), patch( + "homeassistant.components.coolmaster.async_setup", return_value=mock_coro(True) + ) as mock_setup, patch( + "homeassistant.components.coolmaster.async_setup_entry", + return_value=mock_coro(True), + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], _flow_data() + ) + + assert result2["type"] == "create_entry" + assert result2["title"] == "1.1.1.1" + assert result2["data"] == { + "host": "1.1.1.1", + "port": 10102, + "supported_modes": AVAILABLE_MODES, + } + 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_timeout(hass): + """Test we handle a connection timeout.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.coolmaster.config_flow.validate_connection", + side_effect=TimeoutError(), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], _flow_data() + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "connection_error"} + + +async def test_form_connection_refused(hass): + """Test we handle a connection error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.coolmaster.config_flow.validate_connection", + side_effect=ConnectionRefusedError(), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], _flow_data() + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "connection_error"} + + +async def test_form_no_units(hass): + """Test we handle no units found.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.coolmaster.config_flow.validate_connection", + return_value=mock_coro(False), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], _flow_data() + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "no_units"} diff --git a/tests/components/counter/test_init.py b/tests/components/counter/test_init.py index 664f4d014b7..8ce90e164b6 100644 --- a/tests/components/counter/test_init.py +++ b/tests/components/counter/test_init.py @@ -174,14 +174,22 @@ def test_initial_state_overrules_restore_state(hass): @asyncio.coroutine def test_restore_state_overrules_initial_state(hass): """Ensure states are restored on startup.""" + + attr = {"initial": 6, "minimum": 1, "maximum": 8, "step": 2} + mock_restore_cache( - hass, (State("counter.test1", "11"), State("counter.test2", "-22")) + hass, + ( + State("counter.test1", "11"), + State("counter.test2", "-22"), + State("counter.test3", "5", attr), + ), ) hass.state = CoreState.starting yield from async_setup_component( - hass, DOMAIN, {DOMAIN: {"test1": {}, "test2": {CONF_INITIAL: 10}}} + hass, DOMAIN, {DOMAIN: {"test1": {}, "test2": {CONF_INITIAL: 10}, "test3": {}}} ) state = hass.states.get("counter.test1") @@ -192,6 +200,14 @@ def test_restore_state_overrules_initial_state(hass): assert state assert int(state.state) == -22 + state = hass.states.get("counter.test3") + assert state + assert int(state.state) == 5 + assert state.attributes.get("initial") == 6 + assert state.attributes.get("minimum") == 1 + assert state.attributes.get("maximum") == 8 + assert state.attributes.get("step") == 2 + @asyncio.coroutine def test_no_initial_state_and_no_restore_state(hass): @@ -379,11 +395,45 @@ async def test_configure(hass, hass_admin_user): assert state.state == "5" assert 3 == state.attributes.get("step") + # update value + await hass.services.async_call( + "counter", + "configure", + {"entity_id": state.entity_id, "value": 6}, + True, + Context(user_id=hass_admin_user.id), + ) + + state = hass.states.get("counter.test") + assert state is not None + assert state.state == "6" + + # update initial + await hass.services.async_call( + "counter", + "configure", + {"entity_id": state.entity_id, "initial": 5}, + True, + Context(user_id=hass_admin_user.id), + ) + + state = hass.states.get("counter.test") + assert state is not None + assert state.state == "6" + assert 5 == state.attributes.get("initial") + # update all await hass.services.async_call( "counter", "configure", - {"entity_id": state.entity_id, "step": 5, "minimum": 0, "maximum": 9}, + { + "entity_id": state.entity_id, + "step": 5, + "minimum": 0, + "maximum": 9, + "value": 5, + "initial": 6, + }, True, Context(user_id=hass_admin_user.id), ) @@ -394,3 +444,4 @@ async def test_configure(hass, hass_admin_user): assert 5 == state.attributes.get("step") assert 0 == state.attributes.get("minimum") assert 9 == state.attributes.get("maximum") + assert 6 == state.attributes.get("initial") diff --git a/tests/components/counter/test_reproduce_state.py b/tests/components/counter/test_reproduce_state.py new file mode 100644 index 00000000000..aa2c5ddbd9a --- /dev/null +++ b/tests/components/counter/test_reproduce_state.py @@ -0,0 +1,71 @@ +"""Test reproduce state for Counter.""" +from homeassistant.core import State + +from tests.common import async_mock_service + + +async def test_reproducing_states(hass, caplog): + """Test reproducing Counter states.""" + hass.states.async_set("counter.entity", "5", {}) + hass.states.async_set( + "counter.entity_attr", + "8", + {"initial": 12, "minimum": 5, "maximum": 15, "step": 3}, + ) + + configure_calls = async_mock_service(hass, "counter", "configure") + + # These calls should do nothing as entities already in desired state + await hass.helpers.state.async_reproduce_state( + [ + State("counter.entity", "5"), + State( + "counter.entity_attr", + "8", + {"initial": 12, "minimum": 5, "maximum": 15, "step": 3}, + ), + ], + blocking=True, + ) + + assert len(configure_calls) == 0 + + # Test invalid state is handled + await hass.helpers.state.async_reproduce_state( + [State("counter.entity", "not_supported")], blocking=True + ) + + assert "not_supported" in caplog.text + assert len(configure_calls) == 0 + + # Make sure correct services are called + await hass.helpers.state.async_reproduce_state( + [ + State("counter.entity", "2"), + State( + "counter.entity_attr", + "7", + {"initial": 10, "minimum": 3, "maximum": 21, "step": 5}, + ), + # Should not raise + State("counter.non_existing", "6"), + ], + blocking=True, + ) + + valid_calls = [ + {"entity_id": "counter.entity", "value": "2"}, + { + "entity_id": "counter.entity_attr", + "value": "7", + "initial": 10, + "minimum": 3, + "maximum": 21, + "step": 5, + }, + ] + assert len(configure_calls) == 2 + for call in configure_calls: + assert call.domain == "counter" + assert call.data in valid_calls + valid_calls.remove(call.data) diff --git a/tests/components/cover/test_device_condition.py b/tests/components/cover/test_device_condition.py new file mode 100644 index 00000000000..494368f76ff --- /dev/null +++ b/tests/components/cover/test_device_condition.py @@ -0,0 +1,190 @@ +"""The tests for Cover device conditions.""" +import pytest + +from homeassistant.components.cover import DOMAIN +from homeassistant.const import STATE_OPEN, STATE_CLOSED, STATE_OPENING, STATE_CLOSING +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + assert_lists_same, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@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 serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_conditions(hass, device_reg, entity_reg): + """Test we get the expected conditions from a cover.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_conditions = [ + { + "condition": "device", + "domain": DOMAIN, + "type": "is_open", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_closed", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_opening", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_closing", + "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("cover.entity", STATE_OPEN) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "cover.entity", + "type": "is_open", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_open - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event2"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "cover.entity", + "type": "is_closed", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_closed - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event3"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "cover.entity", + "type": "is_opening", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_opening - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event4"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "cover.entity", + "type": "is_closing", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_closing - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + ] + }, + ) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "is_open - event - test_event1" + + hass.states.async_set("cover.entity", STATE_CLOSED) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "is_closed - event - test_event2" + + hass.states.async_set("cover.entity", STATE_OPENING) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event3") + await hass.async_block_till_done() + assert len(calls) == 3 + assert calls[2].data["some"] == "is_opening - event - test_event3" + + hass.states.async_set("cover.entity", STATE_CLOSING) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event4") + await hass.async_block_till_done() + assert len(calls) == 4 + assert calls[3].data["some"] == "is_closing - event - test_event4" diff --git a/tests/components/cover/test_reproduce_state.py b/tests/components/cover/test_reproduce_state.py new file mode 100644 index 00000000000..39fdf3d3992 --- /dev/null +++ b/tests/components/cover/test_reproduce_state.py @@ -0,0 +1,198 @@ +"""Test reproduce state for Cover.""" +from homeassistant.components.cover import ( + ATTR_CURRENT_POSITION, + ATTR_CURRENT_TILT_POSITION, + ATTR_POSITION, + ATTR_TILT_POSITION, +) +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, + STATE_CLOSED, + STATE_OPEN, +) +from homeassistant.core import State +from tests.common import async_mock_service + + +async def test_reproducing_states(hass, caplog): + """Test reproducing Cover states.""" + hass.states.async_set("cover.entity_close", STATE_CLOSED, {}) + hass.states.async_set( + "cover.entity_close_attr", + STATE_CLOSED, + {ATTR_CURRENT_POSITION: 0, ATTR_CURRENT_TILT_POSITION: 0}, + ) + hass.states.async_set( + "cover.entity_close_tilt", STATE_CLOSED, {ATTR_CURRENT_TILT_POSITION: 50} + ) + hass.states.async_set("cover.entity_open", STATE_OPEN, {}) + hass.states.async_set( + "cover.entity_slightly_open", STATE_OPEN, {ATTR_CURRENT_POSITION: 50} + ) + hass.states.async_set( + "cover.entity_open_attr", + STATE_OPEN, + {ATTR_CURRENT_POSITION: 100, ATTR_CURRENT_TILT_POSITION: 0}, + ) + hass.states.async_set( + "cover.entity_open_tilt", + STATE_OPEN, + {ATTR_CURRENT_POSITION: 50, ATTR_CURRENT_TILT_POSITION: 50}, + ) + hass.states.async_set( + "cover.entity_entirely_open", + STATE_OPEN, + {ATTR_CURRENT_POSITION: 100, ATTR_CURRENT_TILT_POSITION: 100}, + ) + + close_calls = async_mock_service(hass, "cover", SERVICE_CLOSE_COVER) + open_calls = async_mock_service(hass, "cover", SERVICE_OPEN_COVER) + close_tilt_calls = async_mock_service(hass, "cover", SERVICE_CLOSE_COVER_TILT) + open_tilt_calls = async_mock_service(hass, "cover", SERVICE_OPEN_COVER_TILT) + position_calls = async_mock_service(hass, "cover", SERVICE_SET_COVER_POSITION) + position_tilt_calls = async_mock_service( + hass, "cover", SERVICE_SET_COVER_TILT_POSITION + ) + + # These calls should do nothing as entities already in desired state + await hass.helpers.state.async_reproduce_state( + [ + State("cover.entity_close", STATE_CLOSED), + State( + "cover.entity_close_attr", + STATE_CLOSED, + {ATTR_CURRENT_POSITION: 0, ATTR_CURRENT_TILT_POSITION: 0}, + ), + State( + "cover.entity_close_tilt", + STATE_CLOSED, + {ATTR_CURRENT_TILT_POSITION: 50}, + ), + State("cover.entity_open", STATE_OPEN), + State( + "cover.entity_slightly_open", STATE_OPEN, {ATTR_CURRENT_POSITION: 50} + ), + State( + "cover.entity_open_attr", + STATE_OPEN, + {ATTR_CURRENT_POSITION: 100, ATTR_CURRENT_TILT_POSITION: 0}, + ), + State( + "cover.entity_open_tilt", + STATE_OPEN, + {ATTR_CURRENT_POSITION: 50, ATTR_CURRENT_TILT_POSITION: 50}, + ), + State( + "cover.entity_entirely_open", + STATE_OPEN, + {ATTR_CURRENT_POSITION: 100, ATTR_CURRENT_TILT_POSITION: 100}, + ), + ], + blocking=True, + ) + + assert len(close_calls) == 0 + assert len(open_calls) == 0 + assert len(close_tilt_calls) == 0 + assert len(open_tilt_calls) == 0 + assert len(position_calls) == 0 + assert len(position_tilt_calls) == 0 + + # Test invalid state is handled + await hass.helpers.state.async_reproduce_state( + [State("cover.entity_close", "not_supported")], blocking=True + ) + + assert "not_supported" in caplog.text + assert len(close_calls) == 0 + assert len(open_calls) == 0 + assert len(close_tilt_calls) == 0 + assert len(open_tilt_calls) == 0 + assert len(position_calls) == 0 + assert len(position_tilt_calls) == 0 + + # Make sure correct services are called + await hass.helpers.state.async_reproduce_state( + [ + State("cover.entity_close", STATE_OPEN), + State( + "cover.entity_close_attr", + STATE_OPEN, + {ATTR_CURRENT_POSITION: 50, ATTR_CURRENT_TILT_POSITION: 50}, + ), + State( + "cover.entity_close_tilt", + STATE_CLOSED, + {ATTR_CURRENT_TILT_POSITION: 100}, + ), + State("cover.entity_open", STATE_CLOSED), + State("cover.entity_slightly_open", STATE_OPEN, {}), + State("cover.entity_open_attr", STATE_CLOSED, {}), + State( + "cover.entity_open_tilt", STATE_OPEN, {ATTR_CURRENT_TILT_POSITION: 0} + ), + State( + "cover.entity_entirely_open", + STATE_CLOSED, + {ATTR_CURRENT_POSITION: 0, ATTR_CURRENT_TILT_POSITION: 0}, + ), + # Should not raise + State("cover.non_existing", "on"), + ], + blocking=True, + ) + + valid_close_calls = [ + {"entity_id": "cover.entity_open"}, + {"entity_id": "cover.entity_open_attr"}, + {"entity_id": "cover.entity_entirely_open"}, + ] + assert len(close_calls) == 3 + for call in close_calls: + assert call.domain == "cover" + assert call.data in valid_close_calls + valid_close_calls.remove(call.data) + + valid_open_calls = [ + {"entity_id": "cover.entity_close"}, + {"entity_id": "cover.entity_slightly_open"}, + {"entity_id": "cover.entity_open_tilt"}, + ] + assert len(open_calls) == 3 + for call in open_calls: + assert call.domain == "cover" + assert call.data in valid_open_calls + valid_open_calls.remove(call.data) + + valid_close_tilt_calls = [ + {"entity_id": "cover.entity_open_tilt"}, + {"entity_id": "cover.entity_entirely_open"}, + ] + assert len(close_tilt_calls) == 2 + for call in close_tilt_calls: + assert call.domain == "cover" + assert call.data in valid_close_tilt_calls + valid_close_tilt_calls.remove(call.data) + + assert len(open_tilt_calls) == 1 + assert open_tilt_calls[0].domain == "cover" + assert open_tilt_calls[0].data == {"entity_id": "cover.entity_close_tilt"} + + assert len(position_calls) == 1 + assert position_calls[0].domain == "cover" + assert position_calls[0].data == { + "entity_id": "cover.entity_close_attr", + ATTR_POSITION: 50, + } + + assert len(position_tilt_calls) == 1 + assert position_tilt_calls[0].domain == "cover" + assert position_tilt_calls[0].data == { + "entity_id": "cover.entity_close_attr", + ATTR_TILT_POSITION: 50, + } diff --git a/tests/components/darksky/test_sensor.py b/tests/components/darksky/test_sensor.py index 23d16ed35f4..be66b74c186 100644 --- a/tests/components/darksky/test_sensor.py +++ b/tests/components/darksky/test_sensor.py @@ -112,7 +112,10 @@ class TestDarkSkySetup(unittest.TestCase): self.hass.stop() @MockDependency("forecastio") - @patch("forecastio.load_forecast", new=load_forecastMock) + @patch( + "homeassistant.components.darksky.sensor.forecastio.load_forecast", + new=load_forecastMock, + ) def test_setup_with_config(self, mock_forecastio): """Test the platform setup with configuration.""" setup_component(self.hass, "sensor", VALID_CONFIG_MINIMAL) @@ -120,9 +123,7 @@ class TestDarkSkySetup(unittest.TestCase): state = self.hass.states.get("sensor.dark_sky_summary") assert state is not None - @MockDependency("forecastio") - @patch("forecastio.load_forecast", new=load_forecastMock) - def test_setup_with_invalid_config(self, mock_forecastio): + def test_setup_with_invalid_config(self): """Test the platform setup with invalid configuration.""" setup_component(self.hass, "sensor", INVALID_CONFIG_MINIMAL) @@ -130,7 +131,10 @@ class TestDarkSkySetup(unittest.TestCase): assert state is None @MockDependency("forecastio") - @patch("forecastio.load_forecast", new=load_forecastMock) + @patch( + "homeassistant.components.darksky.sensor.forecastio.load_forecast", + new=load_forecastMock, + ) def test_setup_with_language_config(self, mock_forecastio): """Test the platform setup with language configuration.""" setup_component(self.hass, "sensor", VALID_CONFIG_LANG_DE) @@ -138,9 +142,7 @@ class TestDarkSkySetup(unittest.TestCase): state = self.hass.states.get("sensor.dark_sky_summary") assert state is not None - @MockDependency("forecastio") - @patch("forecastio.load_forecast", new=load_forecastMock) - def test_setup_with_invalid_language_config(self, mock_forecastio): + def test_setup_with_invalid_language_config(self): """Test the platform setup with language configuration.""" setup_component(self.hass, "sensor", INVALID_CONFIG_LANG) @@ -164,7 +166,10 @@ class TestDarkSkySetup(unittest.TestCase): assert not response @MockDependency("forecastio") - @patch("forecastio.load_forecast", new=load_forecastMock) + @patch( + "homeassistant.components.darksky.sensor.forecastio.load_forecast", + new=load_forecastMock, + ) def test_setup_with_alerts_config(self, mock_forecastio): """Test the platform setup with alert configuration.""" setup_component(self.hass, "sensor", VALID_CONFIG_ALERTS) diff --git a/tests/components/darksky/test_weather.py b/tests/components/darksky/test_weather.py index ea28d3facb9..ca328f45839 100644 --- a/tests/components/darksky/test_weather.py +++ b/tests/components/darksky/test_weather.py @@ -6,6 +6,8 @@ from unittest.mock import patch import forecastio 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 @@ -48,3 +50,16 @@ class TestDarkSky(unittest.TestCase): state = self.hass.states.get("weather.test") assert state.state == "sunny" + + @patch("forecastio.load_forecast", side_effect=ConnectionError()) + def test_failed_setup(self, mock_load_forecast): + """Test to ensure that a network error does not break component state.""" + + assert setup_component( + self.hass, + weather.DOMAIN, + {"weather": {"name": "test", "platform": "darksky", "api_key": "foo"}}, + ) + + state = self.hass.states.get("weather.test") + assert state.state == "unavailable" diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index d0423c394a6..4045201bd18 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -387,7 +387,7 @@ async def test_hassio_confirm(hass): async def test_option_flow(hass): - """Test config flow selection of one of two bridges.""" + """Test config flow options.""" entry = MockConfigEntry(domain=config_flow.DOMAIN, data={}, options=None) hass.config_entries._entries.append(entry) diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index fa78ae94416..3c0e3b1eca7 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -170,6 +170,204 @@ async def test_websocket_get_triggers(hass, hass_ws_client, device_reg, entity_r assert _same_lists(triggers, expected_triggers) +async def test_websocket_get_action_capabilities( + hass, hass_ws_client, device_reg, entity_reg +): + """Test we get the expected action capabilities for an alarm through websocket.""" + await async_setup_component(hass, "device_automation", {}) + 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( + "alarm_control_panel", "test", "5678", device_id=device_entry.id + ) + expected_capabilities = { + "arm_away": {"extra_fields": []}, + "arm_home": {"extra_fields": []}, + "arm_night": {"extra_fields": []}, + "disarm": { + "extra_fields": [{"name": "code", "optional": True, "type": "string"}] + }, + "trigger": {"extra_fields": []}, + } + + client = await hass_ws_client(hass) + await client.send_json( + {"id": 1, "type": "device_automation/action/list", "device_id": device_entry.id} + ) + msg = await client.receive_json() + + assert msg["id"] == 1 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + actions = msg["result"] + + id = 2 + assert len(actions) == 5 + for action in actions: + await client.send_json( + { + "id": id, + "type": "device_automation/action/capabilities", + "action": action, + } + ) + msg = await client.receive_json() + assert msg["id"] == id + assert msg["type"] == TYPE_RESULT + assert msg["success"] + capabilities = msg["result"] + assert capabilities == expected_capabilities[action["type"]] + id = id + 1 + + +async def test_websocket_get_bad_action_capabilities( + hass, hass_ws_client, device_reg, entity_reg +): + """Test we get no action capabilities for a non existing domain.""" + await async_setup_component(hass, "device_automation", {}) + expected_capabilities = {} + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 1, + "type": "device_automation/action/capabilities", + "action": {"domain": "beer"}, + } + ) + msg = await client.receive_json() + assert msg["id"] == 1 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + capabilities = msg["result"] + assert capabilities == expected_capabilities + + +async def test_websocket_get_no_action_capabilities( + hass, hass_ws_client, device_reg, entity_reg +): + """Test we get no action capabilities for a domain with no device action capabilities.""" + await async_setup_component(hass, "device_automation", {}) + expected_capabilities = {} + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 1, + "type": "device_automation/action/capabilities", + "action": {"domain": "deconz"}, + } + ) + msg = await client.receive_json() + assert msg["id"] == 1 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + capabilities = msg["result"] + assert capabilities == expected_capabilities + + +async def test_websocket_get_condition_capabilities( + hass, hass_ws_client, device_reg, entity_reg +): + """Test we get the expected condition capabilities for a light through websocket.""" + await async_setup_component(hass, "device_automation", {}) + 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("light", "test", "5678", device_id=device_entry.id) + expected_capabilities = { + "extra_fields": [ + {"name": "for", "optional": True, "type": "positive_time_period_dict"} + ] + } + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 1, + "type": "device_automation/condition/list", + "device_id": device_entry.id, + } + ) + msg = await client.receive_json() + + assert msg["id"] == 1 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + conditions = msg["result"] + + id = 2 + assert len(conditions) == 2 + for condition in conditions: + await client.send_json( + { + "id": id, + "type": "device_automation/condition/capabilities", + "condition": condition, + } + ) + msg = await client.receive_json() + assert msg["id"] == id + assert msg["type"] == TYPE_RESULT + assert msg["success"] + capabilities = msg["result"] + assert capabilities == expected_capabilities + id = id + 1 + + +async def test_websocket_get_bad_condition_capabilities( + hass, hass_ws_client, device_reg, entity_reg +): + """Test we get no condition capabilities for a non existing domain.""" + await async_setup_component(hass, "device_automation", {}) + expected_capabilities = {} + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 1, + "type": "device_automation/condition/capabilities", + "condition": {"domain": "beer"}, + } + ) + msg = await client.receive_json() + assert msg["id"] == 1 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + capabilities = msg["result"] + assert capabilities == expected_capabilities + + +async def test_websocket_get_no_condition_capabilities( + hass, hass_ws_client, device_reg, entity_reg +): + """Test we get no condition capabilities for a domain with no device condition capabilities.""" + await async_setup_component(hass, "device_automation", {}) + expected_capabilities = {} + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 1, + "type": "device_automation/condition/capabilities", + "condition": {"domain": "deconz"}, + } + ) + msg = await client.receive_json() + assert msg["id"] == 1 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + capabilities = msg["result"] + assert capabilities == expected_capabilities + + async def test_websocket_get_trigger_capabilities( hass, hass_ws_client, device_reg, entity_reg ): @@ -204,6 +402,7 @@ async def test_websocket_get_trigger_capabilities( triggers = msg["result"] id = 2 + assert len(triggers) == 2 for trigger in triggers: await client.send_json( { diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index 57dfa183feb..195345dd489 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -11,9 +11,11 @@ from decimal import Decimal from unittest.mock import Mock import asynctest +import pytest + from homeassistant.bootstrap import async_setup_component from homeassistant.components.dsmr.sensor import DerivativeDSMREntity -import pytest + from tests.common import assert_setup_component @@ -34,10 +36,11 @@ def mock_connection_factory(monkeypatch): # apply the mock to both connection factories monkeypatch.setattr( - "dsmr_parser.clients.protocol.create_dsmr_reader", connection_factory + "homeassistant.components.dsmr.sensor.create_dsmr_reader", connection_factory ) monkeypatch.setattr( - "dsmr_parser.clients.protocol.create_tcp_dsmr_reader", connection_factory + "homeassistant.components.dsmr.sensor.create_tcp_dsmr_reader", + connection_factory, ) return connection_factory, transport, protocol @@ -158,7 +161,8 @@ def test_connection_errors_retry(hass, monkeypatch, mock_connection_factory): ) monkeypatch.setattr( - "dsmr_parser.clients.protocol.create_dsmr_reader", first_fail_connection_factory + "homeassistant.components.dsmr.sensor.create_dsmr_reader", + first_fail_connection_factory, ) yield from async_setup_component(hass, "sensor", {"sensor": config}) diff --git a/tests/components/fan/test_reproduce_state.py b/tests/components/fan/test_reproduce_state.py new file mode 100644 index 00000000000..0dcd38580b8 --- /dev/null +++ b/tests/components/fan/test_reproduce_state.py @@ -0,0 +1,89 @@ +"""Test reproduce state for Fan.""" +from homeassistant.core import State + +from tests.common import async_mock_service + + +async def test_reproducing_states(hass, caplog): + """Test reproducing Fan states.""" + hass.states.async_set("fan.entity_off", "off", {}) + hass.states.async_set("fan.entity_on", "on", {}) + hass.states.async_set("fan.entity_speed", "on", {"speed": "high"}) + hass.states.async_set("fan.entity_oscillating", "on", {"oscillating": True}) + hass.states.async_set("fan.entity_direction", "on", {"direction": "forward"}) + + turn_on_calls = async_mock_service(hass, "fan", "turn_on") + turn_off_calls = async_mock_service(hass, "fan", "turn_off") + set_direction_calls = async_mock_service(hass, "fan", "set_direction") + oscillate_calls = async_mock_service(hass, "fan", "oscillate") + set_speed_calls = async_mock_service(hass, "fan", "set_speed") + + # These calls should do nothing as entities already in desired state + await hass.helpers.state.async_reproduce_state( + [ + State("fan.entity_off", "off"), + State("fan.entity_on", "on"), + State("fan.entity_speed", "on", {"speed": "high"}), + State("fan.entity_oscillating", "on", {"oscillating": True}), + State("fan.entity_direction", "on", {"direction": "forward"}), + ], + blocking=True, + ) + + assert len(turn_on_calls) == 0 + assert len(turn_off_calls) == 0 + assert len(set_direction_calls) == 0 + assert len(oscillate_calls) == 0 + assert len(set_speed_calls) == 0 + + # Test invalid state is handled + await hass.helpers.state.async_reproduce_state( + [State("fan.entity_off", "not_supported")], blocking=True + ) + + assert "not_supported" in caplog.text + assert len(turn_on_calls) == 0 + assert len(turn_off_calls) == 0 + assert len(set_direction_calls) == 0 + assert len(oscillate_calls) == 0 + assert len(set_speed_calls) == 0 + + # Make sure correct services are called + await hass.helpers.state.async_reproduce_state( + [ + State("fan.entity_on", "off"), + State("fan.entity_off", "on"), + State("fan.entity_speed", "on", {"speed": "low"}), + State("fan.entity_oscillating", "on", {"oscillating": False}), + State("fan.entity_direction", "on", {"direction": "reverse"}), + # Should not raise + State("fan.non_existing", "on"), + ], + blocking=True, + ) + + assert len(turn_on_calls) == 1 + assert turn_on_calls[0].domain == "fan" + assert turn_on_calls[0].data == {"entity_id": "fan.entity_off"} + + assert len(set_direction_calls) == 1 + assert set_direction_calls[0].domain == "fan" + assert set_direction_calls[0].data == { + "entity_id": "fan.entity_direction", + "direction": "reverse", + } + + assert len(oscillate_calls) == 1 + assert oscillate_calls[0].domain == "fan" + assert oscillate_calls[0].data == { + "entity_id": "fan.entity_oscillating", + "oscillating": False, + } + + assert len(set_speed_calls) == 1 + assert set_speed_calls[0].domain == "fan" + assert set_speed_calls[0].data == {"entity_id": "fan.entity_speed", "speed": "low"} + + assert len(turn_off_calls) == 1 + assert turn_off_calls[0].domain == "fan" + assert turn_off_calls[0].data == {"entity_id": "fan.entity_on"} diff --git a/tests/components/flux/test_switch.py b/tests/components/flux/test_switch.py index fb35485f5c9..91871666f46 100644 --- a/tests/components/flux/test_switch.py +++ b/tests/components/flux/test_switch.py @@ -10,12 +10,14 @@ from homeassistant.const import ( SERVICE_TURN_ON, SUN_EVENT_SUNRISE, ) +from homeassistant.core import State import homeassistant.util.dt as dt_util from tests.common import ( assert_setup_component, async_fire_time_changed, async_mock_service, + mock_restore_cache, ) from tests.components.light import common as common_light from tests.components.switch import common @@ -35,6 +37,52 @@ async def test_valid_config(hass): }, ) + state = hass.states.get("switch.flux") + assert state + assert state.state == "off" + + +async def test_restore_state_last_on(hass): + """Test restoring state when the last state is on.""" + mock_restore_cache(hass, [State("switch.flux", "on")]) + + assert await async_setup_component( + hass, + "switch", + { + "switch": { + "platform": "flux", + "name": "flux", + "lights": ["light.desk", "light.lamp"], + } + }, + ) + + state = hass.states.get("switch.flux") + assert state + assert state.state == "on" + + +async def test_restore_state_last_off(hass): + """Test restoring state when the last state is off.""" + mock_restore_cache(hass, [State("switch.flux", "off")]) + + assert await async_setup_component( + hass, + "switch", + { + "switch": { + "platform": "flux", + "name": "flux", + "lights": ["light.desk", "light.lamp"], + } + }, + ) + + state = hass.states.get("switch.flux") + assert state + assert state.state == "off" + async def test_valid_config_with_info(hass): """Test configuration.""" diff --git a/tests/components/geo_rss_events/test_sensor.py b/tests/components/geo_rss_events/test_sensor.py index 3a4c5333ba8..492290b9519 100644 --- a/tests/components/geo_rss_events/test_sensor.py +++ b/tests/components/geo_rss_events/test_sensor.py @@ -59,7 +59,7 @@ class TestGeoRssServiceUpdater(unittest.TestCase): feed_entry.category = category return feed_entry - @mock.patch("georss_client.generic_feed.GenericFeed") + @mock.patch("homeassistant.components.geo_rss_events.sensor.GenericFeed") def test_setup(self, mock_feed): """Test the general setup of the platform.""" # Set up some mock feed entries for this test. @@ -122,7 +122,7 @@ class TestGeoRssServiceUpdater(unittest.TestCase): ATTR_ICON: "mdi:alert", } - @mock.patch("georss_client.generic_feed.GenericFeed") + @mock.patch("homeassistant.components.geo_rss_events.sensor.GenericFeed") def test_setup_with_categories(self, mock_feed): """Test the general setup of the platform.""" # Set up some mock feed entries for this test. diff --git a/tests/components/glances/__init__.py b/tests/components/glances/__init__.py new file mode 100644 index 00000000000..488265f970b --- /dev/null +++ b/tests/components/glances/__init__.py @@ -0,0 +1 @@ +"""Tests for Glances.""" diff --git a/tests/components/glances/test_config_flow.py b/tests/components/glances/test_config_flow.py new file mode 100644 index 00000000000..e5be52e6b33 --- /dev/null +++ b/tests/components/glances/test_config_flow.py @@ -0,0 +1,102 @@ +"""Tests for Glances config flow.""" +from unittest.mock import patch + +from glances_api import Glances + +from homeassistant.components.glances import config_flow +from homeassistant.components.glances.const import DOMAIN +from homeassistant.const import CONF_SCAN_INTERVAL + +from tests.common import MockConfigEntry, mock_coro + +NAME = "Glances" +HOST = "0.0.0.0" +USERNAME = "username" +PASSWORD = "password" +PORT = 61208 +VERSION = 3 +SCAN_INTERVAL = 10 + +DEMO_USER_INPUT = { + "name": NAME, + "host": HOST, + "username": USERNAME, + "password": PASSWORD, + "version": VERSION, + "port": PORT, + "ssl": False, + "verify_ssl": True, +} + + +def init_config_flow(hass): + """Init a configuration flow.""" + flow = config_flow.GlancesFlowHandler() + flow.hass = hass + return flow + + +async def test_form(hass): + """Test config entry configured successfully.""" + flow = init_config_flow(hass) + + with patch("glances_api.Glances"), patch.object( + Glances, "get_data", return_value=mock_coro() + ): + + result = await flow.async_step_user(DEMO_USER_INPUT) + + assert result["type"] == "create_entry" + assert result["title"] == NAME + assert result["data"] == DEMO_USER_INPUT + + +async def test_form_cannot_connect(hass): + """Test to return error if we cannot connect.""" + flow = init_config_flow(hass) + + with patch("glances_api.Glances"): + result = await flow.async_step_user(DEMO_USER_INPUT) + + assert result["type"] == "form" + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_form_wrong_version(hass): + """Test to check if wrong version is entered.""" + flow = init_config_flow(hass) + + user_input = DEMO_USER_INPUT.copy() + user_input.update(version=1) + result = await flow.async_step_user(user_input) + + assert result["type"] == "form" + assert result["errors"] == {"version": "wrong_version"} + + +async def test_form_already_configured(hass): + """Test host is already configured.""" + entry = MockConfigEntry( + domain=DOMAIN, data=DEMO_USER_INPUT, options={CONF_SCAN_INTERVAL: 60} + ) + entry.add_to_hass(hass) + + flow = init_config_flow(hass) + result = await flow.async_step_user(DEMO_USER_INPUT) + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + +async def test_options(hass): + """Test options for Glances.""" + entry = MockConfigEntry( + domain=DOMAIN, data=DEMO_USER_INPUT, options={CONF_SCAN_INTERVAL: 60} + ) + entry.add_to_hass(hass) + flow = init_config_flow(hass) + options_flow = flow.async_get_options_flow(entry) + + result = await options_flow.async_step_init({CONF_SCAN_INTERVAL: 10}) + assert result["type"] == "create_entry" + assert result["data"][CONF_SCAN_INTERVAL] == 10 diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py index 8049ac4b0db..09522e9c86f 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -12,12 +12,23 @@ class MockConfig(helpers.AbstractConfig): should_expose=None, entity_config=None, hass=None, + local_sdk_webhook_id=None, + local_sdk_user_id=None, + enabled=True, ): """Initialize config.""" super().__init__(hass) self._should_expose = should_expose self._secure_devices_pin = secure_devices_pin self._entity_config = entity_config or {} + self._local_sdk_webhook_id = local_sdk_webhook_id + self._local_sdk_user_id = local_sdk_user_id + self._enabled = enabled + + @property + def enabled(self): + """Return if Google is enabled.""" + return self._enabled @property def secure_devices_pin(self): @@ -29,6 +40,16 @@ class MockConfig(helpers.AbstractConfig): """Return secure devices pin.""" return self._entity_config + @property + def local_sdk_webhook_id(self): + """Return local SDK webhook id.""" + return self._local_sdk_webhook_id + + @property + def local_sdk_user_id(self): + """Return local SDK webhook id.""" + return self._local_sdk_user_id + def should_expose(self, state): """Expose it all.""" return self._should_expose is None or self._should_expose(state) diff --git a/tests/components/google_assistant/test_google_assistant.py b/tests/components/google_assistant/test_google_assistant.py index 6473e8964b8..b43e913ab27 100644 --- a/tests/components/google_assistant/test_google_assistant.py +++ b/tests/components/google_assistant/test_google_assistant.py @@ -3,7 +3,7 @@ import asyncio import json -from aiohttp.hdrs import CONTENT_TYPE, AUTHORIZATION +from aiohttp.hdrs import AUTHORIZATION import pytest from homeassistant import core, const, setup @@ -24,11 +24,6 @@ from . import DEMO_DEVICES API_PASSWORD = "test1234" -HA_HEADERS = { - const.HTTP_HEADER_HA_AUTH: API_PASSWORD, - CONTENT_TYPE: const.CONTENT_TYPE_JSON, -} - PROJECT_ID = "hasstest-1234" CLIENT_ID = "helloworld" ACCESS_TOKEN = "superdoublesecret" diff --git a/tests/components/google_assistant/test_helpers.py b/tests/components/google_assistant/test_helpers.py new file mode 100644 index 00000000000..497b7b1f0ae --- /dev/null +++ b/tests/components/google_assistant/test_helpers.py @@ -0,0 +1,130 @@ +"""Test Google Assistant helpers.""" +from unittest.mock import Mock +from homeassistant.setup import async_setup_component +from homeassistant.components.google_assistant import helpers +from homeassistant.components.google_assistant.const import EVENT_COMMAND_RECEIVED +from . import MockConfig + +from tests.common import async_capture_events, async_mock_service + + +async def test_google_entity_sync_serialize_with_local_sdk(hass): + """Test sync serialize attributes of a GoogleEntity.""" + hass.states.async_set("light.ceiling_lights", "off") + hass.config.api = Mock(port=1234, use_ssl=True) + config = MockConfig( + hass=hass, + local_sdk_webhook_id="mock-webhook-id", + local_sdk_user_id="mock-user-id", + ) + entity = helpers.GoogleEntity(hass, config, hass.states.get("light.ceiling_lights")) + + serialized = await entity.sync_serialize() + assert "otherDeviceIds" not in serialized + assert "customData" not in serialized + + config.async_enable_local_sdk() + + serialized = await entity.sync_serialize() + assert serialized["otherDeviceIds"] == [{"deviceId": "light.ceiling_lights"}] + assert serialized["customData"] == { + "httpPort": 1234, + "httpSSL": True, + "proxyDeviceId": None, + "webhookId": "mock-webhook-id", + } + + +async def test_config_local_sdk(hass, hass_client): + """Test the local SDK.""" + command_events = async_capture_events(hass, EVENT_COMMAND_RECEIVED) + turn_on_calls = async_mock_service(hass, "light", "turn_on") + hass.states.async_set("light.ceiling_lights", "off") + + assert await async_setup_component(hass, "webhook", {}) + + config = MockConfig( + hass=hass, + local_sdk_webhook_id="mock-webhook-id", + local_sdk_user_id="mock-user-id", + ) + + client = await hass_client() + + config.async_enable_local_sdk() + + resp = await client.post( + "/api/webhook/mock-webhook-id", + json={ + "inputs": [ + { + "context": {"locale_country": "US", "locale_language": "en"}, + "intent": "action.devices.EXECUTE", + "payload": { + "commands": [ + { + "devices": [{"id": "light.ceiling_lights"}], + "execution": [ + { + "command": "action.devices.commands.OnOff", + "params": {"on": True}, + } + ], + } + ], + "structureData": {}, + }, + } + ], + "requestId": "mock-req-id", + }, + ) + assert resp.status == 200 + result = await resp.json() + assert result["requestId"] == "mock-req-id" + + assert len(command_events) == 1 + assert command_events[0].context.user_id == config.local_sdk_user_id + + assert len(turn_on_calls) == 1 + assert turn_on_calls[0].context is command_events[0].context + + config.async_disable_local_sdk() + + # Webhook is no longer active + resp = await client.post("/api/webhook/mock-webhook-id") + assert resp.status == 200 + assert await resp.read() == b"" + + +async def test_config_local_sdk_if_disabled(hass, hass_client): + """Test the local SDK.""" + assert await async_setup_component(hass, "webhook", {}) + + config = MockConfig( + hass=hass, + local_sdk_webhook_id="mock-webhook-id", + local_sdk_user_id="mock-user-id", + enabled=False, + ) + + client = await hass_client() + + config.async_enable_local_sdk() + + resp = await client.post( + "/api/webhook/mock-webhook-id", json={"requestId": "mock-req-id"} + ) + assert resp.status == 200 + result = await resp.json() + assert result == { + "payload": {"errorCode": "deviceTurnedOff"}, + "requestId": "mock-req-id", + } + + config.async_disable_local_sdk() + + # Webhook is no longer active + resp = await client.post("/api/webhook/mock-webhook-id") + assert resp.status == 200 + assert await resp.read() == b"" diff --git a/tests/components/google_assistant/test_http.py b/tests/components/google_assistant/test_http.py new file mode 100644 index 00000000000..4b26bbeba7f --- /dev/null +++ b/tests/components/google_assistant/test_http.py @@ -0,0 +1,157 @@ +"""Test Google http services.""" +from datetime import datetime, timezone, timedelta +from asynctest import patch, ANY + +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( + { + "project_id": "1234", + "service_account": { + "private_key": "-----BEGIN PRIVATE KEY-----\nMIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAKYscIlwm7soDsHAz6L6YvUkCvkrX19rS6yeYOmovvhoK5WeYGWUsd8V72zmsyHB7XO94YgJVjvxfzn5K8bLePjFzwoSJjZvhBJ/ZQ05d8VmbvgyWUoPdG9oEa4fZ/lCYrXoaFdTot2xcJvrb/ZuiRl4s4eZpNeFYvVK/Am7UeFPAgMBAAECgYAUetOfzLYUudofvPCaKHu7tKZ5kQPfEa0w6BAPnBF1Mfl1JiDBRDMryFtKs6AOIAVwx00dY/Ex0BCbB3+Cr58H7t4NaPTJxCpmR09pK7o17B7xAdQv8+SynFNud9/5vQ5AEXMOLNwKiU7wpXT6Z7ZIibUBOR7ewsWgsHCDpN1iqQJBAOMODPTPSiQMwRAUHIc6GPleFSJnIz2PAoG3JOG9KFAL6RtIc19lob2ZXdbQdzKtjSkWo+O5W20WDNAl1k32h6MCQQC7W4ZCIY67mPbL6CxXfHjpSGF4Dr9VWJ7ZrKHr6XUoOIcEvsn/pHvWonjMdy93rQMSfOE8BKd/I1+GHRmNVgplAkAnSo4paxmsZVyfeKt7Jy2dMY+8tVZe17maUuQaAE7Sk00SgJYegwrbMYgQnWCTL39HBfj0dmYA2Zj8CCAuu6O7AkEAryFiYjaUAO9+4iNoL27+ZrFtypeeadyov7gKs0ZKaQpNyzW8A+Zwi7TbTeSqzic/E+z/bOa82q7p/6b7141xsQJBANCAcIwMcVb6KVCHlQbOtKspo5Eh4ZQi8bGl+IcwbQ6JSxeTx915IfAldgbuU047wOB04dYCFB2yLDiUGVXTifU=\n-----END PRIVATE KEY-----\n", + "client_email": "dummy@dummy.iam.gserviceaccount.com", + }, + } +) +MOCK_TOKEN = {"access_token": "dummtoken", "expires_in": 3600} +MOCK_JSON = {"devices": {}} +MOCK_URL = "https://dummy" +MOCK_HEADER = { + "Authorization": "Bearer {}".format(MOCK_TOKEN["access_token"]), + "X-GFE-SSL": "yes", +} + + +async def test_get_jwt(hass): + """Test signing of key.""" + + jwt = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJkdW1teUBkdW1teS5pYW0uZ3NlcnZpY2VhY2NvdW50LmNvbSIsInNjb3BlIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vYXV0aC9ob21lZ3JhcGgiLCJhdWQiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20vby9vYXV0aDIvdG9rZW4iLCJpYXQiOjE1NzEwMTEyMDAsImV4cCI6MTU3MTAxNDgwMH0.gG06SmY-zSvFwSrdFfqIdC6AnC22rwz-d2F2UDeWbywjdmFL_1zceL-OOLBwjD8MJr6nR0kmN_Osu7ml9-EzzZjJqsRUxMjGn2G8nSYHbv16R4FYIp62Ibvt6Jj_wdFobEPoy_5OJ28P5Hdu0giGMlFBJMy0Tc6MgEDZA-cwOBw" + res = _get_homegraph_jwt( + datetime(2019, 10, 14, tzinfo=timezone.utc), + DUMMY_CONFIG["service_account"]["client_email"], + DUMMY_CONFIG["service_account"]["private_key"], + ) + assert res == jwt + + +async def test_get_access_token(hass, aioclient_mock): + """Test the function to get access token.""" + jwt = "dummyjwt" + + aioclient_mock.post( + HOMEGRAPH_TOKEN_URL, + status=200, + json={"access_token": "1234", "expires_in": 3600}, + ) + + await _get_homegraph_token(hass, jwt) + assert aioclient_mock.call_count == 1 + assert aioclient_mock.mock_calls[0][3] == { + "Authorization": "Bearer {}".format(jwt), + "Content-Type": "application/x-www-form-urlencoded", + } + + +async def test_update_access_token(hass): + """Test the function to update access token when expired.""" + jwt = "dummyjwt" + + config = GoogleConfig(hass, DUMMY_CONFIG) + + base_time = datetime(2019, 10, 14, tzinfo=timezone.utc) + with patch( + "homeassistant.components.google_assistant.http._get_homegraph_token" + ) as mock_get_token, patch( + "homeassistant.components.google_assistant.http._get_homegraph_jwt" + ) as mock_get_jwt, patch( + "homeassistant.core.dt_util.utcnow" + ) as mock_utcnow: + mock_utcnow.return_value = base_time + mock_get_jwt.return_value = jwt + mock_get_token.return_value = MOCK_TOKEN + + await config._async_update_token() + mock_get_token.assert_called_once() + + mock_get_token.reset_mock() + + mock_utcnow.return_value = base_time + timedelta(seconds=3600) + await config._async_update_token() + mock_get_token.assert_not_called() + + mock_get_token.reset_mock() + + mock_utcnow.return_value = base_time + timedelta(seconds=3601) + await config._async_update_token() + mock_get_token.assert_called_once() + + +async def test_call_homegraph_api(hass, aioclient_mock, hass_storage): + """Test the function to call the homegraph api.""" + config = GoogleConfig(hass, DUMMY_CONFIG) + with patch( + "homeassistant.components.google_assistant.http._get_homegraph_token" + ) as mock_get_token: + mock_get_token.return_value = MOCK_TOKEN + + aioclient_mock.post(MOCK_URL, status=200, json={}) + + await config.async_call_homegraph_api(MOCK_URL, MOCK_JSON) + + assert mock_get_token.call_count == 1 + assert aioclient_mock.call_count == 1 + + call = aioclient_mock.mock_calls[0] + assert call[2] == MOCK_JSON + assert call[3] == MOCK_HEADER + + +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) + with patch( + "homeassistant.components.google_assistant.http._get_homegraph_token" + ) as mock_get_token: + mock_get_token.return_value = MOCK_TOKEN + + aioclient_mock.post(MOCK_URL, status=401, json={}) + + await config.async_call_homegraph_api(MOCK_URL, MOCK_JSON) + + assert mock_get_token.call_count == 2 + assert aioclient_mock.call_count == 2 + + call = aioclient_mock.mock_calls[0] + assert call[2] == MOCK_JSON + assert call[3] == MOCK_HEADER + call = aioclient_mock.mock_calls[1] + assert call[2] == MOCK_JSON + assert call[3] == MOCK_HEADER + + +async def test_report_state(hass, aioclient_mock, hass_storage): + """Test the report state function.""" + config = GoogleConfig(hass, DUMMY_CONFIG) + 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) + mock_call.assert_called_once_with( + REPORT_STATE_BASE_URL, + {"requestId": ANY, "agentUserId": owner.id, "payload": message}, + ) diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 6ecd4af446b..2f7fdb8e131 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -3,7 +3,7 @@ from unittest.mock import patch, Mock import pytest from homeassistant.core import State, EVENT_CALL_SERVICE -from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS +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 ( @@ -734,3 +734,137 @@ async def test_trait_execute_adding_query_data(hass): ] }, } + + +async def test_identify(hass): + """Test identify message.""" + result = await sh.async_handle_message( + hass, + BASIC_CONFIG, + None, + { + "requestId": REQ_ID, + "inputs": [ + { + "intent": "action.devices.IDENTIFY", + "payload": { + "device": { + "mdnsScanData": { + "additionals": [ + { + "type": "TXT", + "class": "IN", + "name": "devhome._home-assistant._tcp.local", + "ttl": 4500, + "data": [ + "version=0.101.0.dev0", + "base_url=http://192.168.1.101:8123", + "requires_api_password=true", + ], + } + ] + } + }, + "structureData": {}, + }, + } + ], + "devices": [ + { + "id": "light.ceiling_lights", + "customData": { + "httpPort": 8123, + "httpSSL": False, + "proxyDeviceId": BASIC_CONFIG.agent_user_id, + "webhookId": "dde3b9800a905e886cc4d38e226a6e7e3f2a6993d2b9b9f63d13e42ee7de3219", + }, + } + ], + }, + ) + + assert result == { + "requestId": REQ_ID, + "payload": { + "device": { + "id": BASIC_CONFIG.agent_user_id, + "isLocalOnly": True, + "isProxy": True, + "deviceInfo": { + "hwVersion": "UNKNOWN_HW_VERSION", + "manufacturer": "Home Assistant", + "model": "Home Assistant", + "swVersion": __version__, + }, + } + }, + } + + +async def test_reachable_devices(hass): + """Test REACHABLE_DEVICES intent.""" + # Matching passed in device. + hass.states.async_set("light.ceiling_lights", "on") + + # Unsupported entity + hass.states.async_set("not_supported.entity", "something") + + # Excluded via config + hass.states.async_set("light.not_expose", "on") + + # Not passed in as google_id + hass.states.async_set("light.not_mentioned", "on") + + config = MockConfig( + should_expose=lambda state: state.entity_id != "light.not_expose" + ) + + result = await sh.async_handle_message( + hass, + config, + None, + { + "requestId": REQ_ID, + "inputs": [ + { + "intent": "action.devices.REACHABLE_DEVICES", + "payload": { + "device": { + "proxyDevice": { + "id": "6a04f0f7-6125-4356-a846-861df7e01497", + "customData": "{}", + "proxyData": "{}", + } + }, + "structureData": {}, + }, + } + ], + "devices": [ + { + "id": "light.ceiling_lights", + "customData": { + "httpPort": 8123, + "httpSSL": False, + "proxyDeviceId": BASIC_CONFIG.agent_user_id, + "webhookId": "dde3b9800a905e886cc4d38e226a6e7e3f2a6993d2b9b9f63d13e42ee7de3219", + }, + }, + { + "id": "light.not_expose", + "customData": { + "httpPort": 8123, + "httpSSL": False, + "proxyDeviceId": BASIC_CONFIG.agent_user_id, + "webhookId": "dde3b9800a905e886cc4d38e226a6e7e3f2a6993d2b9b9f63d13e42ee7de3219", + }, + }, + {"id": BASIC_CONFIG.agent_user_id, "customData": {}}, + ], + }, + ) + + assert result == { + "requestId": REQ_ID, + "payload": {"devices": [{"verificationId": "light.ceiling_lights"}]}, + } diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index a5c527dacfe..d6ec24a7867 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -48,11 +48,11 @@ _LOGGER = logging.getLogger(__name__) REQ_ID = "ff36a3cc-ec34-11e6-b1a0-64510650abcf" -BASIC_DATA = helpers.RequestData(BASIC_CONFIG, "test-agent", REQ_ID) +BASIC_DATA = helpers.RequestData(BASIC_CONFIG, "test-agent", REQ_ID, None) PIN_CONFIG = MockConfig(secure_devices_pin="1234") -PIN_DATA = helpers.RequestData(PIN_CONFIG, "test-agent", REQ_ID) +PIN_DATA = helpers.RequestData(PIN_CONFIG, "test-agent", REQ_ID, None) async def test_brightness_light(hass): diff --git a/tests/components/hassio/__init__.py b/tests/components/hassio/__init__.py index 8e2b6db777d..767ec59f366 100644 --- a/tests/components/hassio/__init__.py +++ b/tests/components/hassio/__init__.py @@ -1,4 +1,3 @@ """Tests for Hassio component.""" -API_PASSWORD = "pass1234" HASSIO_TOKEN = "123456" diff --git a/tests/components/hassio/conftest.py b/tests/components/hassio/conftest.py index d7d50b97eb8..0e246cf1b46 100644 --- a/tests/components/hassio/conftest.py +++ b/tests/components/hassio/conftest.py @@ -9,7 +9,7 @@ from homeassistant.setup import async_setup_component from homeassistant.components.hassio.handler import HassIO, HassioAPIError from tests.common import mock_coro -from . import API_PASSWORD, HASSIO_TOKEN +from . import HASSIO_TOKEN @pytest.fixture @@ -39,23 +39,19 @@ def hassio_stubs(hassio_env, hass, hass_client, aioclient_mock): side_effect=HassioAPIError(), ): hass.state = CoreState.starting - hass.loop.run_until_complete( - async_setup_component( - hass, "hassio", {"http": {"api_password": API_PASSWORD}} - ) - ) + hass.loop.run_until_complete(async_setup_component(hass, "hassio", {})) @pytest.fixture def hassio_client(hassio_stubs, hass, hass_client): """Return a Hass.io HTTP client.""" - yield hass.loop.run_until_complete(hass_client()) + return hass.loop.run_until_complete(hass_client()) @pytest.fixture def hassio_noauth_client(hassio_stubs, hass, aiohttp_client): """Return a Hass.io HTTP client without auth.""" - yield hass.loop.run_until_complete(aiohttp_client(hass.http.app)) + return hass.loop.run_until_complete(aiohttp_client(hass.http.app)) @pytest.fixture diff --git a/tests/components/hassio/test_addon_panel.py b/tests/components/hassio/test_addon_panel.py index 114935df3fc..480df508968 100644 --- a/tests/components/hassio/test_addon_panel.py +++ b/tests/components/hassio/test_addon_panel.py @@ -4,10 +4,8 @@ from unittest.mock import patch, Mock import pytest from homeassistant.setup import async_setup_component -from homeassistant.const import HTTP_HEADER_HA_AUTH from tests.common import mock_coro -from . import API_PASSWORD @pytest.fixture(autouse=True) @@ -53,9 +51,7 @@ async def test_hassio_addon_panel_startup(hass, aioclient_mock, hassio_env): "homeassistant.components.hassio.addon_panel._register_panel", Mock(return_value=mock_coro()), ) as mock_panel: - await async_setup_component( - hass, "hassio", {"http": {"api_password": API_PASSWORD}} - ) + await async_setup_component(hass, "hassio", {}) await hass.async_block_till_done() assert aioclient_mock.call_count == 3 @@ -98,9 +94,7 @@ async def test_hassio_addon_panel_api(hass, aioclient_mock, hassio_env, hass_cli "homeassistant.components.hassio.addon_panel._register_panel", Mock(return_value=mock_coro()), ) as mock_panel: - await async_setup_component( - hass, "hassio", {"http": {"api_password": API_PASSWORD}} - ) + await async_setup_component(hass, "hassio", {}) await hass.async_block_till_done() assert aioclient_mock.call_count == 3 @@ -113,14 +107,10 @@ async def test_hassio_addon_panel_api(hass, aioclient_mock, hassio_env, hass_cli hass_client = await hass_client() - resp = await hass_client.post( - "/api/hassio_push/panel/test2", headers={HTTP_HEADER_HA_AUTH: API_PASSWORD} - ) + resp = await hass_client.post("/api/hassio_push/panel/test2") assert resp.status == 400 - resp = await hass_client.post( - "/api/hassio_push/panel/test1", headers={HTTP_HEADER_HA_AUTH: API_PASSWORD} - ) + resp = await hass_client.post("/api/hassio_push/panel/test1") assert resp.status == 200 assert mock_panel.call_count == 2 diff --git a/tests/components/hassio/test_auth.py b/tests/components/hassio/test_auth.py index a2839b297b8..1fb6d32ccf7 100644 --- a/tests/components/hassio/test_auth.py +++ b/tests/components/hassio/test_auth.py @@ -1,11 +1,9 @@ """The tests for the hassio component.""" from unittest.mock import patch, Mock -from homeassistant.const import HTTP_HEADER_HA_AUTH from homeassistant.exceptions import HomeAssistantError from tests.common import mock_coro -from . import API_PASSWORD async def test_login_success(hass, hassio_client): @@ -18,7 +16,6 @@ async def test_login_success(hass, hassio_client): resp = await hassio_client.post( "/api/hassio_auth", json={"username": "test", "password": "123456", "addon": "samba"}, - headers={HTTP_HEADER_HA_AUTH: API_PASSWORD}, ) # Check we got right response @@ -36,7 +33,6 @@ async def test_login_error(hass, hassio_client): resp = await hassio_client.post( "/api/hassio_auth", json={"username": "test", "password": "123456", "addon": "samba"}, - headers={HTTP_HEADER_HA_AUTH: API_PASSWORD}, ) # Check we got right response @@ -51,9 +47,7 @@ async def test_login_no_data(hass, hassio_client): "HassAuthProvider.async_validate_login", Mock(side_effect=HomeAssistantError()), ) as mock_login: - resp = await hassio_client.post( - "/api/hassio_auth", headers={HTTP_HEADER_HA_AUTH: API_PASSWORD} - ) + resp = await hassio_client.post("/api/hassio_auth") # Check we got right response assert resp.status == 400 @@ -68,9 +62,7 @@ async def test_login_no_username(hass, hassio_client): Mock(side_effect=HomeAssistantError()), ) as mock_login: resp = await hassio_client.post( - "/api/hassio_auth", - json={"password": "123456", "addon": "samba"}, - headers={HTTP_HEADER_HA_AUTH: API_PASSWORD}, + "/api/hassio_auth", json={"password": "123456", "addon": "samba"} ) # Check we got right response @@ -93,7 +85,6 @@ async def test_login_success_extra(hass, hassio_client): "addon": "samba", "path": "/share", }, - headers={HTTP_HEADER_HA_AUTH: API_PASSWORD}, ) # Check we got right response diff --git a/tests/components/hassio/test_discovery.py b/tests/components/hassio/test_discovery.py index 89f1483ffab..a1b4ae2e900 100644 --- a/tests/components/hassio/test_discovery.py +++ b/tests/components/hassio/test_discovery.py @@ -3,10 +3,9 @@ from unittest.mock import patch, Mock from homeassistant.setup import async_setup_component from homeassistant.components.hassio.handler import HassioAPIError -from homeassistant.const import EVENT_HOMEASSISTANT_START, HTTP_HEADER_HA_AUTH +from homeassistant.const import EVENT_HOMEASSISTANT_START from tests.common import mock_coro -from . import API_PASSWORD async def test_hassio_discovery_startup(hass, aioclient_mock, hassio_client): @@ -101,9 +100,7 @@ async def test_hassio_discovery_startup_done(hass, aioclient_mock, hassio_client Mock(return_value=mock_coro({"type": "abort"})), ) as mock_mqtt: await hass.async_start() - await async_setup_component( - hass, "hassio", {"http": {"api_password": API_PASSWORD}} - ) + await async_setup_component(hass, "hassio", {}) await hass.async_block_till_done() assert aioclient_mock.call_count == 2 @@ -151,7 +148,6 @@ async def test_hassio_discovery_webhook(hass, aioclient_mock, hassio_client): ) as mock_mqtt: resp = await hassio_client.post( "/api/hassio_push/discovery/testuuid", - headers={HTTP_HEADER_HA_AUTH: API_PASSWORD}, json={"addon": "mosquitto", "service": "mqtt", "uuid": "testuuid"}, ) await hass.async_block_till_done() diff --git a/tests/components/hassio/test_http.py b/tests/components/hassio/test_http.py index 8f77d6b6234..96d53f93c3a 100644 --- a/tests/components/hassio/test_http.py +++ b/tests/components/hassio/test_http.py @@ -4,19 +4,13 @@ from unittest.mock import patch import pytest -from homeassistant.const import HTTP_HEADER_HA_AUTH - -from . import API_PASSWORD - @asyncio.coroutine 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", headers={HTTP_HEADER_HA_AUTH: API_PASSWORD} - ) + resp = yield from hassio_client.post("/api/hassio/beer") # Check we got right response assert resp.status == 200 @@ -87,9 +81,7 @@ 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", headers={HTTP_HEADER_HA_AUTH: API_PASSWORD} - ) + resp = yield from hassio_client.get("/api/hassio/beer/logs") # Check we got right response assert resp.status == 200 @@ -107,9 +99,7 @@ def test_bad_gateway_when_cannot_find_supervisor(hassio_client): "homeassistant.components.hassio.http.async_timeout.timeout", side_effect=asyncio.TimeoutError, ): - resp = yield from hassio_client.get( - "/api/hassio/addons/test/info", headers={HTTP_HEADER_HA_AUTH: API_PASSWORD} - ) + resp = yield from hassio_client.get("/api/hassio/addons/test/info") assert resp.status == 502 diff --git a/tests/components/homeassistant/test_scene.py b/tests/components/homeassistant/test_scene.py index 02c018a0b49..c7c3f2bc5d5 100644 --- a/tests/components/homeassistant/test_scene.py +++ b/tests/components/homeassistant/test_scene.py @@ -28,3 +28,26 @@ async def test_reload_config_service(hass): assert hass.states.get("scene.hallo") is None assert hass.states.get("scene.bye") is not None + + +async def test_apply_service(hass): + """Test the apply service.""" + assert await async_setup_component(hass, "scene", {}) + assert await async_setup_component(hass, "light", {"light": {"platform": "demo"}}) + + assert await hass.services.async_call( + "scene", "apply", {"entities": {"light.bed_light": "off"}}, blocking=True + ) + + assert hass.states.get("light.bed_light").state == "off" + + assert await hass.services.async_call( + "scene", + "apply", + {"entities": {"light.bed_light": {"state": "on", "brightness": 50}}}, + blocking=True, + ) + + state = hass.states.get("light.bed_light") + assert state.state == "on" + assert state.attributes["brightness"] == 50 diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 61893af7008..97838eaa852 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -69,7 +69,7 @@ async def test_setup_min(hass): assert await setup.async_setup_component(hass, DOMAIN, {DOMAIN: {}}) mock_homekit.assert_any_call( - hass, BRIDGE_NAME, DEFAULT_PORT, None, ANY, {}, DEFAULT_SAFE_MODE + hass, BRIDGE_NAME, DEFAULT_PORT, None, ANY, {}, DEFAULT_SAFE_MODE, None ) assert mock_homekit().setup.called is True @@ -98,7 +98,7 @@ async def test_setup_auto_start_disabled(hass): assert await setup.async_setup_component(hass, DOMAIN, config) mock_homekit.assert_any_call( - hass, "Test Name", 11111, "172.0.0.0", ANY, {}, DEFAULT_SAFE_MODE + hass, "Test Name", 11111, "172.0.0.0", ANY, {}, DEFAULT_SAFE_MODE, None ) assert mock_homekit().setup.called is True @@ -136,7 +136,11 @@ async def test_homekit_setup(hass, hk_driver): path = hass.config.path(HOMEKIT_FILE) assert isinstance(homekit.bridge, HomeBridge) mock_driver.assert_called_with( - hass, address=IP_ADDRESS, port=DEFAULT_PORT, persist_file=path + hass, + address=IP_ADDRESS, + port=DEFAULT_PORT, + persist_file=path, + advertised_address=None, ) assert homekit.driver.safe_mode is False @@ -153,7 +157,30 @@ async def test_homekit_setup_ip_address(hass, hk_driver): ) as mock_driver: await hass.async_add_job(homekit.setup) mock_driver.assert_called_with( - hass, address="172.0.0.0", port=DEFAULT_PORT, persist_file=ANY + hass, + address="172.0.0.0", + port=DEFAULT_PORT, + persist_file=ANY, + advertised_address=None, + ) + + +async def test_homekit_setup_advertise_ip(hass, hk_driver): + """Test setup with given IP address to advertise.""" + homekit = HomeKit( + hass, BRIDGE_NAME, DEFAULT_PORT, "0.0.0.0", {}, {}, None, "192.168.1.100" + ) + + with patch( + PATH_HOMEKIT + ".accessories.HomeDriver", return_value=hk_driver + ) as mock_driver: + await hass.async_add_job(homekit.setup) + mock_driver.assert_called_with( + hass, + address="0.0.0.0", + port=DEFAULT_PORT, + persist_file=ANY, + advertised_address="192.168.1.100", ) diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index d967d325561..8ad46e489d6 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -96,10 +96,10 @@ async def test_thermostat(hass, hk_driver, cls, events): }, ) await hass.async_block_till_done() - assert acc.char_target_temp.value == 22.0 + assert acc.char_target_temp.value == 22.2 assert acc.char_current_heat_cool.value == 1 assert acc.char_target_heat_cool.value == 1 - assert acc.char_current_temp.value == 18.0 + assert acc.char_current_temp.value == 17.8 assert acc.char_display_units.value == 0 hass.states.async_set( @@ -432,7 +432,7 @@ async def test_thermostat_fahrenheit(hass, hk_driver, cls, events): ) await hass.async_block_till_done() assert acc.get_temperature_range() == (7.0, 35.0) - assert acc.char_heating_thresh_temp.value == 20.0 + assert acc.char_heating_thresh_temp.value == 20.1 assert acc.char_cooling_thresh_temp.value == 24.0 assert acc.char_current_temp.value == 23.0 assert acc.char_target_temp.value == 22.0 diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index 923cbaca42f..8898f988f9a 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -173,7 +173,7 @@ def test_convert_to_float(): def test_temperature_to_homekit(): """Test temperature conversion from HA to HomeKit.""" assert temperature_to_homekit(20.46, TEMP_CELSIUS) == 20.5 - assert temperature_to_homekit(92.1, TEMP_FAHRENHEIT) == 33.5 + assert temperature_to_homekit(92.1, TEMP_FAHRENHEIT) == 33.4 def test_temperature_to_states(): diff --git a/tests/components/homematicip_cloud/conftest.py b/tests/components/homematicip_cloud/conftest.py new file mode 100644 index 00000000000..f60f8d659b5 --- /dev/null +++ b/tests/components/homematicip_cloud/conftest.py @@ -0,0 +1,148 @@ +"""Initializer helpers for HomematicIP fake server.""" +from asynctest import MagicMock, Mock, patch +from homematicip.aio.auth import AsyncAuth +from homematicip.aio.connection import AsyncConnection +from homematicip.aio.home import AsyncHome +import pytest + +from homeassistant import config_entries +from homeassistant.components.homematicip_cloud import ( + DOMAIN as HMIPC_DOMAIN, + async_setup as hmip_async_setup, + const as hmipc, + hap as hmip_hap, +) +from homeassistant.helpers.typing import ConfigType, HomeAssistantType + +from .helper import AUTH_TOKEN, HAPID, HAPPIN, HomeTemplate + +from tests.common import MockConfigEntry, mock_coro + + +@pytest.fixture(name="mock_connection") +def mock_connection_fixture() -> AsyncConnection: + """Return a mocked connection.""" + connection = MagicMock(spec=AsyncConnection) + + def _rest_call_side_effect(path, body=None): + return path, body + + connection._restCall.side_effect = _rest_call_side_effect # pylint: disable=W0212 + connection.api_call.return_value = mock_coro(True) + connection.init.side_effect = mock_coro(True) + + return connection + + +@pytest.fixture(name="hmip_config_entry") +def hmip_config_entry_fixture() -> config_entries.ConfigEntry: + """Create a mock config entriy for homematic ip cloud.""" + entry_data = { + hmipc.HMIPC_HAPID: HAPID, + hmipc.HMIPC_AUTHTOKEN: AUTH_TOKEN, + hmipc.HMIPC_NAME: "", + hmipc.HMIPC_PIN: HAPPIN, + } + config_entry = MockConfigEntry( + version=1, + domain=HMIPC_DOMAIN, + title=HAPID, + data=entry_data, + source="import", + connection_class=config_entries.CONN_CLASS_CLOUD_PUSH, + system_options={"disable_new_entities": False}, + ) + + return config_entry + + +@pytest.fixture(name="default_mock_home") +def default_mock_home_fixture(mock_connection) -> AsyncHome: + """Create a fake homematic async home.""" + return HomeTemplate(connection=mock_connection).init_home().get_async_home_mock() + + +@pytest.fixture(name="default_mock_hap") +async def default_mock_hap_fixture( + hass: HomeAssistantType, mock_connection, hmip_config_entry +) -> hmip_hap.HomematicipHAP: + """Create a mocked homematic access point.""" + return await get_mock_hap(hass, mock_connection, hmip_config_entry) + + +async def get_mock_hap( + hass: HomeAssistantType, + mock_connection, + hmip_config_entry: config_entries.ConfigEntry, +) -> hmip_hap.HomematicipHAP: + """Create a mocked homematic access point.""" + hass.config.components.add(HMIPC_DOMAIN) + hap = hmip_hap.HomematicipHAP(hass, hmip_config_entry) + home_name = hmip_config_entry.data["name"] + mock_home = ( + HomeTemplate(connection=mock_connection, home_name=home_name) + .init_home() + .get_async_home_mock() + ) + with patch.object(hap, "get_hap", return_value=mock_coro(mock_home)): + assert await hap.async_setup() + mock_home.on_update(hap.async_update) + mock_home.on_create(hap.async_create_entity) + + hass.data[HMIPC_DOMAIN] = {HAPID: hap} + + await hass.async_block_till_done() + + return hap + + +@pytest.fixture(name="hmip_config") +def hmip_config_fixture() -> ConfigType: + """Create a config for homematic ip cloud.""" + + entry_data = { + hmipc.HMIPC_HAPID: HAPID, + hmipc.HMIPC_AUTHTOKEN: AUTH_TOKEN, + hmipc.HMIPC_NAME: "", + hmipc.HMIPC_PIN: HAPPIN, + } + + return {HMIPC_DOMAIN: [entry_data]} + + +@pytest.fixture(name="dummy_config") +def dummy_config_fixture() -> ConfigType: + """Create a dummy config.""" + return {"blabla": None} + + +@pytest.fixture(name="mock_hap_with_service") +async def mock_hap_with_service_fixture( + hass: HomeAssistantType, default_mock_hap, dummy_config +) -> hmip_hap.HomematicipHAP: + """Create a fake homematic access point with hass services.""" + await hmip_async_setup(hass, dummy_config) + await hass.async_block_till_done() + hass.data[HMIPC_DOMAIN] = {HAPID: default_mock_hap} + return default_mock_hap + + +@pytest.fixture(name="simple_mock_home") +def simple_mock_home_fixture() -> AsyncHome: + """Return a simple AsyncHome Mock.""" + return Mock( + spec=AsyncHome, + devices=[], + groups=[], + location=Mock(), + weather=Mock(create=True), + id=42, + dutyCycle=88, + connected=True, + ) + + +@pytest.fixture(name="simple_mock_auth") +def simple_mock_auth_fixture() -> AsyncAuth: + """Return a simple AsyncAuth Mock.""" + return Mock(spec=AsyncAuth, pin=HAPPIN, create=True) diff --git a/tests/components/homematicip_cloud/helper.py b/tests/components/homematicip_cloud/helper.py new file mode 100644 index 00000000000..78c78ec0ab9 --- /dev/null +++ b/tests/components/homematicip_cloud/helper.py @@ -0,0 +1,144 @@ +"""Helper for HomematicIP Cloud Tests.""" +import json + +from asynctest import Mock +from homematicip.aio.class_maps import ( + TYPE_CLASS_MAP, + TYPE_GROUP_MAP, + TYPE_SECURITY_EVENT_MAP, +) +from homematicip.aio.device import AsyncDevice +from homematicip.aio.group import AsyncGroup +from homematicip.aio.home import AsyncHome +from homematicip.home import Home + +from homeassistant.components.homematicip_cloud.device import ( + ATTR_IS_GROUP, + ATTR_MODEL_TYPE, +) + +from tests.common import load_fixture + +HAPID = "3014F7110000000000000001" +HAPPIN = "5678" +AUTH_TOKEN = "1234" +HOME_JSON = "homematicip_cloud.json" + + +def get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model +): + """Get and test basic device.""" + ha_state = hass.states.get(entity_id) + assert ha_state is not None + if device_model: + assert ha_state.attributes[ATTR_MODEL_TYPE] == device_model + assert ha_state.name == entity_name + + hmip_device = default_mock_hap.hmip_device_by_entity_id.get(entity_id) + + if hmip_device: + if isinstance(hmip_device, AsyncDevice): + assert ha_state.attributes[ATTR_IS_GROUP] is False + elif isinstance(hmip_device, AsyncGroup): + assert ha_state.attributes[ATTR_IS_GROUP] is True + return ha_state, hmip_device + + +async def async_manipulate_test_data( + hass, hmip_device, attribute, new_value, channel=1, fire_device=None +): + """Set new value on hmip device.""" + if channel == 1: + setattr(hmip_device, attribute, new_value) + if hasattr(hmip_device, "functionalChannels"): + functional_channel = hmip_device.functionalChannels[channel] + setattr(functional_channel, attribute, new_value) + + 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 + else: + fire_target.fire_update_event() + + await hass.async_block_till_done() + + +class HomeTemplate(Home): + """ + Home template as builder for home mock. + + It is based on the upstream libs home class to generate hmip devices + and groups based on the given homematicip_cloud.json. + + All further testing activities should be done by using the AsyncHome mock, + that is generated by get_async_home_mock(self). + + The class also generated mocks of devices and groups for further testing. + """ + + _typeClassMap = TYPE_CLASS_MAP + _typeGroupMap = TYPE_GROUP_MAP + _typeSecurityEventMap = TYPE_SECURITY_EVENT_MAP + + def __init__(self, connection=None, home_name=""): + """Init template with connection.""" + super().__init__(connection=connection) + self.label = "Access Point" + self.name = home_name + self.model_type = "HmIP-HAP" + self.init_json_state = None + + def init_home(self, json_path=HOME_JSON): + """Init template with json.""" + self.init_json_state = json.loads(load_fixture(HOME_JSON), encoding="UTF-8") + self.update_home(json_state=self.init_json_state, clearConfig=True) + return self + + def update_home(self, json_state, clearConfig: bool = False): + """Update home and ensure that mocks are created.""" + result = super().update_home(json_state, clearConfig) + self._generate_mocks() + return result + + def _generate_mocks(self): + """Generate mocks for groups and devices.""" + mock_devices = [] + for device in self.devices: + mock_devices.append(_get_mock(device)) + self.devices = mock_devices + + mock_groups = [] + for group in self.groups: + mock_groups.append(_get_mock(group)) + self.groups = mock_groups + + def download_configuration(self): + """Return the initial json config.""" + return self.init_json_state + + def get_async_home_mock(self): + """ + Create Mock for Async_Home. based on template to be used for testing. + + It adds collections of mocked devices and groups to the home objects, + and sets required attributes. + """ + mock_home = Mock( + spec=AsyncHome, wraps=self, label="Access Point", modelType="HmIP-HAP" + ) + mock_home.__dict__.update(self.__dict__) + + return mock_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 + return instance + + mock = Mock(spec=instance, wraps=instance) + mock.__dict__.update(instance.__dict__) + return mock diff --git a/tests/components/homematicip_cloud/test_alarm_control_panel.py b/tests/components/homematicip_cloud/test_alarm_control_panel.py new file mode 100644 index 00000000000..2798a0879b7 --- /dev/null +++ b/tests/components/homematicip_cloud/test_alarm_control_panel.py @@ -0,0 +1,148 @@ +"""Tests for HomematicIP Cloud alarm control panel.""" +from homematicip.base.enums import WindowState +from homematicip.group import SecurityZoneGroup + +from homeassistant.components.alarm_control_panel import ( + DOMAIN as ALARM_CONTROL_PANEL_DOMAIN, +) +from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN +from homeassistant.const import ( + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_DISARMED, + STATE_ALARM_TRIGGERED, +) +from homeassistant.setup import async_setup_component + +from .helper import get_and_check_entity_basics + + +def _get_security_zones(groups): # pylint: disable=W0221 + """Get the security zones.""" + for group in groups: + if isinstance(group, SecurityZoneGroup): + if group.label == "EXTERNAL": + external = group + elif group.label == "INTERNAL": + internal = group + return internal, external + + +async def _async_manipulate_security_zones( + hass, home, internal_active, external_active, window_state +): + """Set new values on hmip security zones.""" + internal_zone, external_zone = _get_security_zones(home.groups) + external_zone.active = external_active + external_zone.windowState = window_state + internal_zone.active = internal_active + + # Just one call to a security zone is required to refresh the ACP. + internal_zone.fire_update_event() + + await hass.async_block_till_done() + + +async def test_manually_configured_platform(hass): + """Test that we do not set up an access point.""" + assert ( + await async_setup_component( + hass, + ALARM_CONTROL_PANEL_DOMAIN, + {ALARM_CONTROL_PANEL_DOMAIN: {"platform": HMIPC_DOMAIN}}, + ) + is True + ) + assert not hass.data.get(HMIPC_DOMAIN) + + +async def test_hmip_alarm_control_panel(hass, default_mock_hap): + """Test HomematicipAlarmControlPanel.""" + entity_id = "alarm_control_panel.hmip_alarm_control_panel" + entity_name = "HmIP Alarm Control Panel" + device_model = None + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "disarmed" + assert not hmip_device + + home = default_mock_hap.home + service_call_counter = len(home.mock_calls) + + await hass.services.async_call( + "alarm_control_panel", "alarm_arm_away", {"entity_id": entity_id}, blocking=True + ) + assert len(home.mock_calls) == service_call_counter + 1 + assert home.mock_calls[-1][0] == "set_security_zones_activation" + assert home.mock_calls[-1][1] == (True, True) + await _async_manipulate_security_zones( + hass, + home, + internal_active=True, + external_active=True, + window_state=WindowState.CLOSED, + ) + assert hass.states.get(entity_id).state is STATE_ALARM_ARMED_AWAY + + await hass.services.async_call( + "alarm_control_panel", "alarm_arm_home", {"entity_id": entity_id}, blocking=True + ) + assert len(home.mock_calls) == service_call_counter + 3 + assert home.mock_calls[-1][0] == "set_security_zones_activation" + assert home.mock_calls[-1][1] == (False, True) + await _async_manipulate_security_zones( + hass, + home, + internal_active=False, + external_active=True, + window_state=WindowState.CLOSED, + ) + assert hass.states.get(entity_id).state is STATE_ALARM_ARMED_HOME + + await hass.services.async_call( + "alarm_control_panel", "alarm_disarm", {"entity_id": entity_id}, blocking=True + ) + assert len(home.mock_calls) == service_call_counter + 5 + assert home.mock_calls[-1][0] == "set_security_zones_activation" + assert home.mock_calls[-1][1] == (False, False) + await _async_manipulate_security_zones( + hass, + home, + internal_active=False, + external_active=False, + window_state=WindowState.CLOSED, + ) + assert hass.states.get(entity_id).state is STATE_ALARM_DISARMED + + await hass.services.async_call( + "alarm_control_panel", "alarm_arm_away", {"entity_id": entity_id}, blocking=True + ) + assert len(home.mock_calls) == service_call_counter + 7 + assert home.mock_calls[-1][0] == "set_security_zones_activation" + assert home.mock_calls[-1][1] == (True, True) + await _async_manipulate_security_zones( + hass, + home, + internal_active=True, + external_active=True, + window_state=WindowState.OPEN, + ) + assert hass.states.get(entity_id).state is STATE_ALARM_TRIGGERED + + await hass.services.async_call( + "alarm_control_panel", "alarm_arm_home", {"entity_id": entity_id}, blocking=True + ) + assert len(home.mock_calls) == service_call_counter + 9 + assert home.mock_calls[-1][0] == "set_security_zones_activation" + assert home.mock_calls[-1][1] == (False, True) + await _async_manipulate_security_zones( + hass, + home, + internal_active=False, + external_active=True, + window_state=WindowState.OPEN, + ) + assert hass.states.get(entity_id).state is STATE_ALARM_TRIGGERED diff --git a/tests/components/homematicip_cloud/test_binary_sensor.py b/tests/components/homematicip_cloud/test_binary_sensor.py new file mode 100644 index 00000000000..0760518171e --- /dev/null +++ b/tests/components/homematicip_cloud/test_binary_sensor.py @@ -0,0 +1,305 @@ +"""Tests for HomematicIP Cloud binary sensor.""" +from homematicip.base.enums import SmokeDetectorAlarmType, WindowState + +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN +from homeassistant.components.homematicip_cloud.binary_sensor import ( + ATTR_ACCELERATION_SENSOR_MODE, + ATTR_ACCELERATION_SENSOR_NEUTRAL_POSITION, + ATTR_ACCELERATION_SENSOR_SENSITIVITY, + ATTR_ACCELERATION_SENSOR_TRIGGER_ANGLE, + ATTR_LOW_BATTERY, + ATTR_MOTION_DETECTED, +) +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.setup import async_setup_component + +from .helper import async_manipulate_test_data, get_and_check_entity_basics + + +async def test_manually_configured_platform(hass): + """Test that we do not set up an access point.""" + assert ( + await async_setup_component( + hass, + BINARY_SENSOR_DOMAIN, + {BINARY_SENSOR_DOMAIN: {"platform": HMIPC_DOMAIN}}, + ) + is True + ) + assert not hass.data.get(HMIPC_DOMAIN) + + +async def test_hmip_acceleration_sensor(hass, default_mock_hap): + """Test HomematicipAccelerationSensor.""" + entity_id = "binary_sensor.garagentor" + entity_name = "Garagentor" + device_model = "HmIP-SAM" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_ON + assert ha_state.attributes[ATTR_ACCELERATION_SENSOR_MODE] == "FLAT_DECT" + assert ha_state.attributes[ATTR_ACCELERATION_SENSOR_NEUTRAL_POSITION] == "VERTICAL" + assert ( + ha_state.attributes[ATTR_ACCELERATION_SENSOR_SENSITIVITY] == "SENSOR_RANGE_4G" + ) + assert ha_state.attributes[ATTR_ACCELERATION_SENSOR_TRIGGER_ANGLE] == 45 + service_call_counter = len(hmip_device.mock_calls) + + await async_manipulate_test_data( + hass, hmip_device, "accelerationSensorTriggered", False + ) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF + assert len(hmip_device.mock_calls) == service_call_counter + 1 + + await async_manipulate_test_data( + hass, hmip_device, "accelerationSensorTriggered", True + ) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + assert len(hmip_device.mock_calls) == service_call_counter + 2 + + +async def test_hmip_contact_interface(hass, default_mock_hap): + """Test HomematicipContactInterface.""" + entity_id = "binary_sensor.kontakt_schnittstelle_unterputz_1_fach" + entity_name = "Kontakt-Schnittstelle Unterputz – 1-fach" + device_model = "HmIP-FCI1" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_OFF + await async_manipulate_test_data(hass, hmip_device, "windowState", WindowState.OPEN) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + + await async_manipulate_test_data(hass, hmip_device, "windowState", None) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF + + +async def test_hmip_shutter_contact(hass, default_mock_hap): + """Test HomematicipShutterContact.""" + entity_id = "binary_sensor.fenstergriffsensor" + entity_name = "Fenstergriffsensor" + device_model = "HmIP-SRH" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_ON + await async_manipulate_test_data( + hass, hmip_device, "windowState", WindowState.CLOSED + ) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF + + await async_manipulate_test_data(hass, hmip_device, "windowState", None) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF + + +async def test_hmip_motion_detector(hass, default_mock_hap): + """Test HomematicipMotionDetector.""" + entity_id = "binary_sensor.bewegungsmelder_fur_55er_rahmen_innen" + entity_name = "Bewegungsmelder für 55er Rahmen – innen" + device_model = "HmIP-SMI55" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_OFF + await async_manipulate_test_data(hass, hmip_device, "motionDetected", True) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + + +async def test_hmip_presence_detector(hass, default_mock_hap): + """Test HomematicipPresenceDetector.""" + entity_id = "binary_sensor.spi_1" + entity_name = "SPI_1" + device_model = "HmIP-SPI" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_OFF + await async_manipulate_test_data(hass, hmip_device, "presenceDetected", True) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + + +async def test_hmip_smoke_detector(hass, default_mock_hap): + """Test HomematicipSmokeDetector.""" + entity_id = "binary_sensor.rauchwarnmelder" + entity_name = "Rauchwarnmelder" + device_model = "HmIP-SWSD" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_OFF + await async_manipulate_test_data( + hass, + hmip_device, + "smokeDetectorAlarmType", + SmokeDetectorAlarmType.PRIMARY_ALARM, + ) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + + +async def test_hmip_water_detector(hass, default_mock_hap): + """Test HomematicipWaterDetector.""" + entity_id = "binary_sensor.wassersensor" + entity_name = "Wassersensor" + device_model = "HmIP-SWD" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_OFF + await async_manipulate_test_data(hass, hmip_device, "waterlevelDetected", True) + await async_manipulate_test_data(hass, hmip_device, "moistureDetected", False) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + + await async_manipulate_test_data(hass, hmip_device, "waterlevelDetected", True) + await async_manipulate_test_data(hass, hmip_device, "moistureDetected", True) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + + await async_manipulate_test_data(hass, hmip_device, "waterlevelDetected", False) + await async_manipulate_test_data(hass, hmip_device, "moistureDetected", True) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + + await async_manipulate_test_data(hass, hmip_device, "waterlevelDetected", False) + await async_manipulate_test_data(hass, hmip_device, "moistureDetected", False) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF + + +async def test_hmip_storm_sensor(hass, default_mock_hap): + """Test HomematicipStormSensor.""" + entity_id = "binary_sensor.weather_sensor_plus_storm" + entity_name = "Weather Sensor – plus Storm" + device_model = "HmIP-SWO-PL" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_OFF + await async_manipulate_test_data(hass, hmip_device, "storm", True) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + + +async def test_hmip_rain_sensor(hass, default_mock_hap): + """Test HomematicipRainSensor.""" + entity_id = "binary_sensor.wettersensor_pro_raining" + entity_name = "Wettersensor - pro Raining" + device_model = "HmIP-SWO-PR" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_OFF + await async_manipulate_test_data(hass, hmip_device, "raining", True) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + + +async def test_hmip_sunshine_sensor(hass, default_mock_hap): + """Test HomematicipSunshineSensor.""" + entity_id = "binary_sensor.wettersensor_pro_sunshine" + entity_name = "Wettersensor - pro Sunshine" + device_model = "HmIP-SWO-PR" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_ON + assert ha_state.attributes["today_sunshine_duration_in_minutes"] == 100 + await async_manipulate_test_data(hass, hmip_device, "sunshine", False) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF + + +async def test_hmip_battery_sensor(hass, default_mock_hap): + """Test HomematicipSunshineSensor.""" + entity_id = "binary_sensor.wohnungsture_battery" + entity_name = "Wohnungstüre Battery" + device_model = "HMIP-SWDO" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_OFF + await async_manipulate_test_data(hass, hmip_device, "lowBat", True) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + + +async def test_hmip_security_zone_sensor_group(hass, default_mock_hap): + """Test HomematicipSecurityZoneSensorGroup.""" + entity_id = "binary_sensor.internal_securityzone" + entity_name = "INTERNAL SecurityZone" + device_model = "HmIP-SecurityZone" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_OFF + await async_manipulate_test_data(hass, hmip_device, "motionDetected", True) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + assert ha_state.attributes[ATTR_MOTION_DETECTED] is True + + +async def test_hmip_security_sensor_group(hass, default_mock_hap): + """Test HomematicipSecuritySensorGroup.""" + entity_id = "binary_sensor.buro_sensors" + entity_name = "Büro Sensors" + device_model = None + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_OFF + assert not ha_state.attributes.get("low_bat") + await async_manipulate_test_data(hass, hmip_device, "lowBat", True) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + assert ha_state.attributes[ATTR_LOW_BATTERY] is True + + await async_manipulate_test_data(hass, hmip_device, "lowBat", False) + await async_manipulate_test_data( + hass, + hmip_device, + "smokeDetectorAlarmType", + SmokeDetectorAlarmType.PRIMARY_ALARM, + ) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + assert ( + ha_state.attributes["smoke_detector_alarm"] + == SmokeDetectorAlarmType.PRIMARY_ALARM + ) diff --git a/tests/components/homematicip_cloud/test_climate.py b/tests/components/homematicip_cloud/test_climate.py new file mode 100644 index 00000000000..80e4e74e451 --- /dev/null +++ b/tests/components/homematicip_cloud/test_climate.py @@ -0,0 +1,315 @@ +"""Tests for HomematicIP Cloud climate.""" +import datetime + +from homematicip.base.enums import AbsenceType +from homematicip.functionalHomes import IndoorClimateHome + +from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN +from homeassistant.components.climate.const import ( + ATTR_CURRENT_TEMPERATURE, + ATTR_PRESET_MODE, + ATTR_PRESET_MODES, + HVAC_MODE_AUTO, + HVAC_MODE_HEAT, + PRESET_AWAY, + PRESET_BOOST, + PRESET_ECO, + PRESET_NONE, +) +from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN +from homeassistant.setup import async_setup_component + +from .helper import HAPID, async_manipulate_test_data, get_and_check_entity_basics + + +async def test_manually_configured_platform(hass): + """Test that we do not set up an access point.""" + assert ( + await async_setup_component( + hass, CLIMATE_DOMAIN, {CLIMATE_DOMAIN: {"platform": HMIPC_DOMAIN}} + ) + is True + ) + assert not hass.data.get(HMIPC_DOMAIN) + + +async def test_hmip_heating_group(hass, default_mock_hap): + """Test HomematicipHeatingGroup.""" + entity_id = "climate.badezimmer" + entity_name = "Badezimmer" + device_model = None + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == HVAC_MODE_AUTO + assert ha_state.attributes["current_temperature"] == 23.8 + assert ha_state.attributes["min_temp"] == 5.0 + assert ha_state.attributes["max_temp"] == 30.0 + assert ha_state.attributes["temperature"] == 5.0 + assert ha_state.attributes["current_humidity"] == 47 + assert ha_state.attributes[ATTR_PRESET_MODE] == "STD" + assert ha_state.attributes[ATTR_PRESET_MODES] == [ + PRESET_NONE, + PRESET_BOOST, + "STD", + "Winter", + ] + + service_call_counter = len(hmip_device.mock_calls) + + await hass.services.async_call( + "climate", + "set_temperature", + {"entity_id": entity_id, "temperature": 22.5}, + blocking=True, + ) + assert len(hmip_device.mock_calls) == service_call_counter + 1 + assert hmip_device.mock_calls[-1][0] == "set_point_temperature" + assert hmip_device.mock_calls[-1][1] == (22.5,) + await async_manipulate_test_data(hass, hmip_device, "actualTemperature", 22.5) + ha_state = hass.states.get(entity_id) + assert ha_state.attributes[ATTR_CURRENT_TEMPERATURE] == 22.5 + + await hass.services.async_call( + "climate", + "set_hvac_mode", + {"entity_id": entity_id, "hvac_mode": HVAC_MODE_HEAT}, + blocking=True, + ) + assert len(hmip_device.mock_calls) == service_call_counter + 3 + assert hmip_device.mock_calls[-1][0] == "set_control_mode" + assert hmip_device.mock_calls[-1][1] == ("MANUAL",) + await async_manipulate_test_data(hass, hmip_device, "controlMode", "MANUAL") + ha_state = hass.states.get(entity_id) + assert ha_state.state == HVAC_MODE_HEAT + + await hass.services.async_call( + "climate", + "set_hvac_mode", + {"entity_id": entity_id, "hvac_mode": HVAC_MODE_AUTO}, + blocking=True, + ) + assert len(hmip_device.mock_calls) == service_call_counter + 5 + assert hmip_device.mock_calls[-1][0] == "set_control_mode" + assert hmip_device.mock_calls[-1][1] == ("AUTOMATIC",) + await async_manipulate_test_data(hass, hmip_device, "controlMode", "AUTO") + ha_state = hass.states.get(entity_id) + assert ha_state.state == HVAC_MODE_AUTO + + await hass.services.async_call( + "climate", + "set_preset_mode", + {"entity_id": entity_id, "preset_mode": PRESET_BOOST}, + blocking=True, + ) + assert len(hmip_device.mock_calls) == service_call_counter + 7 + assert hmip_device.mock_calls[-1][0] == "set_boost" + assert hmip_device.mock_calls[-1][1] == () + await async_manipulate_test_data(hass, hmip_device, "boostMode", True) + ha_state = hass.states.get(entity_id) + assert ha_state.attributes[ATTR_PRESET_MODE] == PRESET_BOOST + + await hass.services.async_call( + "climate", + "set_preset_mode", + {"entity_id": entity_id, "preset_mode": PRESET_NONE}, + blocking=True, + ) + assert len(hmip_device.mock_calls) == service_call_counter + 9 + assert hmip_device.mock_calls[-1][0] == "set_boost" + assert hmip_device.mock_calls[-1][1] == (False,) + await async_manipulate_test_data(hass, hmip_device, "boostMode", False) + ha_state = hass.states.get(entity_id) + assert ha_state.attributes[ATTR_PRESET_MODE] == "STD" + + # Not required for hmip, but a posiblity to send no temperature. + await hass.services.async_call( + "climate", + "set_temperature", + {"entity_id": entity_id, "target_temp_low": 10, "target_temp_high": 10}, + blocking=True, + ) + # No new service call should be in mock_calls. + assert len(hmip_device.mock_calls) == service_call_counter + 10 + # 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, "controlMode", "ECO") + await async_manipulate_test_data( + hass, + default_mock_hap.home.get_functionalHome(IndoorClimateHome), + "absenceType", + AbsenceType.VACATION, + fire_device=hmip_device, + ) + ha_state = hass.states.get(entity_id) + assert ha_state.attributes[ATTR_PRESET_MODE] == PRESET_AWAY + + await async_manipulate_test_data(hass, hmip_device, "controlMode", "ECO") + await async_manipulate_test_data( + hass, + default_mock_hap.home.get_functionalHome(IndoorClimateHome), + "absenceType", + AbsenceType.PERIOD, + fire_device=hmip_device, + ) + ha_state = hass.states.get(entity_id) + assert ha_state.attributes[ATTR_PRESET_MODE] == PRESET_ECO + + # Not required for hmip, but a posiblity to send no temperature. + await hass.services.async_call( + "climate", + "set_preset_mode", + {"entity_id": entity_id, "preset_mode": "Winter"}, + blocking=True, + ) + + assert len(hmip_device.mock_calls) == service_call_counter + 16 + assert hmip_device.mock_calls[-1][0] == "set_active_profile" + assert hmip_device.mock_calls[-1][1] == (1,) + + +async def test_hmip_climate_services(hass, mock_hap_with_service): + """Test HomematicipHeatingGroup.""" + + home = mock_hap_with_service.home + + await hass.services.async_call( + "homematicip_cloud", + "activate_eco_mode_with_duration", + {"duration": 60, "accesspoint_id": HAPID}, + blocking=True, + ) + 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 + + await hass.services.async_call( + "homematicip_cloud", + "activate_eco_mode_with_duration", + {"duration": 60}, + blocking=True, + ) + 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 + + await hass.services.async_call( + "homematicip_cloud", + "activate_eco_mode_with_period", + {"endtime": "2019-02-17 14:00", "accesspoint_id": HAPID}, + blocking=True, + ) + 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 + + await hass.services.async_call( + "homematicip_cloud", + "activate_eco_mode_with_period", + {"endtime": "2019-02-17 14:00"}, + blocking=True, + ) + 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 + + await hass.services.async_call( + "homematicip_cloud", + "activate_vacation", + {"endtime": "2019-02-17 14:00", "temperature": 18.5, "accesspoint_id": HAPID}, + blocking=True, + ) + 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 + + await hass.services.async_call( + "homematicip_cloud", + "activate_vacation", + {"endtime": "2019-02-17 14:00", "temperature": 18.5}, + blocking=True, + ) + 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 + + await hass.services.async_call( + "homematicip_cloud", + "deactivate_eco_mode", + {"accesspoint_id": HAPID}, + blocking=True, + ) + assert home.mock_calls[-1][0] == "deactivate_absence" + assert home.mock_calls[-1][1] == () + assert len(home._connection.mock_calls) == 7 # pylint: disable=W0212 + + 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 + + await hass.services.async_call( + "homematicip_cloud", + "deactivate_vacation", + {"accesspoint_id": HAPID}, + blocking=True, + ) + assert home.mock_calls[-1][0] == "deactivate_vacation" + assert home.mock_calls[-1][1] == () + assert len(home._connection.mock_calls) == 9 # pylint: disable=W0212 + + 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 + + not_existing_hap_id = "5555F7110000000000000001" + await hass.services.async_call( + "homematicip_cloud", + "deactivate_vacation", + {"accesspoint_id": not_existing_hap_id}, + blocking=True, + ) + 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 + + +async def test_hmip_heating_group_services(hass, mock_hap_with_service): + """Test HomematicipHeatingGroup services.""" + entity_id = "climate.badezimmer" + entity_name = "Badezimmer" + device_model = None + + ha_state, hmip_device = get_and_check_entity_basics( + hass, mock_hap_with_service, entity_id, entity_name, device_model + ) + assert ha_state + + await hass.services.async_call( + "homematicip_cloud", + "set_active_climate_profile", + {"climate_profile_index": 2, "entity_id": "climate.badezimmer"}, + blocking=True, + ) + 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 + + await hass.services.async_call( + "homematicip_cloud", + "set_active_climate_profile", + {"climate_profile_index": 2, "entity_id": "all"}, + blocking=True, + ) + 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 diff --git a/tests/components/homematicip_cloud/test_config_flow.py b/tests/components/homematicip_cloud/test_config_flow.py index c1bad855701..54cb309755d 100644 --- a/tests/components/homematicip_cloud/test_config_flow.py +++ b/tests/components/homematicip_cloud/test_config_flow.py @@ -1,8 +1,7 @@ """Tests for HomematicIP Cloud config flow.""" from unittest.mock import patch -from homeassistant.components.homematicip_cloud import hap as hmipc -from homeassistant.components.homematicip_cloud import config_flow, const +from homeassistant.components.homematicip_cloud import config_flow, const, hap as hmipc from tests.common import MockConfigEntry, mock_coro diff --git a/tests/components/homematicip_cloud/test_cover.py b/tests/components/homematicip_cloud/test_cover.py new file mode 100644 index 00000000000..22922303f9e --- /dev/null +++ b/tests/components/homematicip_cloud/test_cover.py @@ -0,0 +1,155 @@ +"""Tests for HomematicIP Cloud cover.""" +from homeassistant.components.cover import ( + ATTR_CURRENT_POSITION, + ATTR_CURRENT_TILT_POSITION, + DOMAIN as COVER_DOMAIN, +) +from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN +from homeassistant.const import STATE_CLOSED, STATE_OPEN +from homeassistant.setup import async_setup_component + +from .helper import async_manipulate_test_data, get_and_check_entity_basics + + +async def test_manually_configured_platform(hass): + """Test that we do not set up an access point.""" + assert ( + await async_setup_component( + hass, COVER_DOMAIN, {COVER_DOMAIN: {"platform": HMIPC_DOMAIN}} + ) + is True + ) + assert not hass.data.get(HMIPC_DOMAIN) + + +async def test_hmip_cover_shutter(hass, default_mock_hap): + """Test HomematicipCoverShutte.""" + entity_id = "cover.sofa_links" + entity_name = "Sofa links" + device_model = "HmIP-FBL" + + 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 + assert ha_state.attributes["current_tilt_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] == "set_shutter_level" + assert hmip_device.mock_calls[-1][1] == (0,) + await async_manipulate_test_data(hass, hmip_device, "shutterLevel", 0) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OPEN + assert ha_state.attributes[ATTR_CURRENT_POSITION] == 100 + assert ha_state.attributes[ATTR_CURRENT_TILT_POSITION] == 0 + + await hass.services.async_call( + "cover", + "set_cover_position", + {"entity_id": entity_id, "position": "50"}, + blocking=True, + ) + assert len(hmip_device.mock_calls) == service_call_counter + 3 + assert hmip_device.mock_calls[-1][0] == "set_shutter_level" + assert hmip_device.mock_calls[-1][1] == (0.5,) + await async_manipulate_test_data(hass, hmip_device, "shutterLevel", 0.5) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OPEN + assert ha_state.attributes[ATTR_CURRENT_POSITION] == 50 + assert ha_state.attributes[ATTR_CURRENT_TILT_POSITION] == 0 + + await hass.services.async_call( + "cover", "close_cover", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 5 + assert hmip_device.mock_calls[-1][0] == "set_shutter_level" + assert hmip_device.mock_calls[-1][1] == (1,) + await async_manipulate_test_data(hass, hmip_device, "shutterLevel", 1) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_CLOSED + assert ha_state.attributes[ATTR_CURRENT_POSITION] == 0 + assert ha_state.attributes[ATTR_CURRENT_TILT_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 + 7 + assert hmip_device.mock_calls[-1][0] == "set_shutter_stop" + assert hmip_device.mock_calls[-1][1] == () + + await async_manipulate_test_data(hass, hmip_device, "shutterLevel", None) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_CLOSED + + +async def test_hmip_cover_slats(hass, default_mock_hap): + """Test HomematicipCoverSlats.""" + entity_id = "cover.sofa_links" + entity_name = "Sofa links" + device_model = "HmIP-FBL" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_CLOSED + assert ha_state.attributes[ATTR_CURRENT_POSITION] == 0 + assert ha_state.attributes[ATTR_CURRENT_TILT_POSITION] == 0 + service_call_counter = len(hmip_device.mock_calls) + + await hass.services.async_call( + "cover", "open_cover_tilt", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 1 + assert hmip_device.mock_calls[-1][0] == "set_slats_level" + assert hmip_device.mock_calls[-1][1] == (0,) + await async_manipulate_test_data(hass, hmip_device, "shutterLevel", 0) + await async_manipulate_test_data(hass, hmip_device, "slatsLevel", 0) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OPEN + assert ha_state.attributes[ATTR_CURRENT_POSITION] == 100 + assert ha_state.attributes[ATTR_CURRENT_TILT_POSITION] == 100 + + await hass.services.async_call( + "cover", + "set_cover_tilt_position", + {"entity_id": entity_id, "tilt_position": "50"}, + blocking=True, + ) + assert len(hmip_device.mock_calls) == service_call_counter + 4 + assert hmip_device.mock_calls[-1][0] == "set_slats_level" + assert hmip_device.mock_calls[-1][1] == (0.5,) + await async_manipulate_test_data(hass, hmip_device, "slatsLevel", 0.5) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OPEN + assert ha_state.attributes[ATTR_CURRENT_POSITION] == 100 + assert ha_state.attributes[ATTR_CURRENT_TILT_POSITION] == 50 + + await hass.services.async_call( + "cover", "close_cover_tilt", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 6 + assert hmip_device.mock_calls[-1][0] == "set_slats_level" + assert hmip_device.mock_calls[-1][1] == (1,) + await async_manipulate_test_data(hass, hmip_device, "slatsLevel", 1) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OPEN + assert ha_state.attributes[ATTR_CURRENT_POSITION] == 100 + assert ha_state.attributes[ATTR_CURRENT_TILT_POSITION] == 0 + + await hass.services.async_call( + "cover", "stop_cover_tilt", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 8 + assert hmip_device.mock_calls[-1][0] == "set_shutter_stop" + assert hmip_device.mock_calls[-1][1] == () + + await async_manipulate_test_data(hass, hmip_device, "shutterLevel", None) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OPEN diff --git a/tests/components/homematicip_cloud/test_device.py b/tests/components/homematicip_cloud/test_device.py new file mode 100644 index 00000000000..812f32a3344 --- /dev/null +++ b/tests/components/homematicip_cloud/test_device.py @@ -0,0 +1,132 @@ +"""Common tests for HomematicIP devices.""" +from homeassistant.const import STATE_ON, STATE_UNAVAILABLE +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from .conftest import get_mock_hap +from .helper import async_manipulate_test_data, get_and_check_entity_basics + + +async def test_hmip_remove_device(hass, default_mock_hap): + """Test Remove of hmip device.""" + entity_id = "light.treppe" + entity_name = "Treppe" + device_model = "HmIP-BSL" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_ON + assert hmip_device + + device_registry = await dr.async_get_registry(hass) + entity_registry = await er.async_get_registry(hass) + + pre_device_count = len(device_registry.devices) + pre_entity_count = len(entity_registry.entities) + pre_mapping_count = len(default_mock_hap.hmip_device_by_entity_id) + + hmip_device.fire_remove_event() + + await hass.async_block_till_done() + + assert len(device_registry.devices) == pre_device_count - 1 + assert len(entity_registry.entities) == pre_entity_count - 3 + assert len(default_mock_hap.hmip_device_by_entity_id) == pre_mapping_count - 3 + + +async def test_hmip_remove_group(hass, default_mock_hap): + """Test Remove of hmip group.""" + entity_id = "switch.strom_group" + entity_name = "Strom Group" + device_model = None + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_ON + assert hmip_device + + device_registry = await dr.async_get_registry(hass) + entity_registry = await er.async_get_registry(hass) + + pre_device_count = len(device_registry.devices) + pre_entity_count = len(entity_registry.entities) + pre_mapping_count = len(default_mock_hap.hmip_device_by_entity_id) + + hmip_device.fire_remove_event() + + await hass.async_block_till_done() + + assert len(device_registry.devices) == pre_device_count + assert len(entity_registry.entities) == pre_entity_count - 1 + assert len(default_mock_hap.hmip_device_by_entity_id) == pre_mapping_count - 1 + + +async def test_all_devices_unavailable_when_hap_not_connected(hass, default_mock_hap): + """Test make all devices unavaulable when hap is not connected.""" + entity_id = "light.treppe" + entity_name = "Treppe" + device_model = "HmIP-BSL" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_ON + assert hmip_device + + assert default_mock_hap.home.connected + + await async_manipulate_test_data(hass, default_mock_hap.home, "connected", False) + + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_UNAVAILABLE + + +async def test_hap_reconnected(hass, default_mock_hap): + """Test reconnect hap.""" + entity_id = "light.treppe" + entity_name = "Treppe" + device_model = "HmIP-BSL" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_ON + assert hmip_device + + assert default_mock_hap.home.connected + + await async_manipulate_test_data(hass, default_mock_hap.home, "connected", False) + + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_UNAVAILABLE + + default_mock_hap._accesspoint_connected = False # pylint: disable=W0212 + 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) + assert ha_state.state == STATE_ON + + +async def test_hap_with_name(hass, mock_connection, hmip_config_entry): + """Test hap with name.""" + home_name = "TestName" + entity_id = f"light.{home_name.lower()}_treppe" + entity_name = f"{home_name} Treppe" + device_model = "HmIP-BSL" + + hmip_config_entry.data["name"] = home_name + mock_hap = await get_mock_hap(hass, mock_connection, hmip_config_entry) + assert mock_hap + + ha_state, hmip_device = get_and_check_entity_basics( + hass, mock_hap, entity_id, entity_name, device_model + ) + + assert hmip_device + assert ha_state.state == STATE_ON + assert ha_state.attributes["friendly_name"] == entity_name diff --git a/tests/components/homematicip_cloud/test_hap.py b/tests/components/homematicip_cloud/test_hap.py index 34afd19310f..324649ef515 100644 --- a/tests/components/homematicip_cloud/test_hap.py +++ b/tests/components/homematicip_cloud/test_hap.py @@ -1,11 +1,24 @@ """Test HomematicIP Cloud accesspoint.""" -from unittest.mock import Mock, patch +from asynctest import Mock, patch +from homematicip.aio.auth import AsyncAuth +from homematicip.base.base_connection import HmipConnectionError import pytest +from homeassistant.components.homematicip_cloud import ( + DOMAIN as HMIPC_DOMAIN, + const, + errors, + hap as hmipc, +) +from homeassistant.components.homematicip_cloud.hap import ( + HomematicipAuth, + HomematicipHAP, +) from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.components.homematicip_cloud import hap as hmipc -from homeassistant.components.homematicip_cloud import const, errors + +from .helper import HAPID, HAPPIN + from tests.common import mock_coro, mock_coro_func @@ -18,7 +31,7 @@ async def test_auth_setup(hass): } hap = hmipc.HomematicipAuth(hass, config) with patch.object(hap, "get_auth", return_value=mock_coro()): - assert await hap.async_setup() is True + assert await hap.async_setup() async def test_auth_setup_connection_error(hass): @@ -30,7 +43,7 @@ async def test_auth_setup_connection_error(hass): } hap = hmipc.HomematicipAuth(hass, config) with patch.object(hap, "get_auth", side_effect=errors.HmipcConnectionError): - assert await hap.async_setup() is False + assert not await hap.async_setup() async def test_auth_auth_check_and_register(hass): @@ -49,10 +62,26 @@ async def test_auth_auth_check_and_register(hass): ), patch.object( hap.auth, "confirmAuthToken", return_value=mock_coro() ): - assert await hap.async_checkbutton() is True + assert await hap.async_checkbutton() assert await hap.async_register() == "ABC" +async def test_auth_auth_check_and_register_with_exception(hass): + """Test auth client registration.""" + config = { + const.HMIPC_HAPID: "ABC123", + const.HMIPC_PIN: "123", + const.HMIPC_NAME: "hmip", + } + hap = hmipc.HomematicipAuth(hass, config) + hap.auth = Mock(spec=AsyncAuth) + with patch.object( + hap.auth, "isRequestAcknowledged", side_effect=HmipConnectionError + ), patch.object(hap.auth, "requestAuthToken", side_effect=HmipConnectionError): + assert not await hap.async_checkbutton() + assert await hap.async_register() is False + + async def test_hap_setup_works(aioclient_mock): """Test a successful setup of a accesspoint.""" hass = Mock() @@ -65,7 +94,7 @@ async def test_hap_setup_works(aioclient_mock): } hap = hmipc.HomematicipHAP(hass, entry) with patch.object(hap, "get_hap", return_value=mock_coro(home)): - assert await hap.async_setup() is True + assert await hap.async_setup() assert hap.home is home assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 8 @@ -94,8 +123,8 @@ async def test_hap_setup_connection_error(): ), pytest.raises(ConfigEntryNotReady): await hap.async_setup() - assert len(hass.async_add_job.mock_calls) == 0 - assert len(hass.config_entries.flow.async_init.mock_calls) == 0 + assert not hass.async_add_job.mock_calls + assert not hass.config_entries.flow.async_init.mock_calls async def test_hap_reset_unloads_entry_if_setup(): @@ -111,13 +140,88 @@ async def test_hap_reset_unloads_entry_if_setup(): } hap = hmipc.HomematicipHAP(hass, entry) with patch.object(hap, "get_hap", return_value=mock_coro(home)): - assert await hap.async_setup() is True + assert await hap.async_setup() assert hap.home is home - assert len(hass.services.async_register.mock_calls) == 0 + assert not hass.services.async_register.mock_calls assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 8 hass.config_entries.async_forward_entry_unload.return_value = mock_coro(True) await hap.async_reset() assert len(hass.config_entries.async_forward_entry_unload.mock_calls) == 8 + + +async def test_hap_create(hass, hmip_config_entry, simple_mock_home): + """Mock AsyncHome to execute get_hap.""" + hass.config.components.add(HMIPC_DOMAIN) + hap = HomematicipHAP(hass, hmip_config_entry) + assert hap + with patch( + "homeassistant.components.homematicip_cloud.hap.AsyncHome", + return_value=simple_mock_home, + ), patch.object(hap, "async_connect", return_value=mock_coro(None)): + assert await hap.async_setup() + + +async def test_hap_create_exception(hass, hmip_config_entry, simple_mock_home): + """Mock AsyncHome to execute get_hap.""" + hass.config.components.add(HMIPC_DOMAIN) + hap = HomematicipHAP(hass, hmip_config_entry) + assert hap + + with patch.object(hap, "get_hap", side_effect=HmipConnectionError), pytest.raises( + HmipConnectionError + ): + await hap.async_setup() + + simple_mock_home.init.side_effect = HmipConnectionError + with patch( + "homeassistant.components.homematicip_cloud.hap.AsyncHome", + return_value=simple_mock_home, + ), pytest.raises(ConfigEntryNotReady): + await hap.async_setup() + + +async def test_auth_create(hass, simple_mock_auth): + """Mock AsyncAuth to execute get_auth.""" + config = { + const.HMIPC_HAPID: HAPID, + const.HMIPC_PIN: HAPPIN, + const.HMIPC_NAME: "hmip", + } + hmip_auth = HomematicipAuth(hass, config) + assert hmip_auth + + with patch( + "homeassistant.components.homematicip_cloud.hap.AsyncAuth", + return_value=simple_mock_auth, + ): + assert await hmip_auth.async_setup() + await hass.async_block_till_done() + assert hmip_auth.auth.pin == HAPPIN + + +async def test_auth_create_exception(hass, simple_mock_auth): + """Mock AsyncAuth to execute get_auth.""" + config = { + const.HMIPC_HAPID: HAPID, + const.HMIPC_PIN: HAPPIN, + const.HMIPC_NAME: "hmip", + } + hmip_auth = HomematicipAuth(hass, config) + simple_mock_auth.connectionRequest.side_effect = HmipConnectionError + assert hmip_auth + with patch( + "homeassistant.components.homematicip_cloud.hap.AsyncAuth", + return_value=simple_mock_auth, + ): + assert await hmip_auth.async_setup() + await hass.async_block_till_done() + assert not hmip_auth.auth + + with patch( + "homeassistant.components.homematicip_cloud.hap.AsyncAuth", + return_value=simple_mock_auth, + ): + assert not await hmip_auth.get_auth(hass, HAPID, HAPPIN) diff --git a/tests/components/homematicip_cloud/test_init.py b/tests/components/homematicip_cloud/test_init.py index d77d4a7e5b2..894db2e691b 100644 --- a/tests/components/homematicip_cloud/test_init.py +++ b/tests/components/homematicip_cloud/test_init.py @@ -2,10 +2,10 @@ from unittest.mock import patch -from homeassistant.setup import async_setup_component from homeassistant.components import homematicip_cloud as hmipc +from homeassistant.setup import async_setup_component -from tests.common import mock_coro, MockConfigEntry +from tests.common import MockConfigEntry, mock_coro async def test_config_with_accesspoint_passed_to_config_entry(hass): @@ -53,7 +53,7 @@ async def test_config_already_registered_not_passed_to_config_entry(hass): ) # No flow started - assert len(mock_config_entries.flow.mock_calls) == 0 + assert not mock_config_entries.flow.mock_calls async def test_setup_entry_successful(hass): diff --git a/tests/components/homematicip_cloud/test_light.py b/tests/components/homematicip_cloud/test_light.py new file mode 100644 index 00000000000..17e92d9d99d --- /dev/null +++ b/tests/components/homematicip_cloud/test_light.py @@ -0,0 +1,223 @@ +"""Tests for HomematicIP Cloud light.""" +from homematicip.base.enums import RGBColorState + +from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN +from homeassistant.components.homematicip_cloud.light import ( + ATTR_ENERGY_COUNTER, + ATTR_POWER_CONSUMPTION, +) +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_NAME, + DOMAIN as LIGHT_DOMAIN, +) +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.setup import async_setup_component + +from .helper import async_manipulate_test_data, get_and_check_entity_basics + + +async def test_manually_configured_platform(hass): + """Test that we do not set up an access point.""" + assert ( + await async_setup_component( + hass, LIGHT_DOMAIN, {LIGHT_DOMAIN: {"platform": HMIPC_DOMAIN}} + ) + is True + ) + assert not hass.data.get(HMIPC_DOMAIN) + + +async def test_hmip_light(hass, default_mock_hap): + """Test HomematicipLight.""" + entity_id = "light.treppe" + entity_name = "Treppe" + device_model = "HmIP-BSL" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_ON + + service_call_counter = len(hmip_device.mock_calls) + await hass.services.async_call( + "light", "turn_off", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 1 + assert hmip_device.mock_calls[-1][0] == "turn_off" + assert hmip_device.mock_calls[-1][1] == () + + await async_manipulate_test_data(hass, hmip_device, "on", False) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF + + await hass.services.async_call( + "light", "turn_on", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 3 + assert hmip_device.mock_calls[-1][0] == "turn_on" + assert hmip_device.mock_calls[-1][1] == () + + await async_manipulate_test_data(hass, hmip_device, "on", True) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + + +async def test_hmip_notification_light(hass, default_mock_hap): + """Test HomematicipNotificationLight.""" + entity_id = "light.treppe_top_notification" + entity_name = "Treppe Top Notification" + device_model = "HmIP-BSL" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_OFF + service_call_counter = len(hmip_device.mock_calls) + + # Send all color via service call. + await hass.services.async_call( + "light", "turn_on", {"entity_id": entity_id}, blocking=True + ) + assert hmip_device.mock_calls[-1][0] == "set_rgb_dim_level" + assert hmip_device.mock_calls[-1][1] == (2, RGBColorState.RED, 1.0) + + color_list = { + RGBColorState.WHITE: [0.0, 0.0], + RGBColorState.RED: [0.0, 100.0], + RGBColorState.YELLOW: [60.0, 100.0], + RGBColorState.GREEN: [120.0, 100.0], + RGBColorState.TURQUOISE: [180.0, 100.0], + RGBColorState.BLUE: [240.0, 100.0], + RGBColorState.PURPLE: [300.0, 100.0], + } + + for color, hs_color in color_list.items(): + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": entity_id, "hs_color": hs_color}, + blocking=True, + ) + assert hmip_device.mock_calls[-1][0] == "set_rgb_dim_level" + assert hmip_device.mock_calls[-1][1] == (2, color, 0.0392156862745098) + + assert len(hmip_device.mock_calls) == service_call_counter + 8 + + assert hmip_device.mock_calls[-1][0] == "set_rgb_dim_level" + assert hmip_device.mock_calls[-1][1] == ( + 2, + RGBColorState.PURPLE, + 0.0392156862745098, + ) + await async_manipulate_test_data(hass, hmip_device, "dimLevel", 1, 2) + await async_manipulate_test_data( + hass, hmip_device, "simpleRGBColorState", RGBColorState.PURPLE, 2 + ) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + assert ha_state.attributes[ATTR_COLOR_NAME] == RGBColorState.PURPLE + assert ha_state.attributes[ATTR_BRIGHTNESS] == 255 + + await hass.services.async_call( + "light", "turn_off", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 11 + assert hmip_device.mock_calls[-1][0] == "set_rgb_dim_level" + assert hmip_device.mock_calls[-1][1] == (2, RGBColorState.PURPLE, 0.0) + await async_manipulate_test_data(hass, hmip_device, "dimLevel", 0, 2) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF + + await async_manipulate_test_data(hass, hmip_device, "dimLevel", None, 2) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF + assert not ha_state.attributes.get(ATTR_BRIGHTNESS) + + +async def test_hmip_dimmer(hass, default_mock_hap): + """Test HomematicipDimmer.""" + entity_id = "light.schlafzimmerlicht" + entity_name = "Schlafzimmerlicht" + device_model = "HmIP-BDT" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_OFF + service_call_counter = len(hmip_device.mock_calls) + + await hass.services.async_call( + "light", "turn_on", {"entity_id": entity_id}, blocking=True + ) + assert hmip_device.mock_calls[-1][0] == "set_dim_level" + assert hmip_device.mock_calls[-1][1] == (1,) + + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": entity_id, "brightness_pct": "100"}, + blocking=True, + ) + assert len(hmip_device.mock_calls) == service_call_counter + 2 + assert hmip_device.mock_calls[-1][0] == "set_dim_level" + assert hmip_device.mock_calls[-1][1] == (1.0,) + await async_manipulate_test_data(hass, hmip_device, "dimLevel", 1) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + assert ha_state.attributes[ATTR_BRIGHTNESS] == 255 + + await hass.services.async_call( + "light", "turn_off", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 4 + assert hmip_device.mock_calls[-1][0] == "set_dim_level" + assert hmip_device.mock_calls[-1][1] == (0,) + await async_manipulate_test_data(hass, hmip_device, "dimLevel", 0) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF + + await async_manipulate_test_data(hass, hmip_device, "dimLevel", None) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF + assert not ha_state.attributes.get(ATTR_BRIGHTNESS) + + +async def test_hmip_light_measuring(hass, default_mock_hap): + """Test HomematicipLightMeasuring.""" + entity_id = "light.flur_oben" + entity_name = "Flur oben" + device_model = "HmIP-BSM" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_OFF + service_call_counter = len(hmip_device.mock_calls) + + await hass.services.async_call( + "light", "turn_on", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 1 + assert hmip_device.mock_calls[-1][0] == "turn_on" + assert hmip_device.mock_calls[-1][1] == () + await async_manipulate_test_data(hass, hmip_device, "on", True) + await async_manipulate_test_data(hass, hmip_device, "currentPowerConsumption", 50) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + assert ha_state.attributes[ATTR_POWER_CONSUMPTION] == 50 + assert ha_state.attributes[ATTR_ENERGY_COUNTER] == 6.33 + + await hass.services.async_call( + "light", "turn_off", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 4 + assert hmip_device.mock_calls[-1][0] == "turn_off" + assert hmip_device.mock_calls[-1][1] == () + await async_manipulate_test_data(hass, hmip_device, "on", False) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF diff --git a/tests/components/homematicip_cloud/test_sensor.py b/tests/components/homematicip_cloud/test_sensor.py new file mode 100644 index 00000000000..8412cd19f4d --- /dev/null +++ b/tests/components/homematicip_cloud/test_sensor.py @@ -0,0 +1,268 @@ +"""Tests for HomematicIP Cloud sensor.""" +from homematicip.base.enums import ValveState + +from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN +from homeassistant.components.homematicip_cloud.sensor import ( + ATTR_LEFT_COUNTER, + ATTR_RIGHT_COUNTER, + ATTR_TEMPERATURE_OFFSET, + ATTR_WIND_DIRECTION, + ATTR_WIND_DIRECTION_VARIATION, +) +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, POWER_WATT, TEMP_CELSIUS +from homeassistant.setup import async_setup_component + +from .helper import async_manipulate_test_data, get_and_check_entity_basics + + +async def test_manually_configured_platform(hass): + """Test that we do not set up an access point.""" + assert ( + await async_setup_component( + hass, SENSOR_DOMAIN, {SENSOR_DOMAIN: {"platform": HMIPC_DOMAIN}} + ) + is True + ) + assert not hass.data.get(HMIPC_DOMAIN) + + +async def test_hmip_accesspoint_status(hass, default_mock_hap): + """Test HomematicipSwitch.""" + entity_id = "sensor.access_point" + entity_name = "Access Point" + device_model = None + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + assert hmip_device + assert ha_state.state == "8.0" + assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "%" + + await async_manipulate_test_data(hass, hmip_device, "dutyCycle", 17.3) + + ha_state = hass.states.get(entity_id) + assert ha_state.state == "17.3" + + +async def test_hmip_heating_thermostat(hass, default_mock_hap): + """Test HomematicipHeatingThermostat.""" + entity_id = "sensor.heizkorperthermostat_heating" + entity_name = "Heizkörperthermostat Heating" + device_model = "HMIP-eTRV" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "0" + assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "%" + await async_manipulate_test_data(hass, hmip_device, "valvePosition", 0.37) + ha_state = hass.states.get(entity_id) + assert ha_state.state == "37" + + await async_manipulate_test_data(hass, hmip_device, "valveState", "nn") + ha_state = hass.states.get(entity_id) + assert ha_state.state == "nn" + + await async_manipulate_test_data( + hass, hmip_device, "valveState", ValveState.ADAPTION_DONE + ) + ha_state = hass.states.get(entity_id) + assert ha_state.state == "37" + + await async_manipulate_test_data(hass, hmip_device, "lowBat", True) + ha_state = hass.states.get(entity_id) + assert ha_state.attributes["icon"] == "mdi:battery-outline" + + +async def test_hmip_humidity_sensor(hass, default_mock_hap): + """Test HomematicipHumiditySensor.""" + entity_id = "sensor.bwth_1_humidity" + entity_name = "BWTH 1 Humidity" + device_model = "HmIP-BWTH" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "40" + assert ha_state.attributes["unit_of_measurement"] == "%" + await async_manipulate_test_data(hass, hmip_device, "humidity", 45) + ha_state = hass.states.get(entity_id) + assert ha_state.state == "45" + + +async def test_hmip_temperature_sensor1(hass, default_mock_hap): + """Test HomematicipTemperatureSensor.""" + entity_id = "sensor.bwth_1_temperature" + entity_name = "BWTH 1 Temperature" + device_model = "HmIP-BWTH" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "21.0" + assert ha_state.attributes["unit_of_measurement"] == TEMP_CELSIUS + await async_manipulate_test_data(hass, hmip_device, "actualTemperature", 23.5) + ha_state = hass.states.get(entity_id) + assert ha_state.state == "23.5" + + assert not ha_state.attributes.get("temperature_offset") + await async_manipulate_test_data(hass, hmip_device, "temperatureOffset", 10) + ha_state = hass.states.get(entity_id) + assert ha_state.attributes[ATTR_TEMPERATURE_OFFSET] == 10 + + +async def test_hmip_temperature_sensor2(hass, default_mock_hap): + """Test HomematicipTemperatureSensor.""" + entity_id = "sensor.heizkorperthermostat_temperature" + entity_name = "Heizkörperthermostat Temperature" + device_model = "HMIP-eTRV" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "20.0" + assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + await async_manipulate_test_data(hass, hmip_device, "valveActualTemperature", 23.5) + ha_state = hass.states.get(entity_id) + assert ha_state.state == "23.5" + + assert not ha_state.attributes.get(ATTR_TEMPERATURE_OFFSET) + await async_manipulate_test_data(hass, hmip_device, "temperatureOffset", 10) + ha_state = hass.states.get(entity_id) + assert ha_state.attributes[ATTR_TEMPERATURE_OFFSET] == 10 + + +async def test_hmip_power_sensor(hass, default_mock_hap): + """Test HomematicipPowerSensor.""" + entity_id = "sensor.flur_oben_power" + entity_name = "Flur oben Power" + device_model = "HmIP-BSM" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "0.0" + assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == POWER_WATT + await async_manipulate_test_data(hass, hmip_device, "currentPowerConsumption", 23.5) + ha_state = hass.states.get(entity_id) + assert ha_state.state == "23.5" + + +async def test_hmip_illuminance_sensor1(hass, default_mock_hap): + """Test HomematicipIlluminanceSensor.""" + entity_id = "sensor.wettersensor_illuminance" + entity_name = "Wettersensor Illuminance" + device_model = "HmIP-SWO-B" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "4890.0" + assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "lx" + await async_manipulate_test_data(hass, hmip_device, "illumination", 231) + ha_state = hass.states.get(entity_id) + assert ha_state.state == "231" + + +async def test_hmip_illuminance_sensor2(hass, default_mock_hap): + """Test HomematicipIlluminanceSensor.""" + entity_id = "sensor.lichtsensor_nord_illuminance" + entity_name = "Lichtsensor Nord Illuminance" + device_model = "HmIP-SLO" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "807.3" + assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "lx" + await async_manipulate_test_data(hass, hmip_device, "averageIllumination", 231) + ha_state = hass.states.get(entity_id) + assert ha_state.state == "231" + + +async def test_hmip_windspeed_sensor(hass, default_mock_hap): + """Test HomematicipWindspeedSensor.""" + entity_id = "sensor.wettersensor_pro_windspeed" + entity_name = "Wettersensor - pro Windspeed" + device_model = "HmIP-SWO-PR" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "2.6" + assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "km/h" + await async_manipulate_test_data(hass, hmip_device, "windSpeed", 9.4) + ha_state = hass.states.get(entity_id) + assert ha_state.state == "9.4" + + assert ha_state.attributes[ATTR_WIND_DIRECTION_VARIATION] == 56.25 + assert ha_state.attributes[ATTR_WIND_DIRECTION] == "WNW" + + wind_directions = { + 25: "NNE", + 37.5: "NE", + 70: "ENE", + 92.5: "E", + 115: "ESE", + 137.5: "SE", + 160: "SSE", + 182.5: "S", + 205: "SSW", + 227.5: "SW", + 250: "WSW", + 272.5: "W", + 295: "WNW", + 317.5: "NW", + 340: "NNW", + 0: "N", + } + + for direction, txt in wind_directions.items(): + await async_manipulate_test_data(hass, hmip_device, "windDirection", direction) + ha_state = hass.states.get(entity_id) + assert ha_state.attributes[ATTR_WIND_DIRECTION] == txt + + +async def test_hmip_today_rain_sensor(hass, default_mock_hap): + """Test HomematicipTodayRainSensor.""" + entity_id = "sensor.weather_sensor_plus_today_rain" + entity_name = "Weather Sensor – plus Today Rain" + device_model = "HmIP-SWO-PL" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "3.9" + assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "mm" + await async_manipulate_test_data(hass, hmip_device, "todayRainCounter", 14.2) + ha_state = hass.states.get(entity_id) + assert ha_state.state == "14.2" + + +async def test_hmip_passage_detector_delta_counter(hass, default_mock_hap): + """Test HomematicipPassageDetectorDeltaCounter.""" + entity_id = "sensor.spdr_1" + entity_name = "SPDR_1" + device_model = "HmIP-SPDR" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "164" + assert ha_state.attributes[ATTR_LEFT_COUNTER] == 966 + assert ha_state.attributes[ATTR_RIGHT_COUNTER] == 802 + await async_manipulate_test_data(hass, hmip_device, "leftRightCounterDelta", 190) + ha_state = hass.states.get(entity_id) + assert ha_state.state == "190" diff --git a/tests/components/homematicip_cloud/test_switch.py b/tests/components/homematicip_cloud/test_switch.py new file mode 100644 index 00000000000..9e33d1d9587 --- /dev/null +++ b/tests/components/homematicip_cloud/test_switch.py @@ -0,0 +1,173 @@ +"""Tests for HomematicIP Cloud switch.""" +from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN +from homeassistant.components.homematicip_cloud.device import ( + ATTR_GROUP_MEMBER_UNREACHABLE, +) +from homeassistant.components.switch import ( + ATTR_CURRENT_POWER_W, + ATTR_TODAY_ENERGY_KWH, + DOMAIN as SWITCH_DOMAIN, +) +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.setup import async_setup_component + +from .helper import async_manipulate_test_data, get_and_check_entity_basics + + +async def test_manually_configured_platform(hass): + """Test that we do not set up an access point.""" + assert ( + await async_setup_component( + hass, SWITCH_DOMAIN, {SWITCH_DOMAIN: {"platform": HMIPC_DOMAIN}} + ) + is True + ) + assert not hass.data.get(HMIPC_DOMAIN) + + +async def test_hmip_switch(hass, default_mock_hap): + """Test HomematicipSwitch.""" + entity_id = "switch.schrank" + entity_name = "Schrank" + device_model = "HMIP-PS" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_ON + service_call_counter = len(hmip_device.mock_calls) + + await hass.services.async_call( + "switch", "turn_off", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 1 + assert hmip_device.mock_calls[-1][0] == "turn_off" + assert hmip_device.mock_calls[-1][1] == () + await async_manipulate_test_data(hass, hmip_device, "on", False) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF + + await hass.services.async_call( + "switch", "turn_on", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 3 + assert hmip_device.mock_calls[-1][0] == "turn_on" + assert hmip_device.mock_calls[-1][1] == () + await async_manipulate_test_data(hass, hmip_device, "on", True) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + + +async def test_hmip_switch_measuring(hass, default_mock_hap): + """Test HomematicipSwitchMeasuring.""" + entity_id = "switch.pc" + entity_name = "Pc" + device_model = "HMIP-PSM" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_ON + service_call_counter = len(hmip_device.mock_calls) + + await hass.services.async_call( + "switch", "turn_off", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 1 + assert hmip_device.mock_calls[-1][0] == "turn_off" + assert hmip_device.mock_calls[-1][1] == () + await async_manipulate_test_data(hass, hmip_device, "on", False) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF + + await hass.services.async_call( + "switch", "turn_on", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 3 + assert hmip_device.mock_calls[-1][0] == "turn_on" + assert hmip_device.mock_calls[-1][1] == () + await async_manipulate_test_data(hass, hmip_device, "on", True) + await async_manipulate_test_data(hass, hmip_device, "currentPowerConsumption", 50) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + assert ha_state.attributes[ATTR_CURRENT_POWER_W] == 50 + assert ha_state.attributes[ATTR_TODAY_ENERGY_KWH] == 36 + + await async_manipulate_test_data(hass, hmip_device, "energyCounter", None) + ha_state = hass.states.get(entity_id) + assert not ha_state.attributes.get(ATTR_TODAY_ENERGY_KWH) + + +async def test_hmip_group_switch(hass, default_mock_hap): + """Test HomematicipGroupSwitch.""" + entity_id = "switch.strom_group" + entity_name = "Strom Group" + device_model = None + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_ON + service_call_counter = len(hmip_device.mock_calls) + + await hass.services.async_call( + "switch", "turn_off", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 1 + assert hmip_device.mock_calls[-1][0] == "turn_off" + assert hmip_device.mock_calls[-1][1] == () + await async_manipulate_test_data(hass, hmip_device, "on", False) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF + + await hass.services.async_call( + "switch", "turn_on", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 3 + assert hmip_device.mock_calls[-1][0] == "turn_on" + assert hmip_device.mock_calls[-1][1] == () + await async_manipulate_test_data(hass, hmip_device, "on", True) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + + assert not ha_state.attributes.get(ATTR_GROUP_MEMBER_UNREACHABLE) + await async_manipulate_test_data(hass, hmip_device, "unreach", True) + ha_state = hass.states.get(entity_id) + assert ha_state.attributes[ATTR_GROUP_MEMBER_UNREACHABLE] is True + + +async def test_hmip_multi_switch(hass, default_mock_hap): + """Test HomematicipMultiSwitch.""" + entity_id = "switch.jalousien_1_kizi_2_schlazi_channel1" + entity_name = "Jalousien - 1 KiZi, 2 SchlaZi Channel1" + device_model = "HmIP-PCBS2" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_OFF + service_call_counter = len(hmip_device.mock_calls) + + await hass.services.async_call( + "switch", "turn_on", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 1 + assert hmip_device.mock_calls[-1][0] == "turn_on" + assert hmip_device.mock_calls[-1][1] == (1,) + await async_manipulate_test_data(hass, hmip_device, "on", True) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + + await hass.services.async_call( + "switch", "turn_off", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 3 + assert hmip_device.mock_calls[-1][0] == "turn_off" + assert hmip_device.mock_calls[-1][1] == (1,) + await async_manipulate_test_data(hass, hmip_device, "on", False) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF diff --git a/tests/components/homematicip_cloud/test_weather.py b/tests/components/homematicip_cloud/test_weather.py new file mode 100644 index 00000000000..9427a2d05bf --- /dev/null +++ b/tests/components/homematicip_cloud/test_weather.py @@ -0,0 +1,96 @@ +"""Tests for HomematicIP Cloud weather.""" +from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN +from homeassistant.components.weather import ( + ATTR_WEATHER_ATTRIBUTION, + ATTR_WEATHER_HUMIDITY, + ATTR_WEATHER_TEMPERATURE, + ATTR_WEATHER_WIND_BEARING, + ATTR_WEATHER_WIND_SPEED, + DOMAIN as WEATHER_DOMAIN, +) +from homeassistant.setup import async_setup_component + +from .helper import async_manipulate_test_data, get_and_check_entity_basics + + +async def test_manually_configured_platform(hass): + """Test that we do not set up an access point.""" + assert ( + await async_setup_component( + hass, WEATHER_DOMAIN, {WEATHER_DOMAIN: {"platform": HMIPC_DOMAIN}} + ) + is True + ) + assert not hass.data.get(HMIPC_DOMAIN) + + +async def test_hmip_weather_sensor(hass, default_mock_hap): + """Test HomematicipWeatherSensor.""" + entity_id = "weather.weather_sensor_plus" + entity_name = "Weather Sensor – plus" + device_model = "HmIP-SWO-PL" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "" + assert ha_state.attributes[ATTR_WEATHER_TEMPERATURE] == 4.3 + assert ha_state.attributes[ATTR_WEATHER_HUMIDITY] == 97 + assert ha_state.attributes[ATTR_WEATHER_WIND_SPEED] == 15.0 + assert ha_state.attributes[ATTR_WEATHER_ATTRIBUTION] == "Powered by Homematic IP" + + await async_manipulate_test_data(hass, hmip_device, "actualTemperature", 12.1) + ha_state = hass.states.get(entity_id) + assert ha_state.attributes[ATTR_WEATHER_TEMPERATURE] == 12.1 + + +async def test_hmip_weather_sensor_pro(hass, default_mock_hap): + """Test HomematicipWeatherSensorPro.""" + entity_id = "weather.wettersensor_pro" + entity_name = "Wettersensor - pro" + device_model = "HmIP-SWO-PR" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "sunny" + assert ha_state.attributes[ATTR_WEATHER_TEMPERATURE] == 15.4 + assert ha_state.attributes[ATTR_WEATHER_HUMIDITY] == 65 + assert ha_state.attributes[ATTR_WEATHER_WIND_SPEED] == 2.6 + assert ha_state.attributes[ATTR_WEATHER_WIND_BEARING] == 295.0 + assert ha_state.attributes[ATTR_WEATHER_ATTRIBUTION] == "Powered by Homematic IP" + + await async_manipulate_test_data(hass, hmip_device, "actualTemperature", 12.1) + ha_state = hass.states.get(entity_id) + assert ha_state.attributes[ATTR_WEATHER_TEMPERATURE] == 12.1 + + +async def test_hmip_home_weather(hass, default_mock_hap): + """Test HomematicipHomeWeather.""" + entity_id = "weather.weather_1010_wien_osterreich" + entity_name = "Weather 1010 Wien, Österreich" + device_model = None + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + assert hmip_device + assert ha_state.state == "partlycloudy" + assert ha_state.attributes[ATTR_WEATHER_TEMPERATURE] == 16.6 + assert ha_state.attributes[ATTR_WEATHER_HUMIDITY] == 54 + assert ha_state.attributes[ATTR_WEATHER_WIND_SPEED] == 8.6 + assert ha_state.attributes[ATTR_WEATHER_WIND_BEARING] == 294 + assert ha_state.attributes[ATTR_WEATHER_ATTRIBUTION] == "Powered by Homematic IP" + + await async_manipulate_test_data( + hass, + default_mock_hap.home.weather, + "temperature", + 28.3, + fire_device=default_mock_hap.home, + ) + + ha_state = hass.states.get(entity_id) + assert ha_state.attributes[ATTR_WEATHER_TEMPERATURE] == 28.3 diff --git a/tests/components/html5/test_notify.py b/tests/components/html5/test_notify.py index d9246e685dc..481d7a010c9 100644 --- a/tests/components/html5/test_notify.py +++ b/tests/components/html5/test_notify.py @@ -87,7 +87,7 @@ class TestHtml5Notify: assert service is not None - @patch("pywebpush.WebPusher") + @patch("homeassistant.components.html5.notify.WebPusher") def test_dismissing_message(self, mock_wp): """Test dismissing message.""" hass = MagicMock() @@ -115,7 +115,7 @@ class TestHtml5Notify: assert payload["dismiss"] is True assert payload["tag"] == "test" - @patch("pywebpush.WebPusher") + @patch("homeassistant.components.html5.notify.WebPusher") def test_sending_message(self, mock_wp): """Test sending message.""" hass = MagicMock() @@ -145,7 +145,7 @@ class TestHtml5Notify: assert payload["body"] == "Hello" assert payload["icon"] == "beer.png" - @patch("pywebpush.WebPusher") + @patch("homeassistant.components.html5.notify.WebPusher") def test_gcm_key_include(self, mock_wp): """Test if the gcm_key is only included for GCM endpoints.""" hass = MagicMock() @@ -176,7 +176,7 @@ class TestHtml5Notify: assert mock_wp.mock_calls[1][2]["gcm_key"] is not None assert mock_wp.mock_calls[4][2]["gcm_key"] is None - @patch("pywebpush.WebPusher") + @patch("homeassistant.components.html5.notify.WebPusher") def test_fcm_key_include(self, mock_wp): """Test if the FCM header is included.""" hass = MagicMock() @@ -201,7 +201,7 @@ class TestHtml5Notify: # Get the keys passed to the WebPusher's send method assert mock_wp.mock_calls[1][2]["headers"]["Authorization"] is not None - @patch("pywebpush.WebPusher") + @patch("homeassistant.components.html5.notify.WebPusher") def test_fcm_send_with_unknown_priority(self, mock_wp): """Test if the gcm_key is only included for GCM endpoints.""" hass = MagicMock() @@ -226,7 +226,7 @@ class TestHtml5Notify: # Get the keys passed to the WebPusher's send method assert mock_wp.mock_calls[1][2]["headers"]["priority"] == "normal" - @patch("pywebpush.WebPusher") + @patch("homeassistant.components.html5.notify.WebPusher") def test_fcm_no_targets(self, mock_wp): """Test if the gcm_key is only included for GCM endpoints.""" hass = MagicMock() @@ -251,7 +251,7 @@ class TestHtml5Notify: # Get the keys passed to the WebPusher's send method assert mock_wp.mock_calls[1][2]["headers"]["priority"] == "normal" - @patch("pywebpush.WebPusher") + @patch("homeassistant.components.html5.notify.WebPusher") def test_fcm_additional_data(self, mock_wp): """Test if the gcm_key is only included for GCM endpoints.""" hass = MagicMock() @@ -475,7 +475,7 @@ async def test_callback_view_with_jwt(hass, hass_client): registrations = {"device": SUBSCRIPTION_1} client = await mock_client(hass, hass_client, registrations) - with patch("pywebpush.WebPusher") as mock_wp: + with patch("homeassistant.components.html5.notify.WebPusher") as mock_wp: await hass.services.async_call( "notify", "notify", @@ -511,7 +511,7 @@ async def test_send_fcm_without_targets(hass, hass_client): """Test that the notification is send with FCM without targets.""" registrations = {"device": SUBSCRIPTION_5} await mock_client(hass, hass_client, registrations) - with patch("pywebpush.WebPusher") as mock_wp: + with patch("homeassistant.components.html5.notify.WebPusher") as mock_wp: await hass.services.async_call( "notify", "notify", diff --git a/tests/components/http/__init__.py b/tests/components/http/__init__.py index c4f73fd15a6..db5e1ea5c7a 100644 --- a/tests/components/http/__init__.py +++ b/tests/components/http/__init__.py @@ -6,6 +6,10 @@ 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" + + def mock_real_ip(app): """Inject middleware to mock real IP. diff --git a/tests/components/http/test_auth.py b/tests/components/http/test_auth.py index 842201beace..499ceab1556 100644 --- a/tests/components/http/test_auth.py +++ b/tests/components/http/test_auth.py @@ -11,10 +11,8 @@ from homeassistant.auth.providers import trusted_networks from homeassistant.components.http.auth import setup_auth, async_sign_path from homeassistant.components.http.const import KEY_AUTHENTICATED from homeassistant.components.http.real_ip import setup_real_ip -from homeassistant.const import HTTP_HEADER_HA_AUTH from homeassistant.setup import async_setup_component -from . import mock_real_ip - +from . import mock_real_ip, HTTP_HEADER_HA_AUTH API_PASSWORD = "test-password" @@ -87,29 +85,29 @@ async def test_auth_middleware_loaded_by_default(hass): assert len(mock_setup.mock_calls) == 1 -async def test_access_with_password_in_header(app, aiohttp_client, legacy_auth, hass): +async def test_cant_access_with_password_in_header( + app, aiohttp_client, legacy_auth, hass +): """Test access with password in header.""" setup_auth(hass, app) client = await aiohttp_client(app) - user = await get_legacy_user(hass.auth) req = await client.get("/", headers={HTTP_HEADER_HA_AUTH: API_PASSWORD}) - assert req.status == 200 - assert await req.json() == {"user_id": user.id} + assert req.status == 401 req = await client.get("/", headers={HTTP_HEADER_HA_AUTH: "wrong-pass"}) assert req.status == 401 -async def test_access_with_password_in_query(app, aiohttp_client, legacy_auth, hass): +async def test_cant_access_with_password_in_query( + app, aiohttp_client, legacy_auth, hass +): """Test access with password in URL.""" setup_auth(hass, app) client = await aiohttp_client(app) - user = await get_legacy_user(hass.auth) resp = await client.get("/", params={"api_password": API_PASSWORD}) - assert resp.status == 200 - assert await resp.json() == {"user_id": user.id} + assert resp.status == 401 resp = await client.get("/") assert resp.status == 401 @@ -118,15 +116,13 @@ async def test_access_with_password_in_query(app, aiohttp_client, legacy_auth, h assert resp.status == 401 -async def test_basic_auth_works(app, aiohttp_client, hass, legacy_auth): +async def test_basic_auth_does_not_work(app, aiohttp_client, hass, legacy_auth): """Test access with basic authentication.""" setup_auth(hass, app) client = await aiohttp_client(app) - user = await get_legacy_user(hass.auth) req = await client.get("/", auth=BasicAuth("homeassistant", API_PASSWORD)) - assert req.status == 200 - assert await req.json() == {"user_id": user.id} + assert req.status == 401 req = await client.get("/", auth=BasicAuth("wrong_username", API_PASSWORD)) assert req.status == 401 @@ -138,7 +134,7 @@ async def test_basic_auth_works(app, aiohttp_client, hass, legacy_auth): assert req.status == 401 -async def test_access_with_trusted_ip( +async def test_cannot_access_with_trusted_ip( hass, app2, trusted_networks_auth, aiohttp_client, hass_owner_user ): """Test access with an untrusted ip address.""" @@ -155,8 +151,7 @@ async def test_access_with_trusted_ip( for remote_addr in TRUSTED_ADDRESSES: set_mock_ip(remote_addr) resp = await client.get("/") - assert resp.status == 200, "{} should be trusted".format(remote_addr) - assert await resp.json() == {"user_id": hass_owner_user.id} + assert resp.status == 401, "{} shouldn't be trusted".format(remote_addr) async def test_auth_active_access_with_access_token_in_header( @@ -209,29 +204,24 @@ async def test_auth_active_access_with_trusted_ip( for remote_addr in TRUSTED_ADDRESSES: set_mock_ip(remote_addr) resp = await client.get("/") - assert resp.status == 200, "{} should be trusted".format(remote_addr) - assert await resp.json() == {"user_id": hass_owner_user.id} + assert resp.status == 401, "{} shouldn't be trusted".format(remote_addr) -async def test_auth_legacy_support_api_password_access( +async def test_auth_legacy_support_api_password_cannot_access( app, aiohttp_client, legacy_auth, hass ): """Test access using api_password if auth.support_legacy.""" setup_auth(hass, app) client = await aiohttp_client(app) - user = await get_legacy_user(hass.auth) req = await client.get("/", headers={HTTP_HEADER_HA_AUTH: API_PASSWORD}) - assert req.status == 200 - assert await req.json() == {"user_id": user.id} + assert req.status == 401 resp = await client.get("/", params={"api_password": API_PASSWORD}) - assert resp.status == 200 - assert await resp.json() == {"user_id": user.id} + assert resp.status == 401 req = await client.get("/", auth=BasicAuth("homeassistant", API_PASSWORD)) - assert req.status == 200 - assert await req.json() == {"user_id": user.id} + assert req.status == 401 async def test_auth_access_signed_path(hass, app, aiohttp_client, hass_access_token): diff --git a/tests/components/http/test_ban.py b/tests/components/http/test_ban.py index fc31de4b950..f50afcef8a8 100644 --- a/tests/components/http/test_ban.py +++ b/tests/components/http/test_ban.py @@ -148,6 +148,8 @@ async def test_failed_login_attempts_counter(hass, aiohttp_client): assert resp.status == 200 assert app[KEY_FAILED_LOGIN_ATTEMPTS][remote_ip] == 2 + # This used to check that with trusted networks we reset login attempts + # We no longer support trusted networks. resp = await client.get("/auth_true") assert resp.status == 200 - assert remote_ip not in app[KEY_FAILED_LOGIN_ATTEMPTS] + assert app[KEY_FAILED_LOGIN_ATTEMPTS][remote_ip] == 2 diff --git a/tests/components/http/test_cors.py b/tests/components/http/test_cors.py index 99b9e0b6e9a..1cea900d971 100644 --- a/tests/components/http/test_cors.py +++ b/tests/components/http/test_cors.py @@ -13,11 +13,12 @@ from aiohttp.hdrs import ( ) import pytest -from homeassistant.const import HTTP_HEADER_HA_AUTH from homeassistant.setup import async_setup_component from homeassistant.components.http.cors import setup_cors from homeassistant.components.http.view import HomeAssistantView +from . import HTTP_HEADER_HA_AUTH + TRUSTED_ORIGIN = "https://home-assistant.io" @@ -91,13 +92,13 @@ async def test_cors_preflight_allowed(client): headers={ ORIGIN: TRUSTED_ORIGIN, ACCESS_CONTROL_REQUEST_METHOD: "GET", - ACCESS_CONTROL_REQUEST_HEADERS: "x-ha-access", + ACCESS_CONTROL_REQUEST_HEADERS: "x-requested-with", }, ) assert req.status == 200 assert req.headers[ACCESS_CONTROL_ALLOW_ORIGIN] == TRUSTED_ORIGIN - assert req.headers[ACCESS_CONTROL_ALLOW_HEADERS] == HTTP_HEADER_HA_AUTH.upper() + assert req.headers[ACCESS_CONTROL_ALLOW_HEADERS] == "X-REQUESTED-WITH" async def test_cors_middleware_with_cors_allowed_view(hass): diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index d8e613df6df..ad8e3ac10fd 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -133,7 +133,7 @@ async def test_not_log_password(hass, aiohttp_client, caplog, legacy_auth): resp = await client.get("/api/", params={"api_password": "test-password"}) - assert resp.status == 200 + assert resp.status == 401 logs = caplog.text # Ensure we don't log API passwords diff --git a/tests/components/hydroquebec/__init__.py b/tests/components/hydroquebec/__init__.py deleted file mode 100644 index 1342395d265..00000000000 --- a/tests/components/hydroquebec/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the hydroquebec component.""" diff --git a/tests/components/hydroquebec/test_sensor.py b/tests/components/hydroquebec/test_sensor.py deleted file mode 100644 index 9b2dd5ab5b5..00000000000 --- a/tests/components/hydroquebec/test_sensor.py +++ /dev/null @@ -1,102 +0,0 @@ -"""The test for the hydroquebec sensor platform.""" -import asyncio -import logging -import sys -from unittest.mock import MagicMock - -from homeassistant.bootstrap import async_setup_component -from homeassistant.components.hydroquebec import sensor as hydroquebec -from tests.common import assert_setup_component - - -CONTRACT = "123456789" - - -class HydroQuebecClientMock: - """Fake Hydroquebec client.""" - - def __init__(self, username, password, contract=None, httpsession=None): - """Fake Hydroquebec client init.""" - pass - - def get_data(self, contract): - """Return fake hydroquebec data.""" - return {CONTRACT: {"balance": 160.12}} - - def get_contracts(self): - """Return fake hydroquebec contracts.""" - return [CONTRACT] - - @asyncio.coroutine - def fetch_data(self): - """Return fake fetching data.""" - pass - - -class HydroQuebecClientMockError(HydroQuebecClientMock): - """Fake Hydroquebec client error.""" - - def get_contracts(self): - """Return fake hydroquebec contracts.""" - return [] - - @asyncio.coroutine - def fetch_data(self): - """Return fake fetching data.""" - raise PyHydroQuebecErrorMock("Fake Error") - - -class PyHydroQuebecErrorMock(BaseException): - """Fake PyHydroquebec Error.""" - - -class PyHydroQuebecClientFakeModule: - """Fake pyfido.client module.""" - - PyHydroQuebecError = PyHydroQuebecErrorMock - - -class PyHydroQuebecFakeModule: - """Fake pyfido module.""" - - HydroQuebecClient = HydroQuebecClientMockError - - -@asyncio.coroutine -def test_hydroquebec_sensor(loop, hass): - """Test the Hydroquebec number sensor.""" - sys.modules["pyhydroquebec"] = MagicMock() - sys.modules["pyhydroquebec.client"] = MagicMock() - sys.modules["pyhydroquebec.client.PyHydroQuebecError"] = PyHydroQuebecErrorMock - import pyhydroquebec.client - - pyhydroquebec.HydroQuebecClient = HydroQuebecClientMock - pyhydroquebec.client.PyHydroQuebecError = PyHydroQuebecErrorMock - config = { - "sensor": { - "platform": "hydroquebec", - "name": "hydro", - "contract": CONTRACT, - "username": "myusername", - "password": "password", - "monitored_variables": ["balance"], - } - } - with assert_setup_component(1): - yield from async_setup_component(hass, "sensor", config) - state = hass.states.get("sensor.hydro_balance") - assert state.state == "160.12" - assert state.attributes.get("unit_of_measurement") == "CAD" - - -@asyncio.coroutine -def test_error(hass, caplog): - """Test the Hydroquebec sensor errors.""" - caplog.set_level(logging.ERROR) - sys.modules["pyhydroquebec"] = PyHydroQuebecFakeModule() - sys.modules["pyhydroquebec.client"] = PyHydroQuebecClientFakeModule() - - config = {} - fake_async_add_entities = MagicMock() - yield from hydroquebec.async_setup_platform(hass, config, fake_async_add_entities) - assert fake_async_add_entities.called is False diff --git a/tests/components/input_datetime/test_reproduce_state.py b/tests/components/input_datetime/test_reproduce_state.py new file mode 100644 index 00000000000..71f0658923c --- /dev/null +++ b/tests/components/input_datetime/test_reproduce_state.py @@ -0,0 +1,69 @@ +"""Test reproduce state for Input datetime.""" +from homeassistant.core import State + +from tests.common import async_mock_service + + +async def test_reproducing_states(hass, caplog): + """Test reproducing Input datetime states.""" + hass.states.async_set( + "input_datetime.entity_datetime", + "2010-10-10 01:20:00", + {"has_date": True, "has_time": True}, + ) + hass.states.async_set( + "input_datetime.entity_time", "01:20:00", {"has_date": False, "has_time": True} + ) + hass.states.async_set( + "input_datetime.entity_date", + "2010-10-10", + {"has_date": True, "has_time": False}, + ) + + datetime_calls = async_mock_service(hass, "input_datetime", "set_datetime") + + # These calls should do nothing as entities already in desired state + await hass.helpers.state.async_reproduce_state( + [ + State("input_datetime.entity_datetime", "2010-10-10 01:20:00"), + State("input_datetime.entity_time", "01:20:00"), + State("input_datetime.entity_date", "2010-10-10"), + ], + blocking=True, + ) + + assert len(datetime_calls) == 0 + + # Test invalid state is handled + await hass.helpers.state.async_reproduce_state( + [State("input_datetime.entity_datetime", "not_supported")], blocking=True + ) + + assert "not_supported" in caplog.text + assert len(datetime_calls) == 0 + + # Make sure correct services are called + await hass.helpers.state.async_reproduce_state( + [ + State("input_datetime.entity_datetime", "2011-10-10 02:20:00"), + State("input_datetime.entity_time", "02:20:00"), + State("input_datetime.entity_date", "2011-10-10"), + # Should not raise + State("input_datetime.non_existing", "2010-10-10 01:20:00"), + ], + blocking=True, + ) + + valid_calls = [ + { + "entity_id": "input_datetime.entity_datetime", + "datetime": "2011-10-10 02:20:00", + }, + {"entity_id": "input_datetime.entity_time", "time": "02:20:00"}, + {"entity_id": "input_datetime.entity_date", "date": "2011-10-10"}, + ] + assert len(datetime_calls) == 3 + for call in datetime_calls: + assert call.domain == "input_datetime" + assert call.data in valid_calls + valid_calls.remove(call.data) diff --git a/tests/components/input_number/test_reproduce_state.py b/tests/components/input_number/test_reproduce_state.py new file mode 100644 index 00000000000..37ab83f3204 --- /dev/null +++ b/tests/components/input_number/test_reproduce_state.py @@ -0,0 +1,62 @@ +"""Test reproduce state for Input number.""" +from homeassistant.core import State +from homeassistant.setup import async_setup_component + +VALID_NUMBER1 = "19.0" +VALID_NUMBER2 = "99.9" + + +async def test_reproducing_states(hass, caplog): + """Test reproducing Input number states.""" + + assert await async_setup_component( + hass, + "input_number", + { + "input_number": { + "test_number": {"min": "5", "max": "100", "initial": VALID_NUMBER1} + } + }, + ) + + # These calls should do nothing as entities already in desired state + await hass.helpers.state.async_reproduce_state( + [ + State("input_number.test_number", VALID_NUMBER1), + # Should not raise + State("input_number.non_existing", "234"), + ], + blocking=True, + ) + + assert hass.states.get("input_number.test_number").state == VALID_NUMBER1 + + # Test reproducing with different state + await hass.helpers.state.async_reproduce_state( + [ + State("input_number.test_number", VALID_NUMBER2), + # Should not raise + State("input_number.non_existing", "234"), + ], + blocking=True, + ) + + assert hass.states.get("input_number.test_number").state == VALID_NUMBER2 + + # Test setting state to number out of range + await hass.helpers.state.async_reproduce_state( + [State("input_number.test_number", "150")], blocking=True + ) + + # The entity states should be unchanged after trying to set them to out-of-range number + assert hass.states.get("input_number.test_number").state == VALID_NUMBER2 + + await hass.helpers.state.async_reproduce_state( + [ + # Test invalid state + State("input_number.test_number", "invalid_state"), + # Set to state it already is. + State("input_number.test_number", VALID_NUMBER2), + ], + blocking=True, + ) diff --git a/tests/components/input_select/test_reproduce_state.py b/tests/components/input_select/test_reproduce_state.py new file mode 100644 index 00000000000..469c258cb4b --- /dev/null +++ b/tests/components/input_select/test_reproduce_state.py @@ -0,0 +1,72 @@ +"""Test reproduce state for Input select.""" +from homeassistant.core import State +from homeassistant.setup import async_setup_component + +VALID_OPTION1 = "Option A" +VALID_OPTION2 = "Option B" +VALID_OPTION3 = "Option C" +VALID_OPTION4 = "Option D" +VALID_OPTION5 = "Option E" +VALID_OPTION6 = "Option F" +INVALID_OPTION = "Option X" +VALID_OPTION_SET1 = [VALID_OPTION1, VALID_OPTION2, VALID_OPTION3] +VALID_OPTION_SET2 = [VALID_OPTION4, VALID_OPTION5, VALID_OPTION6] +ENTITY = "input_select.test_select" + + +async def test_reproducing_states(hass, caplog): + """Test reproducing Input select states.""" + + # Setup entity + assert await async_setup_component( + hass, + "input_select", + { + "input_select": { + "test_select": {"options": VALID_OPTION_SET1, "initial": VALID_OPTION1} + } + }, + ) + + # These calls should do nothing as entities already in desired state + await hass.helpers.state.async_reproduce_state( + [ + State(ENTITY, VALID_OPTION1), + # Should not raise + State("input_select.non_existing", VALID_OPTION1), + ], + blocking=True, + ) + + # Test that entity is in desired state + assert hass.states.get(ENTITY).state == VALID_OPTION1 + + # Try reproducing with different state + await hass.helpers.state.async_reproduce_state( + [ + State(ENTITY, VALID_OPTION3), + # Should not raise + State("input_select.non_existing", VALID_OPTION3), + ], + blocking=True, + ) + + # Test that we got the desired result + assert hass.states.get(ENTITY).state == VALID_OPTION3 + + # Test setting state to invalid state + await hass.helpers.state.async_reproduce_state( + [State(ENTITY, INVALID_OPTION)], blocking=True + ) + + # The entity state should be unchanged + assert hass.states.get(ENTITY).state == VALID_OPTION3 + + # Test setting a different option set + await hass.helpers.state.async_reproduce_state( + [State(ENTITY, VALID_OPTION5, {"options": VALID_OPTION_SET2})], blocking=True + ) + + # These should fail if options weren't changed to VALID_OPTION_SET2 + assert hass.states.get(ENTITY).attributes == {"options": VALID_OPTION_SET2} + assert hass.states.get(ENTITY).state == VALID_OPTION5 diff --git a/tests/components/input_text/test_reproduce_state.py b/tests/components/input_text/test_reproduce_state.py new file mode 100644 index 00000000000..fd75948d461 --- /dev/null +++ b/tests/components/input_text/test_reproduce_state.py @@ -0,0 +1,65 @@ +"""Test reproduce state for Input text.""" +from homeassistant.core import State +from homeassistant.setup import async_setup_component + +VALID_TEXT1 = "Test text" +VALID_TEXT2 = "LoremIpsum" +INVALID_TEXT1 = "This text is too long!" +INVALID_TEXT2 = "Short" + + +async def test_reproducing_states(hass, caplog): + """Test reproducing Input text states.""" + + # Setup entity for testing + assert await async_setup_component( + hass, + "input_text", + { + "input_text": { + "test_text": {"min": "6", "max": "10", "initial": VALID_TEXT1} + } + }, + ) + + # These calls should do nothing as entities already in desired state + await hass.helpers.state.async_reproduce_state( + [ + State("input_text.test_text", VALID_TEXT1), + # Should not raise + State("input_text.non_existing", VALID_TEXT1), + ], + blocking=True, + ) + + # Test that entity is in desired state + assert hass.states.get("input_text.test_text").state == VALID_TEXT1 + + # Try reproducing with different state + await hass.helpers.state.async_reproduce_state( + [ + State("input_text.test_text", VALID_TEXT2), + # Should not raise + State("input_text.non_existing", VALID_TEXT2), + ], + blocking=True, + ) + + # Test that the state was changed + assert hass.states.get("input_text.test_text").state == VALID_TEXT2 + + # Test setting state to invalid state (length too long) + await hass.helpers.state.async_reproduce_state( + [State("input_text.test_text", INVALID_TEXT1)], blocking=True + ) + + # The entity state should be unchanged + assert hass.states.get("input_text.test_text").state == VALID_TEXT2 + + # Test setting state to invalid state (length too short) + await hass.helpers.state.async_reproduce_state( + [State("input_text.test_text", INVALID_TEXT2)], blocking=True + ) + + # The entity state should be unchanged + assert hass.states.get("input_text.test_text").state == VALID_TEXT2 diff --git a/tests/components/integration/test_sensor.py b/tests/components/integration/test_sensor.py index 48d4178e992..c65ca720235 100644 --- a/tests/components/integration/test_sensor.py +++ b/tests/components/integration/test_sensor.py @@ -200,4 +200,4 @@ async def test_suffix(hass): assert state is not None # Testing a network speed sensor at 1000 bytes/s over 10s = 10kbytes - assert round(float(state.state), config["sensor"]["round"]) == 10.0 + assert round(float(state.state)) == 10 diff --git a/tests/components/jewish_calendar/test_sensor.py b/tests/components/jewish_calendar/test_sensor.py index 8d72830b369..07e0b7cb192 100644 --- a/tests/components/jewish_calendar/test_sensor.py +++ b/tests/components/jewish_calendar/test_sensor.py @@ -1,5 +1,5 @@ """The tests for the Jewish calendar sensors.""" -from datetime import time, timedelta +from datetime import timedelta from datetime import datetime as dt import pytest @@ -42,27 +42,17 @@ TEST_PARAMS = [ False, 'כ"ג אלול ה\' תשע"ח', ), - ( - dt(2018, 9, 10), - "UTC", - 31.778, - 35.235, - "hebrew", - "holiday_name", - False, - "א' ראש השנה", - ), + (dt(2018, 9, 10), "UTC", 31.778, 35.235, "hebrew", "holiday", False, "א' ראש השנה"), ( dt(2018, 9, 10), "UTC", 31.778, 35.235, "english", - "holiday_name", + "holiday", False, "Rosh Hashana I", ), - (dt(2018, 9, 10), "UTC", 31.778, 35.235, "english", "holiday_type", False, 1), ( dt(2018, 9, 8), "UTC", @@ -81,7 +71,7 @@ TEST_PARAMS = [ "hebrew", "t_set_hakochavim", True, - time(19, 48), + dt(2018, 9, 8, 19, 48), ), ( dt(2018, 9, 8), @@ -91,7 +81,7 @@ TEST_PARAMS = [ "hebrew", "t_set_hakochavim", False, - time(19, 21), + dt(2018, 9, 8, 19, 21), ), ( dt(2018, 10, 14), @@ -128,9 +118,8 @@ TEST_PARAMS = [ TEST_IDS = [ "date_output", "date_output_hebrew", - "holiday_name", - "holiday_name_english", - "holiday_type", + "holiday", + "holiday_english", "torah_reading", "first_stars_ny", "first_stars_jerusalem", @@ -183,7 +172,16 @@ async def test_jewish_calendar_sensor( async_fire_time_changed(hass, future) await hass.async_block_till_done() - assert hass.states.get(f"sensor.test_{sensor}").state == str(result) + result = ( + dt_util.as_utc(time_zone.localize(result)) if isinstance(result, dt) else result + ) + + sensor_object = hass.states.get(f"sensor.test_{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" SHABBAT_PARAMS = [ @@ -252,8 +250,8 @@ SHABBAT_PARAMS = [ "english_upcoming_shabbat_havdalah": dt(2018, 9, 15, 19, 50), "english_parshat_hashavua": "Vayeilech", "hebrew_parshat_hashavua": "וילך", - "english_holiday_name": "Erev Rosh Hashana", - "hebrew_holiday_name": "ערב ראש השנה", + "english_holiday": "Erev Rosh Hashana", + "hebrew_holiday": "ערב ראש השנה", }, ), make_nyc_test_params( @@ -265,8 +263,8 @@ SHABBAT_PARAMS = [ "english_upcoming_shabbat_havdalah": dt(2018, 9, 15, 19, 50), "english_parshat_hashavua": "Vayeilech", "hebrew_parshat_hashavua": "וילך", - "english_holiday_name": "Rosh Hashana I", - "hebrew_holiday_name": "א' ראש השנה", + "english_holiday": "Rosh Hashana I", + "hebrew_holiday": "א' ראש השנה", }, ), make_nyc_test_params( @@ -278,8 +276,8 @@ SHABBAT_PARAMS = [ "english_upcoming_shabbat_havdalah": dt(2018, 9, 15, 19, 50), "english_parshat_hashavua": "Vayeilech", "hebrew_parshat_hashavua": "וילך", - "english_holiday_name": "Rosh Hashana II", - "hebrew_holiday_name": "ב' ראש השנה", + "english_holiday": "Rosh Hashana II", + "hebrew_holiday": "ב' ראש השנה", }, ), make_nyc_test_params( @@ -302,8 +300,8 @@ SHABBAT_PARAMS = [ "english_upcoming_shabbat_havdalah": dt(2018, 10, 6, 19, 13), "english_parshat_hashavua": "Bereshit", "hebrew_parshat_hashavua": "בראשית", - "english_holiday_name": "Hoshana Raba", - "hebrew_holiday_name": "הושענא רבה", + "english_holiday": "Hoshana Raba", + "hebrew_holiday": "הושענא רבה", }, ), make_nyc_test_params( @@ -315,8 +313,8 @@ SHABBAT_PARAMS = [ "english_upcoming_shabbat_havdalah": dt(2018, 10, 6, 19, 13), "english_parshat_hashavua": "Bereshit", "hebrew_parshat_hashavua": "בראשית", - "english_holiday_name": "Shmini Atzeret", - "hebrew_holiday_name": "שמיני עצרת", + "english_holiday": "Shmini Atzeret", + "hebrew_holiday": "שמיני עצרת", }, ), make_nyc_test_params( @@ -328,8 +326,8 @@ SHABBAT_PARAMS = [ "english_upcoming_shabbat_havdalah": dt(2018, 10, 6, 19, 13), "english_parshat_hashavua": "Bereshit", "hebrew_parshat_hashavua": "בראשית", - "english_holiday_name": "Simchat Torah", - "hebrew_holiday_name": "שמחת תורה", + "english_holiday": "Simchat Torah", + "hebrew_holiday": "שמחת תורה", }, ), make_jerusalem_test_params( @@ -341,8 +339,8 @@ SHABBAT_PARAMS = [ "english_upcoming_shabbat_havdalah": dt(2018, 10, 6, 18, 56), "english_parshat_hashavua": "Bereshit", "hebrew_parshat_hashavua": "בראשית", - "english_holiday_name": "Hoshana Raba", - "hebrew_holiday_name": "הושענא רבה", + "english_holiday": "Hoshana Raba", + "hebrew_holiday": "הושענא רבה", }, ), make_jerusalem_test_params( @@ -354,8 +352,8 @@ SHABBAT_PARAMS = [ "english_upcoming_shabbat_havdalah": dt(2018, 10, 6, 18, 56), "english_parshat_hashavua": "Bereshit", "hebrew_parshat_hashavua": "בראשית", - "english_holiday_name": "Shmini Atzeret", - "hebrew_holiday_name": "שמיני עצרת", + "english_holiday": "Shmini Atzeret", + "hebrew_holiday": "שמיני עצרת", }, ), make_jerusalem_test_params( @@ -378,8 +376,8 @@ SHABBAT_PARAMS = [ "english_upcoming_shabbat_havdalah": "unknown", "english_parshat_hashavua": "Bamidbar", "hebrew_parshat_hashavua": "במדבר", - "english_holiday_name": "Erev Shavuot", - "hebrew_holiday_name": "ערב שבועות", + "english_holiday": "Erev Shavuot", + "hebrew_holiday": "ערב שבועות", }, ), make_nyc_test_params( @@ -391,8 +389,8 @@ SHABBAT_PARAMS = [ "english_upcoming_shabbat_havdalah": dt(2016, 6, 18, 21, 19), "english_parshat_hashavua": "Nasso", "hebrew_parshat_hashavua": "נשא", - "english_holiday_name": "Shavuot", - "hebrew_holiday_name": "שבועות", + "english_holiday": "Shavuot", + "hebrew_holiday": "שבועות", }, ), make_jerusalem_test_params( @@ -404,8 +402,8 @@ SHABBAT_PARAMS = [ "english_upcoming_shabbat_havdalah": dt(2017, 9, 23, 19, 13), "english_parshat_hashavua": "Ha'Azinu", "hebrew_parshat_hashavua": "האזינו", - "english_holiday_name": "Rosh Hashana I", - "hebrew_holiday_name": "א' ראש השנה", + "english_holiday": "Rosh Hashana I", + "hebrew_holiday": "א' ראש השנה", }, ), make_jerusalem_test_params( @@ -417,8 +415,8 @@ SHABBAT_PARAMS = [ "english_upcoming_shabbat_havdalah": dt(2017, 9, 23, 19, 13), "english_parshat_hashavua": "Ha'Azinu", "hebrew_parshat_hashavua": "האזינו", - "english_holiday_name": "Rosh Hashana II", - "hebrew_holiday_name": "ב' ראש השנה", + "english_holiday": "Rosh Hashana II", + "hebrew_holiday": "ב' ראש השנה", }, ), make_jerusalem_test_params( @@ -430,8 +428,8 @@ SHABBAT_PARAMS = [ "english_upcoming_shabbat_havdalah": dt(2017, 9, 23, 19, 13), "english_parshat_hashavua": "Ha'Azinu", "hebrew_parshat_hashavua": "האזינו", - "english_holiday_name": "", - "hebrew_holiday_name": "", + "english_holiday": "", + "hebrew_holiday": "", }, ), ] @@ -524,6 +522,12 @@ async def test_shabbat_times_sensor( sensor_type = sensor_type.replace(f"{language}_", "") + result_value = ( + dt_util.as_utc(result_value) + if isinstance(result_value, dt) + else result_value + ) + assert hass.states.get(f"sensor.test_{sensor_type}").state == str( result_value ), f"Value for {sensor_type}" diff --git a/tests/components/light/test_device_condition.py b/tests/components/light/test_device_condition.py index 8009fbd6337..a9f4adddfab 100644 --- a/tests/components/light/test_device_condition.py +++ b/tests/components/light/test_device_condition.py @@ -1,11 +1,14 @@ """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 homeassistant.components.automation as automation from homeassistant.helpers import device_registry +import homeassistant.util.dt as dt_util from tests.common import ( MockConfigEntry, @@ -13,6 +16,7 @@ from tests.common import ( mock_device_registry, mock_registry, async_get_device_automations, + async_get_device_automation_capabilities, ) @@ -63,6 +67,28 @@ async def test_get_conditions(hass, device_reg, entity_reg): assert conditions == expected_conditions +async def test_get_condition_capabilities(hass, device_reg, entity_reg): + """Test we get the expected capabilities from a light condition.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_capabilities = { + "extra_fields": [ + {"name": "for", "optional": True, "type": "positive_time_period_dict"} + ] + } + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + for condition in conditions: + capabilities = await async_get_device_automation_capabilities( + hass, "condition", condition + ) + assert capabilities == expected_capabilities + + async def test_if_state(hass, calls): """Test for turn_on and turn_off conditions.""" platform = getattr(hass.components, f"test.{DOMAIN}") @@ -134,3 +160,73 @@ async def test_if_state(hass, calls): await hass.async_block_till_done() assert len(calls) == 2 assert calls[1].data["some"] == "is_off event - test_event2" + + +async def test_if_fires_on_for_condition(hass, calls): + """Test for firing if condition is on with delay.""" + point1 = dt_util.utcnow() + point2 = point1 + timedelta(seconds=10) + point3 = point2 + timedelta(seconds=10) + + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + ent1, ent2, ent3 = platform.ENTITIES + + with patch("homeassistant.core.dt_util.utcnow") as mock_utcnow: + mock_utcnow.return_value = point1 + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "is_off", + "for": {"seconds": 5}, + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "is_off {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ("platform", "event.event_type") + ) + }, + }, + } + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_ON + assert len(calls) == 0 + + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 0 + + # Time travel 10 secs into the future + mock_utcnow.return_value = point2 + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 0 + + hass.states.async_set(ent1.entity_id, STATE_OFF) + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 0 + + # Time travel 20 secs into the future + mock_utcnow.return_value = point3 + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "is_off event - test_event1" diff --git a/tests/components/light/test_reproduce_state.py b/tests/components/light/test_reproduce_state.py new file mode 100644 index 00000000000..92790890a4c --- /dev/null +++ b/tests/components/light/test_reproduce_state.py @@ -0,0 +1,117 @@ +"""Test reproduce state for Light.""" +from homeassistant.core import State + +from tests.common import async_mock_service + +VALID_BRIGHTNESS = {"brightness": 180} +VALID_WHITE_VALUE = {"white_value": 200} +VALID_EFFECT = {"effect": "random"} +VALID_COLOR_TEMP = {"color_temp": 240} +VALID_HS_COLOR = {"hs_color": (345, 75)} +VALID_RGB_COLOR = {"rgb_color": (255, 63, 111)} +VALID_XY_COLOR = {"xy_color": (0.59, 0.274)} + + +async def test_reproducing_states(hass, caplog): + """Test reproducing Light states.""" + hass.states.async_set("light.entity_off", "off", {}) + hass.states.async_set("light.entity_bright", "on", VALID_BRIGHTNESS) + hass.states.async_set("light.entity_white", "on", VALID_WHITE_VALUE) + hass.states.async_set("light.entity_effect", "on", VALID_EFFECT) + hass.states.async_set("light.entity_temp", "on", VALID_COLOR_TEMP) + hass.states.async_set("light.entity_hs", "on", VALID_HS_COLOR) + hass.states.async_set("light.entity_rgb", "on", VALID_RGB_COLOR) + hass.states.async_set("light.entity_xy", "on", VALID_XY_COLOR) + + turn_on_calls = async_mock_service(hass, "light", "turn_on") + turn_off_calls = async_mock_service(hass, "light", "turn_off") + + # These calls should do nothing as entities already in desired state + await hass.helpers.state.async_reproduce_state( + [ + State("light.entity_off", "off"), + State("light.entity_bright", "on", VALID_BRIGHTNESS), + State("light.entity_white", "on", VALID_WHITE_VALUE), + State("light.entity_effect", "on", VALID_EFFECT), + State("light.entity_temp", "on", VALID_COLOR_TEMP), + State("light.entity_hs", "on", VALID_HS_COLOR), + State("light.entity_rgb", "on", VALID_RGB_COLOR), + State("light.entity_xy", "on", VALID_XY_COLOR), + ], + blocking=True, + ) + + assert len(turn_on_calls) == 0 + assert len(turn_off_calls) == 0 + + # Test invalid state is handled + await hass.helpers.state.async_reproduce_state( + [State("light.entity_off", "not_supported")], blocking=True + ) + + assert "not_supported" in caplog.text + assert len(turn_on_calls) == 0 + assert len(turn_off_calls) == 0 + + # Make sure correct services are called + await hass.helpers.state.async_reproduce_state( + [ + State("light.entity_xy", "off"), + State("light.entity_off", "on", VALID_BRIGHTNESS), + State("light.entity_bright", "on", VALID_WHITE_VALUE), + State("light.entity_white", "on", VALID_EFFECT), + State("light.entity_effect", "on", VALID_COLOR_TEMP), + State("light.entity_temp", "on", VALID_HS_COLOR), + State("light.entity_hs", "on", VALID_RGB_COLOR), + State("light.entity_rgb", "on", VALID_XY_COLOR), + ], + blocking=True, + ) + + assert len(turn_on_calls) == 7 + + expected_calls = [] + + expected_off = VALID_BRIGHTNESS + expected_off["entity_id"] = "light.entity_off" + expected_calls.append(expected_off) + + expected_bright = VALID_WHITE_VALUE + expected_bright["entity_id"] = "light.entity_bright" + expected_calls.append(expected_bright) + + expected_white = VALID_EFFECT + expected_white["entity_id"] = "light.entity_white" + expected_calls.append(expected_white) + + expected_effect = VALID_COLOR_TEMP + expected_effect["entity_id"] = "light.entity_effect" + expected_calls.append(expected_effect) + + expected_temp = VALID_HS_COLOR + expected_temp["entity_id"] = "light.entity_temp" + expected_calls.append(expected_temp) + + expected_hs = VALID_RGB_COLOR + expected_hs["entity_id"] = "light.entity_hs" + expected_calls.append(expected_hs) + + expected_rgb = VALID_XY_COLOR + expected_rgb["entity_id"] = "light.entity_rgb" + expected_calls.append(expected_rgb) + + for call in turn_on_calls: + assert call.domain == "light" + found = False + for expected in expected_calls: + if call.data["entity_id"] == expected["entity_id"]: + # We found the matching entry + assert call.data == expected + found = True + break + # No entry found + assert found + + assert len(turn_off_calls) == 1 + assert turn_off_calls[0].domain == "light" + assert turn_off_calls[0].data == {"entity_id": "light.entity_xy"} diff --git a/tests/components/lock/test_device_action.py b/tests/components/lock/test_device_action.py new file mode 100644 index 00000000000..2006f9b3ff1 --- /dev/null +++ b/tests/components/lock/test_device_action.py @@ -0,0 +1,170 @@ +"""The tests for Lock device actions.""" +import pytest + +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 tests.common import ( + MockConfigEntry, + assert_lists_same, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@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) + + +async def test_get_actions_support_open(hass, device_reg, entity_reg): + """Test we get the expected actions from a lock which supports open.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + 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", + platform.ENTITIES["support_open"].unique_id, + device_id=device_entry.id, + ) + + expected_actions = [ + { + "domain": DOMAIN, + "type": "lock", + "device_id": device_entry.id, + "entity_id": "lock.support_open_lock", + }, + { + "domain": DOMAIN, + "type": "unlock", + "device_id": device_entry.id, + "entity_id": "lock.support_open_lock", + }, + { + "domain": DOMAIN, + "type": "open", + "device_id": device_entry.id, + "entity_id": "lock.support_open_lock", + }, + ] + actions = await async_get_device_automations(hass, "action", device_entry.id) + assert_lists_same(actions, expected_actions) + + +async def test_get_actions_not_support_open(hass, device_reg, entity_reg): + """Test we get the expected actions from a lock which doesn't support open.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + 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", + platform.ENTITIES["no_support_open"].unique_id, + device_id=device_entry.id, + ) + + expected_actions = [ + { + "domain": DOMAIN, + "type": "lock", + "device_id": device_entry.id, + "entity_id": "lock.no_support_open_lock", + }, + { + "domain": DOMAIN, + "type": "unlock", + "device_id": device_entry.id, + "entity_id": "lock.no_support_open_lock", + }, + ] + actions = await async_get_device_automations(hass, "action", device_entry.id) + assert_lists_same(actions, expected_actions) + + +async def test_action(hass): + """Test for lock actions.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event_lock"}, + "action": { + "domain": DOMAIN, + "device_id": "abcdefgh", + "entity_id": "lock.entity", + "type": "lock", + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event_unlock"}, + "action": { + "domain": DOMAIN, + "device_id": "abcdefgh", + "entity_id": "lock.entity", + "type": "unlock", + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event_open"}, + "action": { + "domain": DOMAIN, + "device_id": "abcdefgh", + "entity_id": "lock.entity", + "type": "open", + }, + }, + ] + }, + ) + + lock_calls = async_mock_service(hass, "lock", "lock") + unlock_calls = async_mock_service(hass, "lock", "unlock") + open_calls = async_mock_service(hass, "lock", "open") + + hass.bus.async_fire("test_event_lock") + await hass.async_block_till_done() + assert len(lock_calls) == 1 + assert len(unlock_calls) == 0 + assert len(open_calls) == 0 + + hass.bus.async_fire("test_event_unlock") + await hass.async_block_till_done() + assert len(lock_calls) == 1 + assert len(unlock_calls) == 1 + assert len(open_calls) == 0 + + hass.bus.async_fire("test_event_open") + await hass.async_block_till_done() + assert len(lock_calls) == 1 + assert len(unlock_calls) == 1 + assert len(open_calls) == 1 diff --git a/tests/components/lock/test_device_condition.py b/tests/components/lock/test_device_condition.py new file mode 100644 index 00000000000..675f402e770 --- /dev/null +++ b/tests/components/lock/test_device_condition.py @@ -0,0 +1,126 @@ +"""The tests for Lock device conditions.""" +import pytest + +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 tests.common import ( + MockConfigEntry, + assert_lists_same, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@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 serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_conditions(hass, device_reg, entity_reg): + """Test we get the expected conditions from a lock.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_conditions = [ + { + "condition": "device", + "domain": DOMAIN, + "type": "is_locked", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_unlocked", + "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("lock.entity", STATE_LOCKED) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "lock.entity", + "type": "is_locked", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_locked - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event2"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "lock.entity", + "type": "is_unlocked", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_unlocked - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + ] + }, + ) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "is_locked - event - test_event1" + + hass.states.async_set("lock.entity", STATE_UNLOCKED) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "is_unlocked - event - test_event2" diff --git a/tests/components/lock/test_reproduce_state.py b/tests/components/lock/test_reproduce_state.py new file mode 100644 index 00000000000..a9b61fa1219 --- /dev/null +++ b/tests/components/lock/test_reproduce_state.py @@ -0,0 +1,53 @@ +"""Test reproduce state for Lock.""" +from homeassistant.core import State + +from tests.common import async_mock_service + + +async def test_reproducing_states(hass, caplog): + """Test reproducing Lock states.""" + hass.states.async_set("lock.entity_locked", "locked", {}) + hass.states.async_set("lock.entity_unlocked", "unlocked", {}) + + lock_calls = async_mock_service(hass, "lock", "lock") + unlock_calls = async_mock_service(hass, "lock", "unlock") + + # These calls should do nothing as entities already in desired state + await hass.helpers.state.async_reproduce_state( + [ + State("lock.entity_locked", "locked"), + State("lock.entity_unlocked", "unlocked", {}), + ], + blocking=True, + ) + + assert len(lock_calls) == 0 + assert len(unlock_calls) == 0 + + # Test invalid state is handled + await hass.helpers.state.async_reproduce_state( + [State("lock.entity_locked", "not_supported")], blocking=True + ) + + assert "not_supported" in caplog.text + assert len(lock_calls) == 0 + assert len(unlock_calls) == 0 + + # Make sure correct services are called + await hass.helpers.state.async_reproduce_state( + [ + State("lock.entity_locked", "unlocked"), + State("lock.entity_unlocked", "locked"), + # Should not raise + State("lock.non_existing", "on"), + ], + blocking=True, + ) + + assert len(lock_calls) == 1 + assert lock_calls[0].domain == "lock" + assert lock_calls[0].data == {"entity_id": "lock.entity_unlocked"} + + assert len(unlock_calls) == 1 + assert unlock_calls[0].domain == "lock" + assert unlock_calls[0].data == {"entity_id": "lock.entity_locked"} diff --git a/tests/components/melissa/test_init.py b/tests/components/melissa/test_init.py index dfdaf80981f..892f4d60a44 100644 --- a/tests/components/melissa/test_init.py +++ b/tests/components/melissa/test_init.py @@ -1,14 +1,15 @@ """The test for the Melissa Climate component.""" -from tests.common import MockDependency, mock_coro_func - from homeassistant.components import melissa +from tests.common import MockDependency, mock_coro_func + VALID_CONFIG = {"melissa": {"username": "********", "password": "********"}} async def test_setup(hass): """Test setting up the Melissa component.""" with MockDependency("melissa") as mocked_melissa: + melissa.melissa = mocked_melissa mocked_melissa.AsyncMelissa().async_connect = mock_coro_func() await melissa.async_setup(hass, VALID_CONFIG) diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index 12a43030aa8..bb734d2c03d 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -546,6 +546,27 @@ async def test_no_command_topic(hass, mqtt_mock): assert hass.states.get("cover.test").attributes["supported_features"] == 240 +async def test_no_payload_stop(hass, mqtt_mock): + """Test with no stop payload.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + { + cover.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "command-topic", + "qos": 0, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": None, + } + }, + ) + + assert hass.states.get("cover.test").attributes["supported_features"] == 3 + + async def test_with_command_topic_and_tilt(hass, mqtt_mock): """Test with command topic and tilt config.""" assert await async_setup_component( diff --git a/tests/components/mqtt/test_device_tracker.py b/tests/components/mqtt/test_device_tracker.py index caad12b3e39..71348fcf5cb 100644 --- a/tests/components/mqtt/test_device_tracker.py +++ b/tests/components/mqtt/test_device_tracker.py @@ -3,8 +3,11 @@ from asynctest import patch import pytest from homeassistant.components import device_tracker -from homeassistant.components.device_tracker.const import ENTITY_ID_FORMAT -from homeassistant.const import CONF_PLATFORM +from homeassistant.components.device_tracker.const import ( + ENTITY_ID_FORMAT, + SOURCE_TYPE_BLUETOOTH, +) +from homeassistant.const import CONF_PLATFORM, STATE_HOME, STATE_NOT_HOME from homeassistant.setup import async_setup_component from tests.common import async_fire_mqtt_message @@ -156,3 +159,91 @@ async def test_multi_level_wildcard_topic_not_matching(hass, mock_device_tracker async_fire_mqtt_message(hass, topic, location) await hass.async_block_till_done() assert hass.states.get(entity_id) is None + + +async def test_matching_custom_payload_for_home_and_not_home( + hass, mock_device_tracker_conf +): + """Test custom payload_home sets state to home and custom payload_not_home sets state to not_home.""" + dev_id = "paulus" + entity_id = ENTITY_ID_FORMAT.format(dev_id) + topic = "/location/paulus" + payload_home = "present" + payload_not_home = "not present" + + hass.config.components = set(["mqtt", "zone"]) + assert await async_setup_component( + hass, + device_tracker.DOMAIN, + { + device_tracker.DOMAIN: { + CONF_PLATFORM: "mqtt", + "devices": {dev_id: topic}, + "payload_home": payload_home, + "payload_not_home": payload_not_home, + } + }, + ) + async_fire_mqtt_message(hass, topic, payload_home) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_HOME + + async_fire_mqtt_message(hass, topic, payload_not_home) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_NOT_HOME + + +async def test_not_matching_custom_payload_for_home_and_not_home( + hass, mock_device_tracker_conf +): + """Test not matching payload does not set state to home or not_home.""" + dev_id = "paulus" + entity_id = ENTITY_ID_FORMAT.format(dev_id) + topic = "/location/paulus" + payload_home = "present" + payload_not_home = "not present" + payload_not_matching = "test" + + hass.config.components = set(["mqtt", "zone"]) + assert await async_setup_component( + hass, + device_tracker.DOMAIN, + { + device_tracker.DOMAIN: { + CONF_PLATFORM: "mqtt", + "devices": {dev_id: topic}, + "payload_home": payload_home, + "payload_not_home": payload_not_home, + } + }, + ) + async_fire_mqtt_message(hass, topic, payload_not_matching) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state != STATE_HOME + assert hass.states.get(entity_id).state != STATE_NOT_HOME + + +async def test_matching_source_type(hass, mock_device_tracker_conf): + """Test setting source type.""" + dev_id = "paulus" + entity_id = ENTITY_ID_FORMAT.format(dev_id) + topic = "/location/paulus" + source_type = SOURCE_TYPE_BLUETOOTH + location = "work" + + hass.config.components = set(["mqtt", "zone"]) + assert await async_setup_component( + hass, + device_tracker.DOMAIN, + { + device_tracker.DOMAIN: { + CONF_PLATFORM: "mqtt", + "devices": {dev_id: topic}, + "source_type": source_type, + } + }, + ) + + async_fire_mqtt_message(hass, topic, location) + await hass.async_block_till_done() + assert hass.states.get(entity_id).attributes["source_type"] == SOURCE_TYPE_BLUETOOTH diff --git a/tests/components/mqtt/test_legacy_vacuum.py b/tests/components/mqtt/test_legacy_vacuum.py index add27bebdeb..c35740407c7 100644 --- a/tests/components/mqtt/test_legacy_vacuum.py +++ b/tests/components/mqtt/test_legacy_vacuum.py @@ -5,10 +5,8 @@ import json from homeassistant.components import mqtt, vacuum from homeassistant.components.mqtt import CONF_COMMAND_TOPIC from homeassistant.components.mqtt.discovery import async_start -from homeassistant.components.mqtt.vacuum import ( - schema_legacy as mqttvacuum, - services_to_strings, -) +from homeassistant.components.mqtt.vacuum import schema_legacy as mqttvacuum +from homeassistant.components.mqtt.vacuum.schema import services_to_strings from homeassistant.components.mqtt.vacuum.schema_legacy import ( ALL_SERVICES, SERVICE_TO_STRING, @@ -80,7 +78,7 @@ async def test_default_supported_features(hass, mqtt_mock): async def test_all_commands(hass, mqtt_mock): """Test simple commands to the vacuum.""" config = deepcopy(DEFAULT_CONFIG) - config[mqttvacuum.CONF_SUPPORTED_FEATURES] = mqttvacuum.services_to_strings( + config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( ALL_SERVICES, SERVICE_TO_STRING ) @@ -221,7 +219,7 @@ async def test_attributes_without_supported_features(hass, mqtt_mock): async def test_status(hass, mqtt_mock): """Test status updates from the vacuum.""" config = deepcopy(DEFAULT_CONFIG) - config[mqttvacuum.CONF_SUPPORTED_FEATURES] = mqttvacuum.services_to_strings( + config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( ALL_SERVICES, SERVICE_TO_STRING ) @@ -260,7 +258,7 @@ async def test_status(hass, mqtt_mock): async def test_status_battery(hass, mqtt_mock): """Test status updates from the vacuum.""" config = deepcopy(DEFAULT_CONFIG) - config[mqttvacuum.CONF_SUPPORTED_FEATURES] = mqttvacuum.services_to_strings( + config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( ALL_SERVICES, SERVICE_TO_STRING ) @@ -277,7 +275,7 @@ async def test_status_battery(hass, mqtt_mock): async def test_status_cleaning(hass, mqtt_mock): """Test status updates from the vacuum.""" config = deepcopy(DEFAULT_CONFIG) - config[mqttvacuum.CONF_SUPPORTED_FEATURES] = mqttvacuum.services_to_strings( + config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( ALL_SERVICES, SERVICE_TO_STRING ) @@ -294,7 +292,7 @@ async def test_status_cleaning(hass, mqtt_mock): async def test_status_docked(hass, mqtt_mock): """Test status updates from the vacuum.""" config = deepcopy(DEFAULT_CONFIG) - config[mqttvacuum.CONF_SUPPORTED_FEATURES] = mqttvacuum.services_to_strings( + config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( ALL_SERVICES, SERVICE_TO_STRING ) @@ -311,7 +309,7 @@ async def test_status_docked(hass, mqtt_mock): async def test_status_charging(hass, mqtt_mock): """Test status updates from the vacuum.""" config = deepcopy(DEFAULT_CONFIG) - config[mqttvacuum.CONF_SUPPORTED_FEATURES] = mqttvacuum.services_to_strings( + config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( ALL_SERVICES, SERVICE_TO_STRING ) @@ -328,7 +326,7 @@ async def test_status_charging(hass, mqtt_mock): async def test_status_fan_speed(hass, mqtt_mock): """Test status updates from the vacuum.""" config = deepcopy(DEFAULT_CONFIG) - config[mqttvacuum.CONF_SUPPORTED_FEATURES] = mqttvacuum.services_to_strings( + config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( ALL_SERVICES, SERVICE_TO_STRING ) @@ -345,7 +343,7 @@ async def test_status_fan_speed(hass, mqtt_mock): async def test_status_error(hass, mqtt_mock): """Test status updates from the vacuum.""" config = deepcopy(DEFAULT_CONFIG) - config[mqttvacuum.CONF_SUPPORTED_FEATURES] = mqttvacuum.services_to_strings( + config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( ALL_SERVICES, SERVICE_TO_STRING ) @@ -371,7 +369,7 @@ async def test_battery_template(hass, mqtt_mock): config = deepcopy(DEFAULT_CONFIG) config.update( { - mqttvacuum.CONF_SUPPORTED_FEATURES: mqttvacuum.services_to_strings( + mqttvacuum.CONF_SUPPORTED_FEATURES: services_to_strings( ALL_SERVICES, SERVICE_TO_STRING ), mqttvacuum.CONF_BATTERY_LEVEL_TOPIC: "retroroomba/battery_level", @@ -390,7 +388,7 @@ async def test_battery_template(hass, mqtt_mock): async def test_status_invalid_json(hass, mqtt_mock): """Test to make sure nothing breaks if the vacuum sends bad JSON.""" config = deepcopy(DEFAULT_CONFIG) - config[mqttvacuum.CONF_SUPPORTED_FEATURES] = mqttvacuum.services_to_strings( + config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( ALL_SERVICES, SERVICE_TO_STRING ) diff --git a/tests/components/mqtt/test_server.py b/tests/components/mqtt/test_server.py index c210d773faf..71dff7ef3ac 100644 --- a/tests/components/mqtt/test_server.py +++ b/tests/components/mqtt/test_server.py @@ -19,9 +19,13 @@ class TestMQTT: """Stop everything that was started.""" self.hass.stop() - @patch("passlib.apps.custom_app_context", Mock(return_value="")) + @patch( + "homeassistant.components.mqtt.server.custom_app_context", Mock(return_value="") + ) @patch("tempfile.NamedTemporaryFile", Mock(return_value=MagicMock())) - @patch("hbmqtt.broker.Broker", Mock(return_value=MagicMock())) + @patch( + "homeassistant.components.mqtt.server.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): @@ -41,9 +45,13 @@ class TestMQTT: assert mock_mqtt.mock_calls[1][2]["username"] == "homeassistant" assert mock_mqtt.mock_calls[1][2]["password"] == password - @patch("passlib.apps.custom_app_context", Mock(return_value="")) + @patch( + "homeassistant.components.mqtt.server.custom_app_context", Mock(return_value="") + ) @patch("tempfile.NamedTemporaryFile", Mock(return_value=MagicMock())) - @patch("hbmqtt.broker.Broker", Mock(return_value=MagicMock())) + @patch( + "homeassistant.components.mqtt.server.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): @@ -57,12 +65,7 @@ class TestMQTT: self.hass.config.api = MagicMock(api_password="api_password") assert setup_component( - self.hass, - mqtt.DOMAIN, - { - "http": {"api_password": "http_secret"}, - mqtt.DOMAIN: {CONF_PASSWORD: password}, - }, + self.hass, mqtt.DOMAIN, {mqtt.DOMAIN: {CONF_PASSWORD: password}} ) self.hass.block_till_done() assert mock_mqtt.called diff --git a/tests/components/mqtt/test_state_vacuum.py b/tests/components/mqtt/test_state_vacuum.py index fe100bdcb6e..572c3b05752 100644 --- a/tests/components/mqtt/test_state_vacuum.py +++ b/tests/components/mqtt/test_state_vacuum.py @@ -5,11 +5,8 @@ import json from homeassistant.components import mqtt, vacuum from homeassistant.components.mqtt import CONF_COMMAND_TOPIC, CONF_STATE_TOPIC from homeassistant.components.mqtt.discovery import async_start -from homeassistant.components.mqtt.vacuum import ( - CONF_SCHEMA, - schema_state as mqttvacuum, - services_to_strings, -) +from homeassistant.components.mqtt.vacuum import CONF_SCHEMA, schema_state as mqttvacuum +from homeassistant.components.mqtt.vacuum.schema import services_to_strings from homeassistant.components.mqtt.vacuum.schema_state import SERVICE_TO_STRING from homeassistant.components.vacuum import ( ATTR_BATTERY_ICON, @@ -259,7 +256,7 @@ async def test_no_fan_vacuum(hass, mqtt_mock): async def test_status_invalid_json(hass, mqtt_mock): """Test to make sure nothing breaks if the vacuum sends bad JSON.""" config = deepcopy(DEFAULT_CONFIG) - config[mqttvacuum.CONF_SUPPORTED_FEATURES] = mqttvacuum.services_to_strings( + config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( mqttvacuum.ALL_SERVICES, SERVICE_TO_STRING ) diff --git a/tests/components/neato/__init__.py b/tests/components/neato/__init__.py new file mode 100644 index 00000000000..7927918395c --- /dev/null +++ b/tests/components/neato/__init__.py @@ -0,0 +1 @@ +"""Tests for the Neato component.""" diff --git a/tests/components/neato/test_config_flow.py b/tests/components/neato/test_config_flow.py new file mode 100644 index 00000000000..3f4bd90d0c1 --- /dev/null +++ b/tests/components/neato/test_config_flow.py @@ -0,0 +1,161 @@ +"""Tests for the Neato config flow.""" +import pytest +from unittest.mock import patch + +from pybotvac.exceptions import NeatoLoginException, NeatoRobotException + +from homeassistant import data_entry_flow +from homeassistant.components.neato import config_flow +from homeassistant.components.neato.const import CONF_VENDOR, NEATO_DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME + +from tests.common import MockConfigEntry + +USERNAME = "myUsername" +PASSWORD = "myPassword" +VENDOR_NEATO = "neato" +VENDOR_VORWERK = "vorwerk" +VENDOR_INVALID = "invalid" + + +@pytest.fixture(name="account") +def mock_controller_login(): + """Mock a successful login.""" + with patch("homeassistant.components.neato.config_flow.Account", return_value=True): + yield + + +def init_config_flow(hass): + """Init a configuration flow.""" + flow = config_flow.NeatoConfigFlow() + flow.hass = hass + return flow + + +async def test_user(hass, account): + """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" + + result = await flow.async_step_user( + {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, CONF_VENDOR: VENDOR_NEATO} + ) + + 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_VENDOR] == VENDOR_NEATO + + result = await flow.async_step_user( + {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, CONF_VENDOR: VENDOR_VORWERK} + ) + + 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_VENDOR] == VENDOR_VORWERK + + +async def test_import(hass, account): + """Test import step.""" + flow = init_config_flow(hass) + + result = await flow.async_step_import( + {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, CONF_VENDOR: VENDOR_NEATO} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == f"{USERNAME} (from configuration)" + assert result["data"][CONF_USERNAME] == USERNAME + assert result["data"][CONF_PASSWORD] == PASSWORD + assert result["data"][CONF_VENDOR] == VENDOR_NEATO + + +async def test_abort_if_already_setup(hass, account): + """Test we abort if Neato is already setup.""" + flow = init_config_flow(hass) + MockConfigEntry( + domain=NEATO_DOMAIN, + data={ + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_VENDOR: VENDOR_NEATO, + }, + ).add_to_hass(hass) + + # Should fail, same USERNAME (import) + result = await flow.async_step_import( + {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, CONF_VENDOR: VENDOR_NEATO} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + # Should fail, same USERNAME (flow) + result = await flow.async_step_user( + {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, CONF_VENDOR: VENDOR_NEATO} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_abort_on_invalid_credentials(hass): + """Test when we have invalid credentials.""" + flow = init_config_flow(hass) + + with patch( + "homeassistant.components.neato.config_flow.Account", + side_effect=NeatoLoginException(), + ): + result = await flow.async_step_user( + { + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_VENDOR: VENDOR_NEATO, + } + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "invalid_credentials"} + + result = await flow.async_step_import( + { + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_VENDOR: VENDOR_NEATO, + } + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "invalid_credentials" + + +async def test_abort_on_unexpected_error(hass): + """Test when we have an unexpected error.""" + flow = init_config_flow(hass) + + with patch( + "homeassistant.components.neato.config_flow.Account", + side_effect=NeatoRobotException(), + ): + result = await flow.async_step_user( + { + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_VENDOR: VENDOR_NEATO, + } + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "unexpected_error"} + + result = await flow.async_step_import( + { + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_VENDOR: VENDOR_NEATO, + } + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "unexpected_error" diff --git a/tests/components/neato/test_init.py b/tests/components/neato/test_init.py new file mode 100644 index 00000000000..444cbe8cc5d --- /dev/null +++ b/tests/components/neato/test_init.py @@ -0,0 +1,119 @@ +"""Tests for the Neato init file.""" +import pytest +from unittest.mock import patch + +from pybotvac.exceptions import NeatoLoginException + +from homeassistant.components.neato.const import CONF_VENDOR, NEATO_DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + +USERNAME = "myUsername" +PASSWORD = "myPassword" +VENDOR_NEATO = "neato" +VENDOR_VORWERK = "vorwerk" +VENDOR_INVALID = "invalid" + +VALID_CONFIG = { + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_VENDOR: VENDOR_NEATO, +} + +DIFFERENT_CONFIG = { + CONF_USERNAME: "anotherUsername", + CONF_PASSWORD: "anotherPassword", + CONF_VENDOR: VENDOR_VORWERK, +} + +INVALID_CONFIG = { + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_VENDOR: VENDOR_INVALID, +} + + +@pytest.fixture(name="config_flow") +def mock_config_flow_login(): + """Mock a successful login.""" + with patch("homeassistant.components.neato.config_flow.Account", return_value=True): + yield + + +@pytest.fixture(name="hub") +def mock_controller_login(): + """Mock a successful login.""" + with patch("homeassistant.components.neato.Account", return_value=True): + yield + + +async def test_no_config_entry(hass): + """There is nothing in configuration.yaml.""" + res = await async_setup_component(hass, NEATO_DOMAIN, {}) + assert res is True + + +async def test_create_valid_config_entry(hass, config_flow, hub): + """There is something in configuration.yaml.""" + assert hass.config_entries.async_entries(NEATO_DOMAIN) == [] + assert await async_setup_component(hass, NEATO_DOMAIN, {NEATO_DOMAIN: VALID_CONFIG}) + await hass.async_block_till_done() + + entries = hass.config_entries.async_entries(NEATO_DOMAIN) + assert entries + assert entries[0].data[CONF_USERNAME] == USERNAME + assert entries[0].data[CONF_PASSWORD] == PASSWORD + assert entries[0].data[CONF_VENDOR] == VENDOR_NEATO + + +async def test_config_entries_in_sync(hass, hub): + """The config entry and configuration.yaml are in sync.""" + MockConfigEntry(domain=NEATO_DOMAIN, data=VALID_CONFIG).add_to_hass(hass) + + assert hass.config_entries.async_entries(NEATO_DOMAIN) + assert await async_setup_component(hass, NEATO_DOMAIN, {NEATO_DOMAIN: VALID_CONFIG}) + await hass.async_block_till_done() + + entries = hass.config_entries.async_entries(NEATO_DOMAIN) + assert entries + assert entries[0].data[CONF_USERNAME] == USERNAME + assert entries[0].data[CONF_PASSWORD] == PASSWORD + assert entries[0].data[CONF_VENDOR] == VENDOR_NEATO + + +async def test_config_entries_not_in_sync(hass, config_flow, hub): + """The config entry and configuration.yaml are not in sync.""" + MockConfigEntry(domain=NEATO_DOMAIN, data=DIFFERENT_CONFIG).add_to_hass(hass) + + assert hass.config_entries.async_entries(NEATO_DOMAIN) + assert await async_setup_component(hass, NEATO_DOMAIN, {NEATO_DOMAIN: VALID_CONFIG}) + await hass.async_block_till_done() + + entries = hass.config_entries.async_entries(NEATO_DOMAIN) + assert entries + assert entries[0].data[CONF_USERNAME] == USERNAME + assert entries[0].data[CONF_PASSWORD] == PASSWORD + assert entries[0].data[CONF_VENDOR] == VENDOR_NEATO + + +async def test_config_entries_not_in_sync_error(hass): + """The config entry and configuration.yaml are not in sync, the new configuration is wrong.""" + MockConfigEntry(domain=NEATO_DOMAIN, data=VALID_CONFIG).add_to_hass(hass) + + assert hass.config_entries.async_entries(NEATO_DOMAIN) + with patch( + "homeassistant.components.neato.config_flow.Account", + side_effect=NeatoLoginException(), + ): + assert not await async_setup_component( + hass, NEATO_DOMAIN, {NEATO_DOMAIN: DIFFERENT_CONFIG} + ) + await hass.async_block_till_done() + + entries = hass.config_entries.async_entries(NEATO_DOMAIN) + assert entries + assert entries[0].data[CONF_USERNAME] == USERNAME + assert entries[0].data[CONF_PASSWORD] == PASSWORD + assert entries[0].data[CONF_VENDOR] == VENDOR_NEATO diff --git a/tests/components/nuheat/test_init.py b/tests/components/nuheat/test_init.py index a6be3ac14f5..90a209fd897 100644 --- a/tests/components/nuheat/test_init.py +++ b/tests/components/nuheat/test_init.py @@ -1,11 +1,11 @@ """NuHeat component tests.""" import unittest - from unittest.mock import patch -from tests.common import get_test_home_assistant, MockDependency from homeassistant.components import nuheat +from tests.common import MockDependency, get_test_home_assistant + VALID_CONFIG = { "nuheat": {"username": "warm", "password": "feet", "devices": "thermostat123"} } @@ -27,11 +27,12 @@ class TestNuHeat(unittest.TestCase): @patch("homeassistant.helpers.discovery.load_platform") def test_setup(self, mocked_nuheat, mocked_load): """Test setting up the NuHeat component.""" - nuheat.setup(self.hass, self.config) + with patch.object(nuheat, "nuheat", mocked_nuheat): + nuheat.setup(self.hass, self.config) mocked_nuheat.NuHeat.assert_called_with("warm", "feet") assert nuheat.DOMAIN in self.hass.data - assert 2 == len(self.hass.data[nuheat.DOMAIN]) + assert len(self.hass.data[nuheat.DOMAIN]) == 2 assert isinstance( self.hass.data[nuheat.DOMAIN][0], type(mocked_nuheat.NuHeat()) ) diff --git a/tests/components/opentherm_gw/__init__.py b/tests/components/opentherm_gw/__init__.py new file mode 100644 index 00000000000..2dfe9267651 --- /dev/null +++ b/tests/components/opentherm_gw/__init__.py @@ -0,0 +1 @@ +"""Tests for the Opentherm Gateway integration.""" diff --git a/tests/components/opentherm_gw/test_config_flow.py b/tests/components/opentherm_gw/test_config_flow.py new file mode 100644 index 00000000000..89f2783cf71 --- /dev/null +++ b/tests/components/opentherm_gw/test_config_flow.py @@ -0,0 +1,209 @@ +"""Test the Opentherm Gateway config flow.""" +import asyncio +from serial import SerialException +from unittest.mock import patch + +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, +) + +from pyotgw import OTGW_ABOUT +from tests.common import mock_coro, MockConfigEntry + + +async def test_form_user(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.opentherm_gw.async_setup", + return_value=mock_coro(True), + ) as mock_setup, patch( + "homeassistant.components.opentherm_gw.async_setup_entry", + return_value=mock_coro(True), + ) as mock_setup_entry, patch( + "pyotgw.pyotgw.connect", + return_value=mock_coro({OTGW_ABOUT: "OpenTherm Gateway 4.2.5"}), + ) as mock_pyotgw_connect, patch( + "pyotgw.pyotgw.disconnect", return_value=mock_coro(None) + ) as mock_pyotgw_disconnect: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_NAME: "Test Entry 1", CONF_DEVICE: "/dev/ttyUSB0"} + ) + + assert result2["type"] == "create_entry" + assert result2["title"] == "Test Entry 1" + assert result2["data"] == { + CONF_NAME: "Test Entry 1", + CONF_DEVICE: "/dev/ttyUSB0", + CONF_ID: "test_entry_1", + } + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_pyotgw_connect.mock_calls) == 1 + assert len(mock_pyotgw_disconnect.mock_calls) == 1 + + +async def test_form_import(hass): + """Test import from existing config.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + with patch( + "homeassistant.components.opentherm_gw.async_setup", + return_value=mock_coro(True), + ) as mock_setup, patch( + "homeassistant.components.opentherm_gw.async_setup_entry", + return_value=mock_coro(True), + ) as mock_setup_entry, patch( + "pyotgw.pyotgw.connect", + return_value=mock_coro({OTGW_ABOUT: "OpenTherm Gateway 4.2.5"}), + ) as mock_pyotgw_connect, patch( + "pyotgw.pyotgw.disconnect", return_value=mock_coro(None) + ) as mock_pyotgw_disconnect: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={CONF_ID: "legacy_gateway", CONF_DEVICE: "/dev/ttyUSB1"}, + ) + + assert result["type"] == "create_entry" + assert result["title"] == "legacy_gateway" + assert result["data"] == { + CONF_NAME: "legacy_gateway", + CONF_DEVICE: "/dev/ttyUSB1", + CONF_ID: "legacy_gateway", + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_pyotgw_connect.mock_calls) == 1 + assert len(mock_pyotgw_disconnect.mock_calls) == 1 + + +async def test_form_duplicate_entries(hass): + """Test duplicate device or id errors.""" + flow1 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + flow2 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + flow3 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.opentherm_gw.async_setup", + return_value=mock_coro(True), + ) as mock_setup, patch( + "homeassistant.components.opentherm_gw.async_setup_entry", + return_value=mock_coro(True), + ) as mock_setup_entry, patch( + "pyotgw.pyotgw.connect", + return_value=mock_coro({OTGW_ABOUT: "OpenTherm Gateway 4.2.5"}), + ) as mock_pyotgw_connect, patch( + "pyotgw.pyotgw.disconnect", return_value=mock_coro(None) + ) as mock_pyotgw_disconnect: + result1 = await hass.config_entries.flow.async_configure( + flow1["flow_id"], {CONF_NAME: "Test Entry 1", CONF_DEVICE: "/dev/ttyUSB0"} + ) + result2 = await hass.config_entries.flow.async_configure( + flow2["flow_id"], {CONF_NAME: "Test Entry 1", CONF_DEVICE: "/dev/ttyUSB1"} + ) + result3 = await hass.config_entries.flow.async_configure( + flow3["flow_id"], {CONF_NAME: "Test Entry 2", CONF_DEVICE: "/dev/ttyUSB0"} + ) + assert result1["type"] == "create_entry" + assert result2["type"] == "form" + assert result2["errors"] == {"base": "id_exists"} + assert result3["type"] == "form" + assert result3["errors"] == {"base": "already_configured"} + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_pyotgw_connect.mock_calls) == 1 + assert len(mock_pyotgw_disconnect.mock_calls) == 1 + + +async def test_form_connection_timeout(hass): + """Test we handle connection timeout.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "pyotgw.pyotgw.connect", side_effect=(asyncio.TimeoutError) + ) as mock_connect: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_NAME: "Test Entry 1", CONF_DEVICE: "socket://192.0.2.254:1234"}, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "timeout"} + assert len(mock_connect.mock_calls) == 1 + + +async def test_form_connection_error(hass): + """Test we handle serial connection error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch("pyotgw.pyotgw.connect", side_effect=(SerialException)) as mock_connect: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_NAME: "Test Entry 1", CONF_DEVICE: "/dev/ttyUSB0"} + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "serial_error"} + assert len(mock_connect.mock_calls) == 1 + + +async def test_options_form(hass): + """Test the options form.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="Mock Gateway", + data={ + CONF_NAME: "Mock Gateway", + CONF_DEVICE: "/dev/null", + CONF_ID: "mock_gateway", + }, + options={}, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.options.flow.async_init( + entry.entry_id, context={"source": "test"}, data=None + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.flow.async_configure( + result["flow_id"], + user_input={CONF_FLOOR_TEMP: True, CONF_PRECISION: PRECISION_HALVES}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"][CONF_PRECISION] == PRECISION_HALVES + assert result["data"][CONF_FLOOR_TEMP] is True + + result = await hass.config_entries.options.flow.async_init( + entry.entry_id, context={"source": "test"}, data=None + ) + + result = await hass.config_entries.options.flow.async_configure( + result["flow_id"], user_input={CONF_PRECISION: 0} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"][CONF_PRECISION] is None + assert result["data"][CONF_FLOOR_TEMP] is True diff --git a/tests/components/owntracks/test_config_flow.py b/tests/components/owntracks/test_config_flow.py index 76863c61698..c4e2a54f69a 100644 --- a/tests/components/owntracks/test_config_flow.py +++ b/tests/components/owntracks/test_config_flow.py @@ -43,6 +43,7 @@ async def test_config_flow_unload(hass): async def test_with_cloud_sub(hass): """Test creating a config flow while subscribed.""" + hass.config.components.add("cloud") with patch( "homeassistant.components.cloud.async_active_subscription", return_value=True ), patch( diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index 2a2178da9d5..c0d14f1efdc 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -46,14 +46,14 @@ async def test_bad_credentials(hass): assert result["type"] == "form" assert result["step_id"] == "start_website_auth" - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "external" - with patch( "plexapi.myplex.MyPlexAccount", side_effect=plexapi.exceptions.Unauthorized ), asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( "plexauth.PlexAuth.token", return_value="BAD TOKEN" ): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external_done" @@ -103,17 +103,17 @@ async def test_import_file_from_discovery(hass): async def test_discovery(hass): """Test starting a flow from discovery.""" - - result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, - context={"source": "discovery"}, - data={ - CONF_HOST: MOCK_SERVERS[0][CONF_HOST], - CONF_PORT: MOCK_SERVERS[0][CONF_PORT], - }, - ) - assert result["type"] == "abort" - assert result["reason"] == "discovery_no_file" + with patch("homeassistant.components.plex.config_flow.load_json", return_value={}): + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": "discovery"}, + data={ + CONF_HOST: MOCK_SERVERS[0][CONF_HOST], + CONF_PORT: MOCK_SERVERS[0][CONF_PORT], + }, + ) + assert result["type"] == "abort" + assert result["reason"] == "discovery_no_file" async def test_discovery_while_in_progress(hass): @@ -192,12 +192,12 @@ async def test_unknown_exception(hass): assert result["type"] == "form" assert result["step_id"] == "start_website_auth" - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "external" - with patch("plexapi.myplex.MyPlexAccount", side_effect=Exception), asynctest.patch( "plexauth.PlexAuth.initiate_auth" ), asynctest.patch("plexauth.PlexAuth.token", return_value="MOCK_TOKEN"): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external_done" @@ -217,14 +217,13 @@ async def test_no_servers_found(hass): assert result["type"] == "form" assert result["step_id"] == "start_website_auth" - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "external" - with patch( "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(servers=0) ), asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external_done" @@ -248,14 +247,14 @@ async def test_single_available_server(hass): assert result["type"] == "form" assert result["step_id"] == "start_website_auth" - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "external" - with patch("plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()), patch( "plexapi.server.PlexServer", return_value=mock_plex_server ), asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external_done" @@ -287,9 +286,6 @@ async def test_multiple_servers_with_selection(hass): assert result["type"] == "form" assert result["step_id"] == "start_website_auth" - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "external" - with patch( "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(servers=2) ), patch( @@ -299,6 +295,9 @@ async def test_multiple_servers_with_selection(hass): ), asynctest.patch( "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external_done" @@ -349,9 +348,6 @@ async def test_adding_last_unconfigured_server(hass): assert result["type"] == "form" assert result["step_id"] == "start_website_auth" - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "external" - with patch( "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(servers=2) ), patch( @@ -361,6 +357,9 @@ async def test_adding_last_unconfigured_server(hass): ), asynctest.patch( "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external_done" @@ -440,14 +439,14 @@ async def test_all_available_servers_configured(hass): assert result["type"] == "form" assert result["step_id"] == "start_website_auth" - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "external" - 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_configure(result["flow_id"]) + assert result["type"] == "external" + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external_done" @@ -495,12 +494,12 @@ async def test_external_timed_out(hass): assert result["type"] == "form" assert result["step_id"] == "start_website_auth" - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "external" - with asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( "plexauth.PlexAuth.token", return_value=None ): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external_done" @@ -520,12 +519,12 @@ async def test_callback_view(hass, aiohttp_client): assert result["type"] == "form" assert result["step_id"] == "start_website_auth" - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "external" - with asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" + client = await aiohttp_client(hass.http.app) forward_url = f'{config_flow.AUTH_CALLBACK_PATH}?flow_id={result["flow_id"]}' diff --git a/tests/components/ps4/test_config_flow.py b/tests/components/ps4/test_config_flow.py index 42f319e7343..81f81093a67 100644 --- a/tests/components/ps4/test_config_flow.py +++ b/tests/components/ps4/test_config_flow.py @@ -1,6 +1,8 @@ """Define tests for the PlayStation 4 config flow.""" from unittest.mock import patch +from pyps4_2ndscreen.errors import CredentialTimeout + from homeassistant import data_entry_flow from homeassistant.components import ps4 from homeassistant.components.ps4.const import DEFAULT_NAME, DEFAULT_REGION @@ -73,28 +75,28 @@ async def test_full_flow_implementation(hass): manager = hass.config_entries # User Step Started, results in Step Creds - with patch("pyps4_homeassistant.Helper.port_bind", return_value=None): + with patch("pyps4_2ndscreen.Helper.port_bind", return_value=None): result = await flow.async_step_user() assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "creds" # Step Creds results with form in Step Mode. - with patch("pyps4_homeassistant.Helper.get_creds", return_value=MOCK_CREDS): + with patch("pyps4_2ndscreen.Helper.get_creds", return_value=MOCK_CREDS): result = await flow.async_step_creds({}) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "mode" # Step Mode with User Input which is not manual, results in Step Link. with patch( - "pyps4_homeassistant.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}] + "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}] ): result = await flow.async_step_mode(MOCK_AUTO) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "link" # User Input results in created entry. - with patch("pyps4_homeassistant.Helper.link", return_value=(True, True)), patch( - "pyps4_homeassistant.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}] + with patch("pyps4_2ndscreen.Helper.link", return_value=(True, True)), patch( + "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}] ): result = await flow.async_step_link(MOCK_CONFIG) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -126,20 +128,20 @@ async def test_multiple_flow_implementation(hass): manager = hass.config_entries # User Step Started, results in Step Creds - with patch("pyps4_homeassistant.Helper.port_bind", return_value=None): + with patch("pyps4_2ndscreen.Helper.port_bind", return_value=None): result = await flow.async_step_user() assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "creds" # Step Creds results with form in Step Mode. - with patch("pyps4_homeassistant.Helper.get_creds", return_value=MOCK_CREDS): + with patch("pyps4_2ndscreen.Helper.get_creds", return_value=MOCK_CREDS): result = await flow.async_step_creds({}) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "mode" # Step Mode with User Input which is not manual, results in Step Link. with patch( - "pyps4_homeassistant.Helper.has_devices", + "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}, {"host-ip": MOCK_HOST_ADDITIONAL}], ): result = await flow.async_step_mode(MOCK_AUTO) @@ -147,8 +149,8 @@ async def test_multiple_flow_implementation(hass): assert result["step_id"] == "link" # User Input results in created entry. - with patch("pyps4_homeassistant.Helper.link", return_value=(True, True)), patch( - "pyps4_homeassistant.Helper.has_devices", + with patch("pyps4_2ndscreen.Helper.link", return_value=(True, True)), patch( + "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}, {"host-ip": MOCK_HOST_ADDITIONAL}], ): result = await flow.async_step_link(MOCK_CONFIG) @@ -175,8 +177,8 @@ async def test_multiple_flow_implementation(hass): # Test additional flow. # User Step Started, results in Step Mode: - with patch("pyps4_homeassistant.Helper.port_bind", return_value=None), patch( - "pyps4_homeassistant.Helper.has_devices", + with patch("pyps4_2ndscreen.Helper.port_bind", return_value=None), patch( + "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}, {"host-ip": MOCK_HOST_ADDITIONAL}], ): result = await flow.async_step_user() @@ -184,14 +186,14 @@ async def test_multiple_flow_implementation(hass): assert result["step_id"] == "creds" # Step Creds results with form in Step Mode. - with patch("pyps4_homeassistant.Helper.get_creds", return_value=MOCK_CREDS): + with patch("pyps4_2ndscreen.Helper.get_creds", return_value=MOCK_CREDS): result = await flow.async_step_creds({}) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "mode" # Step Mode with User Input which is not manual, results in Step Link. with patch( - "pyps4_homeassistant.Helper.has_devices", + "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}, {"host-ip": MOCK_HOST_ADDITIONAL}], ): result = await flow.async_step_mode(MOCK_AUTO) @@ -200,9 +202,9 @@ async def test_multiple_flow_implementation(hass): # Step Link with patch( - "pyps4_homeassistant.Helper.has_devices", + "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}, {"host-ip": MOCK_HOST_ADDITIONAL}], - ), patch("pyps4_homeassistant.Helper.link", return_value=(True, True)): + ), patch("pyps4_2ndscreen.Helper.link", return_value=(True, True)): result = await flow.async_step_link(MOCK_CONFIG_ADDITIONAL) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["data"][CONF_TOKEN] == MOCK_CREDS @@ -232,13 +234,13 @@ async def test_port_bind_abort(hass): flow = ps4.PlayStation4FlowHandler() flow.hass = hass - with patch("pyps4_homeassistant.Helper.port_bind", return_value=MOCK_UDP_PORT): + with patch("pyps4_2ndscreen.Helper.port_bind", return_value=MOCK_UDP_PORT): reason = "port_987_bind_error" result = await flow.async_step_user(user_input=None) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == reason - with patch("pyps4_homeassistant.Helper.port_bind", return_value=MOCK_TCP_PORT): + with patch("pyps4_2ndscreen.Helper.port_bind", return_value=MOCK_TCP_PORT): reason = "port_997_bind_error" result = await flow.async_step_user(user_input=None) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT @@ -253,7 +255,7 @@ async def test_duplicate_abort(hass): flow.creds = MOCK_CREDS with patch( - "pyps4_homeassistant.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}] + "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}] ): result = await flow.async_step_link(user_input=None) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT @@ -274,9 +276,9 @@ async def test_additional_device(hass): assert len(manager.async_entries()) == 1 with patch( - "pyps4_homeassistant.Helper.has_devices", + "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}, {"host-ip": MOCK_HOST_ADDITIONAL}], - ), patch("pyps4_homeassistant.Helper.link", return_value=(True, True)): + ), patch("pyps4_2ndscreen.Helper.link", return_value=(True, True)): result = await flow.async_step_link(MOCK_CONFIG_ADDITIONAL) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["data"][CONF_TOKEN] == MOCK_CREDS @@ -296,7 +298,7 @@ async def test_no_devices_found_abort(hass): flow = ps4.PlayStation4FlowHandler() flow.hass = hass - with patch("pyps4_homeassistant.Helper.has_devices", return_value=[]): + with patch("pyps4_2ndscreen.Helper.has_devices", return_value=[]): result = await flow.async_step_link() assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "no_devices_found" @@ -310,8 +312,7 @@ async def test_manual_mode(hass): # Step Mode with User Input: manual, results in Step Link. with patch( - "pyps4_homeassistant.Helper.has_devices", - return_value=[{"host-ip": flow.m_device}], + "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": flow.m_device}] ): result = await flow.async_step_mode(MOCK_MANUAL) assert flow.m_device == MOCK_HOST @@ -324,7 +325,7 @@ async def test_credential_abort(hass): flow = ps4.PlayStation4FlowHandler() flow.hass = hass - with patch("pyps4_homeassistant.Helper.get_creds", return_value=None): + with patch("pyps4_2ndscreen.Helper.get_creds", return_value=None): result = await flow.async_step_creds({}) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "credential_error" @@ -332,12 +333,10 @@ async def test_credential_abort(hass): async def test_credential_timeout(hass): """Test that Credential Timeout shows error.""" - from pyps4_homeassistant.errors import CredentialTimeout - flow = ps4.PlayStation4FlowHandler() flow.hass = hass - with patch("pyps4_homeassistant.Helper.get_creds", side_effect=CredentialTimeout): + with patch("pyps4_2ndscreen.Helper.get_creds", side_effect=CredentialTimeout): result = await flow.async_step_creds({}) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {"base": "credential_timeout"} @@ -349,8 +348,8 @@ async def test_wrong_pin_error(hass): flow.hass = hass flow.location = MOCK_LOCATION - with patch("pyps4_homeassistant.Helper.link", return_value=(True, False)), patch( - "pyps4_homeassistant.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}] + with patch("pyps4_2ndscreen.Helper.link", return_value=(True, False)), patch( + "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}] ): result = await flow.async_step_link(MOCK_CONFIG) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -364,8 +363,8 @@ async def test_device_connection_error(hass): flow.hass = hass flow.location = MOCK_LOCATION - with patch("pyps4_homeassistant.Helper.link", return_value=(False, True)), patch( - "pyps4_homeassistant.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}] + with patch("pyps4_2ndscreen.Helper.link", return_value=(False, True)), patch( + "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}] ): result = await flow.async_step_link(MOCK_CONFIG) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM diff --git a/tests/components/ps4/test_media_player.py b/tests/components/ps4/test_media_player.py index e4f2033c3cb..7bf93e37777 100644 --- a/tests/components/ps4/test_media_player.py +++ b/tests/components/ps4/test_media_player.py @@ -1,7 +1,7 @@ """Tests for the PS4 media player platform.""" from unittest.mock import MagicMock, patch -from pyps4_homeassistant.credential import get_ddp_message +from pyps4_2ndscreen.credential import get_ddp_message from homeassistant.components import ps4 from homeassistant.components.media_player.const import ( @@ -169,11 +169,6 @@ async def mock_ddp_response(hass, mock_status_data, games=None): await hass.async_block_till_done() -async def test_async_setup_platform_does_nothing(): - """Test setup platform does nothing (Uses config entries only).""" - await ps4.media_player.async_setup_platform(None, None, None) - - async def test_media_player_is_setup_correctly_with_entry(hass): """Test entity is setup correctly with entry correctly.""" mock_entity_id = await setup_mock_component(hass) @@ -295,9 +290,7 @@ async def test_media_attributes_are_loaded(hass): async def test_device_info_is_set_from_status_correctly(hass): """Test that device info is set correctly from status update.""" mock_d_registry = mock_device_registry(hass) - with patch( - "pyps4_homeassistant.ps4.get_status", return_value=MOCK_STATUS_OFF - ), patch(MOCK_SAVE, side_effect=MagicMock()): + with patch("pyps4_2ndscreen.ps4.get_status", return_value=MOCK_STATUS_OFF): mock_entity_id = await setup_mock_component(hass) await hass.async_block_till_done() @@ -447,9 +440,9 @@ async def test_media_stop(hass): async def test_select_source(hass): """Test that select source service calls function with title.""" mock_data = {MOCK_TITLE_ID: MOCK_GAMES_DATA} - with patch( - "pyps4_homeassistant.ps4.get_status", return_value=MOCK_STATUS_IDLE - ), patch(MOCK_LOAD, return_value=mock_data): + with patch("pyps4_2ndscreen.ps4.get_status", return_value=MOCK_STATUS_IDLE), patch( + MOCK_LOAD, return_value=mock_data + ): mock_entity_id = await setup_mock_component(hass) mock_func = "{}{}".format( @@ -473,9 +466,9 @@ async def test_select_source(hass): async def test_select_source_caps(hass): """Test that select source service calls function with upper case title.""" mock_data = {MOCK_TITLE_ID: MOCK_GAMES_DATA} - with patch( - "pyps4_homeassistant.ps4.get_status", return_value=MOCK_STATUS_IDLE - ), patch(MOCK_LOAD, return_value=mock_data): + with patch("pyps4_2ndscreen.ps4.get_status", return_value=MOCK_STATUS_IDLE), patch( + MOCK_LOAD, return_value=mock_data + ): mock_entity_id = await setup_mock_component(hass) mock_func = "{}{}".format( @@ -502,9 +495,9 @@ async def test_select_source_caps(hass): async def test_select_source_id(hass): """Test that select source service calls function with Title ID.""" mock_data = {MOCK_TITLE_ID: MOCK_GAMES_DATA} - with patch( - "pyps4_homeassistant.ps4.get_status", return_value=MOCK_STATUS_IDLE - ), patch(MOCK_LOAD, return_value=mock_data): + with patch("pyps4_2ndscreen.ps4.get_status", return_value=MOCK_STATUS_IDLE), patch( + MOCK_LOAD, return_value=mock_data + ): mock_entity_id = await setup_mock_component(hass) mock_func = "{}{}".format( diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index 19b7566c37c..81e0423a723 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -23,9 +23,9 @@ def create_engine_test(*args, **kwargs): async def test_schema_update_calls(hass): """Test that schema migrations occur in correct order.""" - with patch("sqlalchemy.create_engine", new=create_engine_test), patch( - "homeassistant.components.recorder.migration._apply_update" - ) as update: + with patch( + "homeassistant.components.recorder.create_engine", new=create_engine_test + ), patch("homeassistant.components.recorder.migration._apply_update") as update: await async_setup_component( hass, "recorder", {"recorder": {"db_url": "sqlite://"}} ) diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py index 1c676e203d2..7e06dcd1e5e 100644 --- a/tests/components/recorder/test_purge.py +++ b/tests/components/recorder/test_purge.py @@ -174,5 +174,5 @@ class TestRecorderPurge(unittest.TestCase): self.hass.data[DATA_INSTANCE].block_till_done() assert ( mock_logger.debug.mock_calls[3][1][0] - == "Vacuuming SQLite to free space" + == "Vacuuming SQL DB to free space" ) diff --git a/tests/components/rest/test_sensor.py b/tests/components/rest/test_sensor.py index d117678ccc7..50acb053347 100644 --- a/tests/components/rest/test_sensor.py +++ b/tests/components/rest/test_sensor.py @@ -76,6 +76,40 @@ class TestRestSensorSetup(unittest.TestCase): ) assert 2 == mock_req.call_count + @requests_mock.Mocker() + def test_setup_minimum_resource_template(self, mock_req): + """Test setup with minimum configuration (resource_template).""" + mock_req.get("http://localhost", status_code=200) + with assert_setup_component(1, "sensor"): + assert setup_component( + self.hass, + "sensor", + { + "sensor": { + "platform": "rest", + "resource_template": "http://localhost", + } + }, + ) + assert mock_req.call_count == 2 + + @requests_mock.Mocker() + def test_setup_duplicate_resource(self, mock_req): + """Test setup with duplicate resources.""" + mock_req.get("http://localhost", status_code=200) + with assert_setup_component(0, "sensor"): + assert setup_component( + self.hass, + "sensor", + { + "sensor": { + "platform": "rest", + "resource": "http://localhost", + "resource_template": "http://localhost", + } + }, + ) + @requests_mock.Mocker() def test_setup_get(self, mock_req): """Test setup with valid configuration.""" @@ -152,6 +186,7 @@ class TestRestSensor(unittest.TestCase): self.value_template = template("{{ value_json.key }}") self.value_template.hass = self.hass self.force_update = False + self.resource_template = None self.sensor = rest.RestSensor( self.hass, @@ -162,6 +197,7 @@ class TestRestSensor(unittest.TestCase): self.value_template, [], self.force_update, + self.resource_template, ) def tearDown(self): @@ -222,6 +258,7 @@ class TestRestSensor(unittest.TestCase): None, [], self.force_update, + self.resource_template, ) self.sensor.update() assert "plain_state" == self.sensor.state @@ -242,6 +279,7 @@ class TestRestSensor(unittest.TestCase): None, ["key"], self.force_update, + self.resource_template, ) self.sensor.update() assert "some_json_value" == self.sensor.device_state_attributes["key"] @@ -261,6 +299,7 @@ class TestRestSensor(unittest.TestCase): None, ["key"], self.force_update, + self.resource_template, ) self.sensor.update() assert {} == self.sensor.device_state_attributes @@ -282,6 +321,7 @@ class TestRestSensor(unittest.TestCase): None, ["key"], self.force_update, + self.resource_template, ) self.sensor.update() assert {} == self.sensor.device_state_attributes @@ -303,6 +343,7 @@ class TestRestSensor(unittest.TestCase): None, ["key"], self.force_update, + self.resource_template, ) self.sensor.update() assert {} == self.sensor.device_state_attributes @@ -326,6 +367,7 @@ class TestRestSensor(unittest.TestCase): self.value_template, ["key"], self.force_update, + self.resource_template, ) self.sensor.update() diff --git a/tests/components/rest_command/test_init.py b/tests/components/rest_command/test_init.py index 2f50f34f140..b7ac5a4be8a 100644 --- a/tests/components/rest_command/test_init.py +++ b/tests/components/rest_command/test_init.py @@ -51,6 +51,7 @@ class TestRestCommandComponent: self.config = { rc.DOMAIN: { "get_test": {"url": self.url, "method": "get"}, + "patch_test": {"url": self.url, "method": "patch"}, "post_test": {"url": self.url, "method": "post"}, "put_test": {"url": self.url, "method": "put"}, "delete_test": {"url": self.url, "method": "delete"}, @@ -65,7 +66,7 @@ class TestRestCommandComponent: def test_setup_tests(self): """Set up test config and test it.""" - with assert_setup_component(4): + with assert_setup_component(5): setup_component(self.hass, rc.DOMAIN, self.config) assert self.hass.services.has_service(rc.DOMAIN, "get_test") @@ -75,7 +76,7 @@ class TestRestCommandComponent: def test_rest_command_timeout(self, aioclient_mock): """Call a rest command with timeout.""" - with assert_setup_component(4): + with assert_setup_component(5): setup_component(self.hass, rc.DOMAIN, self.config) aioclient_mock.get(self.url, exc=asyncio.TimeoutError()) @@ -87,7 +88,7 @@ class TestRestCommandComponent: def test_rest_command_aiohttp_error(self, aioclient_mock): """Call a rest command with aiohttp exception.""" - with assert_setup_component(4): + with assert_setup_component(5): setup_component(self.hass, rc.DOMAIN, self.config) aioclient_mock.get(self.url, exc=aiohttp.ClientError()) @@ -99,7 +100,7 @@ class TestRestCommandComponent: def test_rest_command_http_error(self, aioclient_mock): """Call a rest command with status code 400.""" - with assert_setup_component(4): + with assert_setup_component(5): setup_component(self.hass, rc.DOMAIN, self.config) aioclient_mock.get(self.url, status=400) @@ -114,7 +115,7 @@ class TestRestCommandComponent: data = {"username": "test", "password": "123456"} self.config[rc.DOMAIN]["get_test"].update(data) - with assert_setup_component(4): + with assert_setup_component(5): setup_component(self.hass, rc.DOMAIN, self.config) aioclient_mock.get(self.url, content=b"success") @@ -129,7 +130,7 @@ class TestRestCommandComponent: data = {"payload": "test"} self.config[rc.DOMAIN]["post_test"].update(data) - with assert_setup_component(4): + with assert_setup_component(5): setup_component(self.hass, rc.DOMAIN, self.config) aioclient_mock.post(self.url, content=b"success") @@ -142,7 +143,7 @@ class TestRestCommandComponent: def test_rest_command_get(self, aioclient_mock): """Call a rest command with get.""" - with assert_setup_component(4): + with assert_setup_component(5): setup_component(self.hass, rc.DOMAIN, self.config) aioclient_mock.get(self.url, content=b"success") @@ -154,7 +155,7 @@ class TestRestCommandComponent: def test_rest_command_delete(self, aioclient_mock): """Call a rest command with delete.""" - with assert_setup_component(4): + with assert_setup_component(5): setup_component(self.hass, rc.DOMAIN, self.config) aioclient_mock.delete(self.url, content=b"success") @@ -164,12 +165,28 @@ class TestRestCommandComponent: assert len(aioclient_mock.mock_calls) == 1 + def test_rest_command_patch(self, aioclient_mock): + """Call a rest command with patch.""" + data = {"payload": "data"} + self.config[rc.DOMAIN]["patch_test"].update(data) + + with assert_setup_component(5): + setup_component(self.hass, rc.DOMAIN, self.config) + + aioclient_mock.patch(self.url, content=b"success") + + self.hass.services.call(rc.DOMAIN, "patch_test", {}) + self.hass.block_till_done() + + assert len(aioclient_mock.mock_calls) == 1 + assert aioclient_mock.mock_calls[0][2] == b"data" + def test_rest_command_post(self, aioclient_mock): """Call a rest command with post.""" data = {"payload": "data"} self.config[rc.DOMAIN]["post_test"].update(data) - with assert_setup_component(4): + with assert_setup_component(5): setup_component(self.hass, rc.DOMAIN, self.config) aioclient_mock.post(self.url, content=b"success") @@ -185,7 +202,7 @@ class TestRestCommandComponent: data = {"payload": "data"} self.config[rc.DOMAIN]["put_test"].update(data) - with assert_setup_component(4): + with assert_setup_component(5): setup_component(self.hass, rc.DOMAIN, self.config) aioclient_mock.put(self.url, content=b"success") diff --git a/tests/components/rflink/test_binary_sensor.py b/tests/components/rflink/test_binary_sensor.py index 442ebebdffe..d1fdec579c9 100644 --- a/tests/components/rflink/test_binary_sensor.py +++ b/tests/components/rflink/test_binary_sensor.py @@ -8,14 +8,13 @@ from datetime import timedelta from unittest.mock import patch from homeassistant.components.rflink import CONF_RECONNECT_INTERVAL - -import homeassistant.core as ha from homeassistant.const import ( EVENT_STATE_CHANGED, - STATE_ON, STATE_OFF, + STATE_ON, STATE_UNAVAILABLE, ) +import homeassistant.core as ha import homeassistant.util.dt as dt_util from tests.common import async_fire_time_changed diff --git a/tests/components/rflink/test_cover.py b/tests/components/rflink/test_cover.py index 858258e7efd..dc286502068 100644 --- a/tests/components/rflink/test_cover.py +++ b/tests/components/rflink/test_cover.py @@ -9,13 +9,13 @@ import logging from homeassistant.components.rflink import EVENT_BUTTON_PRESSED from homeassistant.const import ( - SERVICE_OPEN_COVER, - SERVICE_CLOSE_COVER, - STATE_OPEN, - STATE_CLOSED, ATTR_ENTITY_ID, + SERVICE_CLOSE_COVER, + SERVICE_OPEN_COVER, + STATE_CLOSED, + STATE_OPEN, ) -from homeassistant.core import callback, State, CoreState +from homeassistant.core import CoreState, State, callback from tests.common import mock_restore_cache from tests.components.rflink.test_init import mock_rflink diff --git a/tests/components/rflink/test_init.py b/tests/components/rflink/test_init.py index 5e821fbdeb2..df96b0e87ae 100644 --- a/tests/components/rflink/test_init.py +++ b/tests/components/rflink/test_init.py @@ -5,14 +5,14 @@ from unittest.mock import Mock from homeassistant.bootstrap import async_setup_component from homeassistant.components.rflink import ( CONF_RECONNECT_INTERVAL, - SERVICE_SEND_COMMAND, - RflinkCommand, - TMP_ENTITY, DATA_ENTITY_LOOKUP, EVENT_KEY_COMMAND, EVENT_KEY_SENSOR, + SERVICE_SEND_COMMAND, + TMP_ENTITY, + RflinkCommand, ) -from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_STOP_COVER +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_STOP_COVER, SERVICE_TURN_OFF async def mock_rflink( @@ -46,7 +46,9 @@ async def mock_rflink( return transport, protocol mock_create = Mock(wraps=create_rflink_connection) - monkeypatch.setattr("rflink.protocol.create_rflink_connection", mock_create) + monkeypatch.setattr( + "homeassistant.components.rflink.create_rflink_connection", mock_create + ) await async_setup_component(hass, "rflink", config) await async_setup_component(hass, domain, config) diff --git a/tests/components/rflink/test_light.py b/tests/components/rflink/test_light.py index ba4122724ce..b22730a3310 100644 --- a/tests/components/rflink/test_light.py +++ b/tests/components/rflink/test_light.py @@ -11,10 +11,10 @@ from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, - STATE_ON, STATE_OFF, + STATE_ON, ) -from homeassistant.core import callback, State, CoreState +from homeassistant.core import CoreState, State, callback from tests.common import mock_restore_cache from tests.components.rflink.test_init import mock_rflink @@ -313,10 +313,10 @@ async def test_signal_repetitions_cancelling(hass, monkeypatch): await hass.async_block_till_done() - assert protocol.send_command_ack.call_args_list[0][0][1] == "on" - assert protocol.send_command_ack.call_args_list[1][0][1] == "off" - assert protocol.send_command_ack.call_args_list[2][0][1] == "off" - assert protocol.send_command_ack.call_args_list[3][0][1] == "off" + assert protocol.send_command_ack.call_args_list[0][0][1] == "off" + assert protocol.send_command_ack.call_args_list[1][0][1] == "on" + assert protocol.send_command_ack.call_args_list[2][0][1] == "on" + assert protocol.send_command_ack.call_args_list[3][0][1] == "on" async def test_type_toggle(hass, monkeypatch): diff --git a/tests/components/rflink/test_sensor.py b/tests/components/rflink/test_sensor.py index bf6c9e03fbc..3fea3ef6ef4 100644 --- a/tests/components/rflink/test_sensor.py +++ b/tests/components/rflink/test_sensor.py @@ -7,12 +7,13 @@ automatic sensor creation. from homeassistant.components.rflink import ( CONF_RECONNECT_INTERVAL, - TMP_ENTITY, DATA_ENTITY_LOOKUP, EVENT_KEY_COMMAND, EVENT_KEY_SENSOR, + TMP_ENTITY, ) from homeassistant.const import STATE_UNKNOWN + from tests.components.rflink.test_init import mock_rflink DOMAIN = "sensor" diff --git a/tests/components/rflink/test_switch.py b/tests/components/rflink/test_switch.py index 4503f1a232f..d1fced33208 100644 --- a/tests/components/rflink/test_switch.py +++ b/tests/components/rflink/test_switch.py @@ -10,10 +10,10 @@ from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, - STATE_ON, STATE_OFF, + STATE_ON, ) -from homeassistant.core import callback, State, CoreState +from homeassistant.core import CoreState, State, callback from tests.common import mock_restore_cache from tests.components.rflink.test_init import mock_rflink diff --git a/tests/components/rfxtrx/test_cover.py b/tests/components/rfxtrx/test_cover.py index 9fa71bdab67..d85ea5cf6f4 100644 --- a/tests/components/rfxtrx/test_cover.py +++ b/tests/components/rfxtrx/test_cover.py @@ -1,10 +1,11 @@ """The tests for the Rfxtrx cover platform.""" import unittest +import RFXtrx as rfxtrxmod import pytest -from homeassistant.setup import setup_component from homeassistant.components import rfxtrx as rfxtrx_core +from homeassistant.setup import setup_component from tests.common import get_test_home_assistant, mock_component @@ -142,8 +143,6 @@ class TestCoverRfxtrx(unittest.TestCase): }, ) - import RFXtrx as rfxtrxmod - rfxtrx_core.RFXOBJECT = rfxtrxmod.Core( "", transport_protocol=rfxtrxmod.DummyTransport ) diff --git a/tests/components/rfxtrx/test_init.py b/tests/components/rfxtrx/test_init.py index ac046b99897..ec457af7575 100644 --- a/tests/components/rfxtrx/test_init.py +++ b/tests/components/rfxtrx/test_init.py @@ -4,9 +4,10 @@ import unittest import pytest +from homeassistant.components import rfxtrx as rfxtrx from homeassistant.core import callback from homeassistant.setup import setup_component -from homeassistant.components import rfxtrx as rfxtrx + from tests.common import get_test_home_assistant diff --git a/tests/components/rfxtrx/test_light.py b/tests/components/rfxtrx/test_light.py index f3a6bcab1b1..a5230cc5f3c 100644 --- a/tests/components/rfxtrx/test_light.py +++ b/tests/components/rfxtrx/test_light.py @@ -1,10 +1,11 @@ """The tests for the Rfxtrx light platform.""" import unittest +import RFXtrx as rfxtrxmod import pytest -from homeassistant.setup import setup_component from homeassistant.components import rfxtrx as rfxtrx_core +from homeassistant.setup import setup_component from tests.common import get_test_home_assistant, mock_component @@ -109,8 +110,6 @@ class TestLightRfxtrx(unittest.TestCase): }, ) - import RFXtrx as rfxtrxmod - rfxtrx_core.RFXOBJECT = rfxtrxmod.Core( "", transport_protocol=rfxtrxmod.DummyTransport ) diff --git a/tests/components/rfxtrx/test_sensor.py b/tests/components/rfxtrx/test_sensor.py index 3f0cfead3e4..652c823e0cf 100644 --- a/tests/components/rfxtrx/test_sensor.py +++ b/tests/components/rfxtrx/test_sensor.py @@ -3,9 +3,9 @@ import unittest import pytest -from homeassistant.setup import setup_component from homeassistant.components import rfxtrx as rfxtrx_core from homeassistant.const import TEMP_CELSIUS +from homeassistant.setup import setup_component from tests.common import get_test_home_assistant, mock_component diff --git a/tests/components/rfxtrx/test_switch.py b/tests/components/rfxtrx/test_switch.py index dc955a198a7..66da197aae8 100644 --- a/tests/components/rfxtrx/test_switch.py +++ b/tests/components/rfxtrx/test_switch.py @@ -1,17 +1,18 @@ -"""The tests for the Rfxtrx switch platform.""" +"""The tests for the RFXtrx switch platform.""" import unittest +import RFXtrx as rfxtrxmod import pytest -from homeassistant.setup import setup_component from homeassistant.components import rfxtrx as rfxtrx_core +from homeassistant.setup import setup_component from tests.common import get_test_home_assistant, mock_component @pytest.mark.skipif("os.environ.get('RFXTRX') != 'RUN'") class TestSwitchRfxtrx(unittest.TestCase): - """Test the Rfxtrx switch platform.""" + """Test the RFXtrx switch platform.""" def setUp(self): """Set up things to be run when tests are started.""" @@ -166,8 +167,6 @@ class TestSwitchRfxtrx(unittest.TestCase): }, ) - import RFXtrx as rfxtrxmod - rfxtrx_core.RFXOBJECT = rfxtrxmod.Core( "", transport_protocol=rfxtrxmod.DummyTransport ) @@ -200,8 +199,6 @@ class TestSwitchRfxtrx(unittest.TestCase): }, ) - import RFXtrx as rfxtrxmod - rfxtrx_core.RFXOBJECT = rfxtrxmod.Core( "", transport_protocol=rfxtrxmod.DummyTransport ) diff --git a/tests/components/sensor/test_device_condition.py b/tests/components/sensor/test_device_condition.py new file mode 100644 index 00000000000..f3ff15c3ad9 --- /dev/null +++ b/tests/components/sensor/test_device_condition.py @@ -0,0 +1,370 @@ +"""The test for sensor device automation.""" +import pytest + +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.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + 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 + + +@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 serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_conditions(hass, device_reg, entity_reg): + """Test we get the expected conditions from a sensor.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + 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")}, + ) + for device_class in DEVICE_CLASSES: + entity_reg.async_get_or_create( + DOMAIN, + "test", + platform.ENTITIES[device_class].unique_id, + device_id=device_entry.id, + ) + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + expected_conditions = [ + { + "condition": "device", + "domain": DOMAIN, + "type": condition["type"], + "device_id": device_entry.id, + "entity_id": platform.ENTITIES[device_class].entity_id, + } + for device_class in DEVICE_CLASSES + for condition in ENTITY_CONDITIONS[device_class] + if device_class != "none" + ] + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + assert conditions == expected_conditions + + +async def test_get_condition_capabilities(hass, device_reg, entity_reg): + """Test we get the expected capabilities from a sensor condition.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + 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", + platform.ENTITIES["battery"].unique_id, + device_id=device_entry.id, + ) + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + expected_capabilities = { + "extra_fields": [ + { + "description": {"suffix": "%"}, + "name": "above", + "optional": True, + "type": "float", + }, + { + "description": {"suffix": "%"}, + "name": "below", + "optional": True, + "type": "float", + }, + ] + } + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + assert len(conditions) == 1 + for condition in conditions: + capabilities = await async_get_device_automation_capabilities( + hass, "condition", condition + ) + assert capabilities == expected_capabilities + + +async def test_get_condition_capabilities_none(hass, device_reg, entity_reg): + """Test we get the expected capabilities from a sensor condition.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + conditions = [ + { + "condition": "device", + "device_id": "8770c43885354d5fa27604db6817f63f", + "domain": "sensor", + "entity_id": "sensor.beer", + "type": "is_battery_level", + }, + { + "condition": "device", + "device_id": "8770c43885354d5fa27604db6817f63f", + "domain": "sensor", + "entity_id": platform.ENTITIES["none"].entity_id, + "type": "is_battery_level", + }, + ] + + expected_capabilities = {} + for condition in conditions: + capabilities = await async_get_device_automation_capabilities( + hass, "condition", condition + ) + assert capabilities == expected_capabilities + + +async def test_if_state_not_above_below(hass, calls, caplog): + """Test for bad value conditions.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + sensor1 = platform.ENTITIES["battery"] + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": sensor1.entity_id, + "type": "is_battery_level", + } + ], + "action": {"service": "test.automation"}, + } + ] + }, + ) + assert "must contain at least one of below, above" in caplog.text + + +async def test_if_state_above(hass, calls): + """Test for value conditions.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + sensor1 = platform.ENTITIES["battery"] + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": sensor1.entity_id, + "type": "is_battery_level", + "above": 10, + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "{{ trigger.%s }}" + % "}} - {{ trigger.".join(("platform", "event.event_type")) + }, + }, + } + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(sensor1.entity_id).state == STATE_UNKNOWN + assert len(calls) == 0 + + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 0 + + hass.states.async_set(sensor1.entity_id, 9) + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 0 + + hass.states.async_set(sensor1.entity_id, 11) + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "event - test_event1" + + +async def test_if_state_below(hass, calls): + """Test for value conditions.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + sensor1 = platform.ENTITIES["battery"] + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": sensor1.entity_id, + "type": "is_battery_level", + "below": 10, + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "{{ trigger.%s }}" + % "}} - {{ trigger.".join(("platform", "event.event_type")) + }, + }, + } + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(sensor1.entity_id).state == STATE_UNKNOWN + assert len(calls) == 0 + + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 0 + + hass.states.async_set(sensor1.entity_id, 11) + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 0 + + hass.states.async_set(sensor1.entity_id, 9) + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "event - test_event1" + + +async def test_if_state_between(hass, calls): + """Test for value conditions.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + sensor1 = platform.ENTITIES["battery"] + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": sensor1.entity_id, + "type": "is_battery_level", + "above": 10, + "below": 20, + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "{{ trigger.%s }}" + % "}} - {{ trigger.".join(("platform", "event.event_type")) + }, + }, + } + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(sensor1.entity_id).state == STATE_UNKNOWN + assert len(calls) == 0 + + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 0 + + hass.states.async_set(sensor1.entity_id, 9) + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 0 + + hass.states.async_set(sensor1.entity_id, 11) + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "event - test_event1" + + hass.states.async_set(sensor1.entity_id, 21) + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 1 + + hass.states.async_set(sensor1.entity_id, 19) + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "event - test_event1" diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py index 45452dc84a0..b7a921fff18 100644 --- a/tests/components/sensor/test_device_trigger.py +++ b/tests/components/sensor/test_device_trigger.py @@ -74,26 +74,84 @@ async def test_get_triggers(hass, device_reg, entity_reg): if device_class != "none" ] triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert len(triggers) == 8 assert triggers == expected_triggers async def test_get_trigger_capabilities(hass, device_reg, entity_reg): - """Test we get the expected capabilities from a binary_sensor trigger.""" + """Test we get the expected capabilities from a sensor trigger.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + 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) + entity_reg.async_get_or_create( + DOMAIN, + "test", + platform.ENTITIES["battery"].unique_id, + device_id=device_entry.id, + ) + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + expected_capabilities = { "extra_fields": [ - {"name": "above", "optional": True, "type": "float"}, - {"name": "below", "optional": True, "type": "float"}, + { + "description": {"suffix": "%"}, + "name": "above", + "optional": True, + "type": "float", + }, + { + "description": {"suffix": "%"}, + "name": "below", + "optional": True, + "type": "float", + }, {"name": "for", "optional": True, "type": "positive_time_period_dict"}, ] } triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert len(triggers) == 1 + for trigger in triggers: + capabilities = await async_get_device_automation_capabilities( + hass, "trigger", trigger + ) + assert capabilities == expected_capabilities + + +async def test_get_trigger_capabilities_none(hass, device_reg, entity_reg): + """Test we get the expected capabilities from a sensor trigger.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + triggers = [ + { + "platform": "device", + "device_id": "8770c43885354d5fa27604db6817f63f", + "domain": "sensor", + "entity_id": "sensor.beer", + "type": "is_battery_level", + }, + { + "platform": "device", + "device_id": "8770c43885354d5fa27604db6817f63f", + "domain": "sensor", + "entity_id": platform.ENTITIES["none"].entity_id, + "type": "is_battery_level", + }, + ] + + expected_capabilities = {} for trigger in triggers: capabilities = await async_get_device_automation_capabilities( hass, "trigger", trigger diff --git a/tests/components/smartthings/test_config_flow.py b/tests/components/smartthings/test_config_flow.py index fce0129a7bf..521f1c6a6a8 100644 --- a/tests/components/smartthings/test_config_flow.py +++ b/tests/components/smartthings/test_config_flow.py @@ -205,6 +205,8 @@ async def test_cloudhook_app_created_then_show_wait_form( hass, app, app_oauth_client, smartthings_mock ): """Test SmartApp is created with a cloudhoko and shows wait form.""" + hass.config.components.add("cloud") + # Unload the endpoint so we can reload it under the cloud. await smartapp.unload_smartapp_endpoint(hass) diff --git a/tests/components/smartthings/test_init.py b/tests/components/smartthings/test_init.py index 9749ab9bb71..b8cd65f5a0b 100644 --- a/tests/components/smartthings/test_init.py +++ b/tests/components/smartthings/test_init.py @@ -268,6 +268,7 @@ async def test_remove_entry(hass, config_entry, smartthings_mock): async def test_remove_entry_cloudhook(hass, config_entry, smartthings_mock): """Test that the installed app, app, and cloudhook are removed up.""" + hass.config.components.add("cloud") # Arrange config_entry.add_to_hass(hass) hass.data[DOMAIN][CONF_CLOUDHOOK_URL] = "https://test.cloud" diff --git a/tests/components/solarlog/__init__.py b/tests/components/solarlog/__init__.py new file mode 100644 index 00000000000..9074cab8416 --- /dev/null +++ b/tests/components/solarlog/__init__.py @@ -0,0 +1 @@ +"""Tests for the solarlog integration.""" diff --git a/tests/components/solarlog/test_config_flow.py b/tests/components/solarlog/test_config_flow.py new file mode 100644 index 00000000000..86f3b05d975 --- /dev/null +++ b/tests/components/solarlog/test_config_flow.py @@ -0,0 +1,135 @@ +"""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.components.solarlog import config_flow +from homeassistant.components.solarlog.const import DEFAULT_HOST, DOMAIN +from homeassistant.const import CONF_HOST, CONF_NAME + +from tests.common import MockConfigEntry, mock_coro + +NAME = "Solarlog test 1 2 3" +HOST = "http://1.1.1.1" + + +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.solarlog.config_flow.SolarLogConfigFlow._test_connection", + return_value=mock_coro({"title": "solarlog test 1 2 3"}), + ), patch( + "homeassistant.components.solarlog.async_setup", return_value=mock_coro(True) + ) as mock_setup, patch( + "homeassistant.components.solarlog.async_setup_entry", + return_value=mock_coro(True), + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {"host": HOST, "name": NAME} + ) + + assert result2["type"] == "create_entry" + assert result2["title"] == "solarlog_test_1_2_3" + assert result2["data"] == {"host": "http://1.1.1.1"} + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.fixture(name="test_connect") +def mock_controller(): + """Mock a successfull _host_in_configuration_exists.""" + with patch( + "homeassistant.components.solarlog.config_flow.SolarLogConfigFlow._test_connection", + side_effect=lambda *_: mock_coro(True), + ): + yield + + +def init_config_flow(hass): + """Init a configuration flow.""" + flow = config_flow.SolarLogConfigFlow() + flow.hass = hass + return flow + + +async def test_user(hass, test_connect): + """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" + + # tets with all provided + result = await flow.async_step_user({CONF_NAME: NAME, CONF_HOST: HOST}) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "solarlog_test_1_2_3" + assert result["data"][CONF_HOST] == HOST + + +async def test_import(hass, test_connect): + """Test import step.""" + flow = init_config_flow(hass) + + # import with only host + result = await flow.async_step_import({CONF_HOST: HOST}) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "solarlog" + assert result["data"][CONF_HOST] == HOST + + # import with only name + result = await flow.async_step_import({CONF_NAME: NAME}) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "solarlog_test_1_2_3" + assert result["data"][CONF_HOST] == DEFAULT_HOST + + # import with host and name + result = await flow.async_step_import({CONF_HOST: HOST, CONF_NAME: NAME}) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "solarlog_test_1_2_3" + assert result["data"][CONF_HOST] == HOST + + +async def test_abort_if_already_setup(hass, test_connect): + """Test we abort if the device is already setup.""" + flow = init_config_flow(hass) + MockConfigEntry( + domain="solarlog", data={CONF_NAME: NAME, CONF_HOST: HOST} + ).add_to_hass(hass) + + # Should fail, same HOST different NAME (default) + result = await flow.async_step_import( + {CONF_HOST: HOST, CONF_NAME: "solarlog_test_7_8_9"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + # Should fail, same HOST and NAME + result = await flow.async_step_user({CONF_HOST: HOST, CONF_NAME: NAME}) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {CONF_HOST: "already_configured"} + + # SHOULD pass, diff HOST (without http://), different NAME + result = await flow.async_step_import( + {CONF_HOST: "2.2.2.2", CONF_NAME: "solarlog_test_7_8_9"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "solarlog_test_7_8_9" + assert result["data"][CONF_HOST] == "http://2.2.2.2" + + # SHOULD pass, diff HOST, same NAME + result = await flow.async_step_import( + {CONF_HOST: "http://2.2.2.2", CONF_NAME: NAME} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "solarlog_test_1_2_3" + assert result["data"][CONF_HOST] == "http://2.2.2.2" diff --git a/tests/components/somfy/test_config_flow.py b/tests/components/somfy/test_config_flow.py index cbc3784e3f5..d42e7b8e367 100644 --- a/tests/components/somfy/test_config_flow.py +++ b/tests/components/somfy/test_config_flow.py @@ -1,19 +1,35 @@ """Tests for the Somfy config flow.""" import asyncio -from unittest.mock import Mock, patch +from unittest.mock import patch -from pymfy.api.somfy_api import SomfyApi +import pytest -from homeassistant import data_entry_flow +from homeassistant import data_entry_flow, setup, config_entries from homeassistant.components.somfy import config_flow, DOMAIN -from homeassistant.components.somfy.config_flow import register_flow_implementation -from tests.common import MockConfigEntry, mock_coro +from homeassistant.helpers import config_entry_oauth2_flow + +from tests.common import MockConfigEntry CLIENT_SECRET_VALUE = "5678" CLIENT_ID_VALUE = "1234" -AUTH_URL = "http://somfy.com" + +@pytest.fixture() +async def mock_impl(hass): + """Mock implementation.""" + await setup.async_setup_component(hass, "http", {}) + + impl = config_entry_oauth2_flow.LocalOAuth2Implementation( + hass, + DOMAIN, + CLIENT_ID_VALUE, + CLIENT_SECRET_VALUE, + "https://accounts.somfy.com/oauth/oauth/v2/auth", + "https://accounts.somfy.com/oauth/oauth/v2/token", + ) + config_flow.SomfyFlowHandler.async_register_implementation(hass, impl) + return impl async def test_abort_if_no_configuration(hass): @@ -30,47 +46,84 @@ async def test_abort_if_existing_entry(hass): flow = config_flow.SomfyFlowHandler() flow.hass = hass MockConfigEntry(domain=DOMAIN).add_to_hass(hass) - result = await flow.async_step_import() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_setup" + result = await flow.async_step_user() assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_setup" -async def test_full_flow(hass): - """Check classic use case.""" - hass.data[DOMAIN] = {} - register_flow_implementation(hass, CLIENT_ID_VALUE, CLIENT_SECRET_VALUE) - flow = config_flow.SomfyFlowHandler() - flow.hass = hass - hass.config.api = Mock(base_url="https://example.com") - flow._get_authorization_url = Mock(return_value=mock_coro((AUTH_URL, "state"))) - result = await flow.async_step_import() +async def test_full_flow(hass, aiohttp_client, aioclient_mock): + """Check full flow.""" + assert await setup.async_setup_component( + hass, + "somfy", + { + "somfy": { + "client_id": CLIENT_ID_VALUE, + "client_secret": CLIENT_SECRET_VALUE, + }, + "http": {"base_url": "https://example.com"}, + }, + ) + + result = await hass.config_entries.flow.async_init( + "somfy", context={"source": config_entries.SOURCE_USER} + ) + 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"] == AUTH_URL - result = await flow.async_step_auth("my_super_code") - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP_DONE - assert result["step_id"] == "creation" - assert flow.code == "my_super_code" - with patch.object( - SomfyApi, "request_token", return_value={"access_token": "super_token"} - ): - result = await flow.async_step_creation() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["data"]["refresh_args"] == { - "client_id": CLIENT_ID_VALUE, - "client_secret": CLIENT_SECRET_VALUE, + assert result["url"] == ( + "https://accounts.somfy.com/oauth/oauth/v2/auth" + f"?response_type=code&client_id={CLIENT_ID_VALUE}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}" + ) + + client = await aiohttp_client(hass.http.app) + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.post( + "https://accounts.somfy.com/oauth/oauth/v2/token", + json={ + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + }, + ) + + with patch("homeassistant.components.somfy.api.ConfigEntrySomfyApi"): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["data"]["auth_implementation"] == "somfy" + + result["data"]["token"].pop("expires_at") + assert result["data"]["token"] == { + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, } - assert result["title"] == "Somfy" - assert result["data"]["token"] == {"access_token": "super_token"} + + assert "somfy" in hass.config.components + entry = hass.config_entries.async_entries("somfy")[0] + assert entry.state == config_entries.ENTRY_STATE_LOADED + + assert await hass.config_entries.async_unload(entry.entry_id) + assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED -async def test_abort_if_authorization_timeout(hass): +async def test_abort_if_authorization_timeout(hass, mock_impl): """Check Somfy authorization timeout.""" flow = config_flow.SomfyFlowHandler() flow.hass = hass - flow._get_authorization_url = Mock(side_effect=asyncio.TimeoutError) - result = await flow.async_step_auth() + + with patch.object( + mock_impl, "async_generate_authorize_url", side_effect=asyncio.TimeoutError + ): + result = await flow.async_step_user() + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "authorize_url_timeout" diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index 135b7279244..e0257585ad5 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -2,8 +2,8 @@ from asynctest.mock import Mock, patch as patch import pytest -from homeassistant.components.sonos import DOMAIN from homeassistant.components.media_player import DOMAIN as MP_DOMAIN +from homeassistant.components.sonos import DOMAIN from homeassistant.const import CONF_HOSTS from tests.common import MockConfigEntry diff --git a/tests/components/sonos/test_init.py b/tests/components/sonos/test_init.py index b9ceaa49639..86ec90f32b8 100644 --- a/tests/components/sonos/test_init.py +++ b/tests/components/sonos/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 sonos +from homeassistant.setup import async_setup_component from tests.common import mock_coro diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py index ec5861b536a..d21d3f01792 100644 --- a/tests/components/sonos/test_media_player.py +++ b/tests/components/sonos/test_media_player.py @@ -1,5 +1,5 @@ """Tests for the Sonos Media Player platform.""" -from homeassistant.components.sonos import media_player, DOMAIN +from homeassistant.components.sonos import DOMAIN, media_player from homeassistant.setup import async_setup_component diff --git a/tests/components/sql/test_sensor.py b/tests/components/sql/test_sensor.py index 28357ab34b5..afc7bebea09 100644 --- a/tests/components/sql/test_sensor.py +++ b/tests/components/sql/test_sensor.py @@ -1,11 +1,12 @@ """The test for the sql sensor platform.""" import unittest + import pytest import voluptuous as vol from homeassistant.components.sql.sensor import validate_sql_select -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 diff --git a/tests/components/statsd/test_init.py b/tests/components/statsd/test_init.py index 6deb40c082d..4c7e9d29fee 100644 --- a/tests/components/statsd/test_init.py +++ b/tests/components/statsd/test_init.py @@ -2,15 +2,15 @@ import unittest from unittest import mock +import pytest import voluptuous as vol -from homeassistant.setup import setup_component -import homeassistant.core as ha import homeassistant.components.statsd as statsd -from homeassistant.const import STATE_ON, STATE_OFF, EVENT_STATE_CHANGED +from homeassistant.const import 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 -import pytest class TestStatsd(unittest.TestCase): diff --git a/tests/components/stream/common.py b/tests/components/stream/common.py index 32ab36dc477..4c34ec0b341 100644 --- a/tests/components/stream/common.py +++ b/tests/components/stream/common.py @@ -1,8 +1,11 @@ """Collection of test helpers.""" import io +import av +import numpy as np + from homeassistant.components.stream import Stream -from homeassistant.components.stream.const import DOMAIN, ATTR_STREAMS +from homeassistant.components.stream.const import ATTR_STREAMS, DOMAIN def generate_h264_video(): @@ -11,8 +14,6 @@ def generate_h264_video(): See: http://docs.mikeboers.com/pyav/develop/cookbook/numpy.html """ - import numpy as np - import av duration = 5 fps = 24 diff --git a/tests/components/stream/test_hls.py b/tests/components/stream/test_hls.py index ac564ce7553..293f8d1e4cf 100644 --- a/tests/components/stream/test_hls.py +++ b/tests/components/stream/test_hls.py @@ -4,8 +4,8 @@ from urllib.parse import urlparse import pytest -from homeassistant.setup import async_setup_component from homeassistant.components.stream import request_stream +from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from tests.common import async_fire_time_changed diff --git a/tests/components/stream/test_init.py b/tests/components/stream/test_init.py index 80d703c801b..0661a5a9738 100644 --- a/tests/components/stream/test_init.py +++ b/tests/components/stream/test_init.py @@ -1,16 +1,16 @@ """The tests for stream.""" -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch import pytest -from homeassistant.const import CONF_FILENAME from homeassistant.components.stream.const import ( + ATTR_STREAMS, + CONF_LOOKBACK, + CONF_STREAM_SOURCE, DOMAIN, SERVICE_RECORD, - CONF_STREAM_SOURCE, - CONF_LOOKBACK, - ATTR_STREAMS, ) +from homeassistant.const import CONF_FILENAME from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component diff --git a/tests/components/stream/test_recorder.py b/tests/components/stream/test_recorder.py index dce8b95d07c..95eeeecf7ad 100644 --- a/tests/components/stream/test_recorder.py +++ b/tests/components/stream/test_recorder.py @@ -2,11 +2,12 @@ from datetime import timedelta from io import BytesIO from unittest.mock import patch + import pytest -from homeassistant.setup import async_setup_component from homeassistant.components.stream.core import Segment from homeassistant.components.stream.recorder import recorder_save_worker +from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from tests.common import async_fire_time_changed diff --git a/tests/components/switch/test_device_condition.py b/tests/components/switch/test_device_condition.py index e2ce5a373d2..e673527fada 100644 --- a/tests/components/switch/test_device_condition.py +++ b/tests/components/switch/test_device_condition.py @@ -1,20 +1,22 @@ """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 homeassistant.components.automation as automation -from homeassistant.components.device_automation import ( - _async_get_device_automations as async_get_device_automations, -) from homeassistant.helpers import device_registry +import homeassistant.util.dt as dt_util from tests.common import ( MockConfigEntry, async_mock_service, mock_device_registry, mock_registry, + async_get_device_automations, + async_get_device_automation_capabilities, ) @@ -65,6 +67,28 @@ async def test_get_conditions(hass, device_reg, entity_reg): assert conditions == expected_conditions +async def test_get_condition_capabilities(hass, device_reg, entity_reg): + """Test we get the expected capabilities from a switch condition.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_capabilities = { + "extra_fields": [ + {"name": "for", "optional": True, "type": "positive_time_period_dict"} + ] + } + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + for condition in conditions: + capabilities = await async_get_device_automation_capabilities( + hass, "condition", condition + ) + assert capabilities == expected_capabilities + + async def test_if_state(hass, calls): """Test for turn_on and turn_off conditions.""" platform = getattr(hass.components, f"test.{DOMAIN}") @@ -136,3 +160,73 @@ async def test_if_state(hass, calls): await hass.async_block_till_done() assert len(calls) == 2 assert calls[1].data["some"] == "is_off event - test_event2" + + +async def test_if_fires_on_for_condition(hass, calls): + """Test for firing if condition is on with delay.""" + point1 = dt_util.utcnow() + point2 = point1 + timedelta(seconds=10) + point3 = point2 + timedelta(seconds=10) + + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + ent1, ent2, ent3 = platform.ENTITIES + + with patch("homeassistant.core.dt_util.utcnow") as mock_utcnow: + mock_utcnow.return_value = point1 + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "is_off", + "for": {"seconds": 5}, + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "is_off {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ("platform", "event.event_type") + ) + }, + }, + } + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_ON + assert len(calls) == 0 + + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 0 + + # Time travel 10 secs into the future + mock_utcnow.return_value = point2 + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 0 + + hass.states.async_set(ent1.entity_id, STATE_OFF) + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 0 + + # Time travel 20 secs into the future + mock_utcnow.return_value = point3 + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "is_off event - test_event1" diff --git a/tests/components/switch/test_reproduce_state.py b/tests/components/switch/test_reproduce_state.py new file mode 100644 index 00000000000..4b6db84bfdd --- /dev/null +++ b/tests/components/switch/test_reproduce_state.py @@ -0,0 +1,50 @@ +"""Test reproduce state for Switch.""" +from homeassistant.core import State + +from tests.common import async_mock_service + + +async def test_reproducing_states(hass, caplog): + """Test reproducing Switch states.""" + hass.states.async_set("switch.entity_off", "off", {}) + hass.states.async_set("switch.entity_on", "on", {}) + + turn_on_calls = async_mock_service(hass, "switch", "turn_on") + turn_off_calls = async_mock_service(hass, "switch", "turn_off") + + # These calls should do nothing as entities already in desired state + await hass.helpers.state.async_reproduce_state( + [State("switch.entity_off", "off"), State("switch.entity_on", "on", {})], + blocking=True, + ) + + assert len(turn_on_calls) == 0 + assert len(turn_off_calls) == 0 + + # Test invalid state is handled + await hass.helpers.state.async_reproduce_state( + [State("switch.entity_off", "not_supported")], blocking=True + ) + + assert "not_supported" in caplog.text + assert len(turn_on_calls) == 0 + assert len(turn_off_calls) == 0 + + # Make sure correct services are called + await hass.helpers.state.async_reproduce_state( + [ + State("switch.entity_on", "off"), + State("switch.entity_off", "on", {}), + # Should not raise + State("switch.non_existing", "on"), + ], + blocking=True, + ) + + assert len(turn_on_calls) == 1 + assert turn_on_calls[0].domain == "switch" + assert turn_on_calls[0].data == {"entity_id": "switch.entity_off"} + + assert len(turn_off_calls) == 1 + assert turn_off_calls[0].domain == "switch" + assert turn_off_calls[0].data == {"entity_id": "switch.entity_on"} diff --git a/tests/components/timer/test_reproduce_state.py b/tests/components/timer/test_reproduce_state.py new file mode 100644 index 00000000000..5539d8610c3 --- /dev/null +++ b/tests/components/timer/test_reproduce_state.py @@ -0,0 +1,84 @@ +"""Test reproduce state for Timer.""" +from homeassistant.components.timer import ( + ATTR_DURATION, + SERVICE_CANCEL, + SERVICE_PAUSE, + SERVICE_START, + STATUS_ACTIVE, + STATUS_IDLE, + STATUS_PAUSED, +) +from homeassistant.core import State +from tests.common import async_mock_service + + +async def test_reproducing_states(hass, caplog): + """Test reproducing Timer states.""" + hass.states.async_set("timer.entity_idle", STATUS_IDLE, {}) + hass.states.async_set("timer.entity_paused", STATUS_PAUSED, {}) + hass.states.async_set("timer.entity_active", STATUS_ACTIVE, {}) + hass.states.async_set( + "timer.entity_active_attr", STATUS_ACTIVE, {ATTR_DURATION: "00:01:00"} + ) + + start_calls = async_mock_service(hass, "timer", SERVICE_START) + pause_calls = async_mock_service(hass, "timer", SERVICE_PAUSE) + cancel_calls = async_mock_service(hass, "timer", SERVICE_CANCEL) + + # These calls should do nothing as entities already in desired state + await hass.helpers.state.async_reproduce_state( + [ + State("timer.entity_idle", STATUS_IDLE), + State("timer.entity_paused", STATUS_PAUSED), + State("timer.entity_active", STATUS_ACTIVE), + State( + "timer.entity_active_attr", STATUS_ACTIVE, {ATTR_DURATION: "00:01:00"} + ), + ], + blocking=True, + ) + + assert len(start_calls) == 0 + assert len(pause_calls) == 0 + assert len(cancel_calls) == 0 + + # Test invalid state is handled + await hass.helpers.state.async_reproduce_state( + [State("timer.entity_idle", "not_supported")], blocking=True + ) + + assert "not_supported" in caplog.text + assert len(start_calls) == 0 + assert len(pause_calls) == 0 + assert len(cancel_calls) == 0 + + # Make sure correct services are called + await hass.helpers.state.async_reproduce_state( + [ + State("timer.entity_idle", STATUS_ACTIVE, {ATTR_DURATION: "00:01:00"}), + State("timer.entity_paused", STATUS_ACTIVE), + State("timer.entity_active", STATUS_IDLE), + State("timer.entity_active_attr", STATUS_PAUSED), + # Should not raise + State("timer.non_existing", "on"), + ], + blocking=True, + ) + + valid_start_calls = [ + {"entity_id": "timer.entity_idle", ATTR_DURATION: "00:01:00"}, + {"entity_id": "timer.entity_paused"}, + ] + assert len(start_calls) == 2 + for call in start_calls: + assert call.domain == "timer" + assert call.data in valid_start_calls + valid_start_calls.remove(call.data) + + assert len(pause_calls) == 1 + assert pause_calls[0].domain == "timer" + assert pause_calls[0].data == {"entity_id": "timer.entity_active_attr"} + + assert len(cancel_calls) == 1 + assert cancel_calls[0].domain == "timer" + assert cancel_calls[0].data == {"entity_id": "timer.entity_active"} diff --git a/tests/components/tplink/test_device_tracker.py b/tests/components/tplink/test_device_tracker.py deleted file mode 100644 index bbe73dc121a..00000000000 --- a/tests/components/tplink/test_device_tracker.py +++ /dev/null @@ -1,71 +0,0 @@ -"""The tests for the tplink device tracker platform.""" - -import os -import pytest - -from homeassistant.components.device_tracker.legacy import YAML_DEVICES -from homeassistant.components.tplink.device_tracker import Tplink4DeviceScanner -from homeassistant.const import CONF_PLATFORM, CONF_PASSWORD, CONF_USERNAME, CONF_HOST -import requests_mock - - -@pytest.fixture(autouse=True) -def setup_comp(hass): - """Initialize components.""" - yaml_devices = hass.config.path(YAML_DEVICES) - yield - if os.path.isfile(yaml_devices): - os.remove(yaml_devices) - - -async def test_get_mac_addresses_from_both_bands(hass): - """Test grabbing the mac addresses from 2.4 and 5 GHz clients pages.""" - with requests_mock.Mocker() as m: - conf_dict = { - CONF_PLATFORM: "tplink", - CONF_HOST: "fake-host", - CONF_USERNAME: "fake_user", - CONF_PASSWORD: "fake_pass", - } - - # Mock the token retrieval process - FAKE_TOKEN = "fake_token" - fake_auth_token_response = ( - "window.parent.location.href = " - '"https://a/{}/userRpm/Index.htm";'.format(FAKE_TOKEN) - ) - - m.get( - "http://{}/userRpm/LoginRpm.htm?Save=Save".format(conf_dict[CONF_HOST]), - text=fake_auth_token_response, - ) - - FAKE_MAC_1 = "CA-FC-8A-C8-BB-53" - FAKE_MAC_2 = "6C-48-83-21-46-8D" - FAKE_MAC_3 = "77-98-75-65-B1-2B" - mac_response_2_4 = "{} {}".format(FAKE_MAC_1, FAKE_MAC_2) - mac_response_5 = "{}".format(FAKE_MAC_3) - - # Mock the 2.4 GHz clients page - m.get( - "http://{}/{}/userRpm/WlanStationRpm.htm".format( - conf_dict[CONF_HOST], FAKE_TOKEN - ), - text=mac_response_2_4, - ) - - # Mock the 5 GHz clients page - m.get( - "http://{}/{}/userRpm/WlanStationRpm_5g.htm".format( - conf_dict[CONF_HOST], FAKE_TOKEN - ), - text=mac_response_5, - ) - - tplink = Tplink4DeviceScanner(conf_dict) - - expected_mac_results = [ - mac.replace("-", ":") for mac in [FAKE_MAC_1, FAKE_MAC_2, FAKE_MAC_3] - ] - - assert tplink.last_results == expected_mac_results diff --git a/tests/components/tplink/test_init.py b/tests/components/tplink/test_init.py index df9bf2c2ca2..9428bf05483 100644 --- a/tests/components/tplink/test_init.py +++ b/tests/components/tplink/test_init.py @@ -1,21 +1,22 @@ """Tests for the TP-Link component.""" -from typing import Dict, Any +from typing import Any, Dict from unittest.mock import MagicMock, patch +from pyHS100 import SmartBulb, SmartDevice, SmartDeviceException, SmartPlug import pytest -from pyHS100 import SmartPlug, SmartBulb, SmartDevice, SmartDeviceException from homeassistant import config_entries, data_entry_flow from homeassistant.components import tplink from homeassistant.components.tplink.common import ( - CONF_DISCOVERY, CONF_DIMMER, + CONF_DISCOVERY, CONF_LIGHT, CONF_SWITCH, ) from homeassistant.const import CONF_HOST from homeassistant.setup import async_setup_component -from tests.common import MockDependency, MockConfigEntry, mock_coro + +from tests.common import MockConfigEntry, MockDependency, mock_coro MOCK_PYHS100 = MockDependency("pyHS100") @@ -25,7 +26,10 @@ async def test_creating_entry_tries_discover(hass): with MOCK_PYHS100, patch( "homeassistant.components.tplink.async_setup_entry", return_value=mock_coro(True), - ) as mock_setup, patch("pyHS100.Discover.discover", return_value={"host": 1234}): + ) as mock_setup, patch( + "homeassistant.components.tplink.common.Discover.discover", + return_value={"host": 1234}, + ): result = await hass.config_entries.flow.async_init( tplink.DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -43,7 +47,9 @@ async def test_creating_entry_tries_discover(hass): async def test_configuring_tplink_causes_discovery(hass): """Test that specifying empty config does discovery.""" - with MOCK_PYHS100, patch("pyHS100.Discover.discover") as discover: + with MOCK_PYHS100, patch( + "homeassistant.components.tplink.common.Discover.discover" + ) as discover: discover.return_value = {"host": 1234} await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() @@ -61,8 +67,10 @@ async def test_configuring_tplink_causes_discovery(hass): @pytest.mark.parametrize("count", [1, 2, 3]) async def test_configuring_device_types(hass, name, cls, platform, count): """Test that light or switch platform list is filled correctly.""" - with patch("pyHS100.Discover.discover") as discover, patch( - "pyHS100.SmartDevice._query_helper" + with patch( + "homeassistant.components.tplink.common.Discover.discover" + ) as discover, patch( + "homeassistant.components.tplink.common.SmartDevice._query_helper" ): discovery_data = { "123.123.123.{}".format(c): cls("123.123.123.123") for c in range(count) @@ -104,8 +112,10 @@ class UnknownSmartDevice(SmartDevice): async def test_configuring_devices_from_multiple_sources(hass): """Test static and discover devices are not duplicated.""" - with patch("pyHS100.Discover.discover") as discover, patch( - "pyHS100.SmartDevice._query_helper" + with patch( + "homeassistant.components.tplink.common.Discover.discover" + ) as discover, patch( + "homeassistant.components.tplink.common.SmartDevice._query_helper" ): discover_device_fail = SmartPlug("123.123.123.123") discover_device_fail.get_sysinfo = MagicMock(side_effect=SmartDeviceException()) @@ -139,11 +149,15 @@ async def test_configuring_devices_from_multiple_sources(hass): async def test_is_dimmable(hass): """Test that is_dimmable switches are correctly added as lights.""" - with patch("pyHS100.Discover.discover") as discover, patch( + with patch( + "homeassistant.components.tplink.common.Discover.discover" + ) as discover, patch( "homeassistant.components.tplink.light.async_setup_entry", return_value=mock_coro(True), - ) as setup, patch("pyHS100.SmartDevice._query_helper"), patch( - "pyHS100.SmartPlug.is_dimmable", True + ) as setup, patch( + "homeassistant.components.tplink.common.SmartDevice._query_helper" + ), patch( + "homeassistant.components.tplink.common.SmartPlug.is_dimmable", True ): dimmable_switch = SmartPlug("123.123.123.123") discover.return_value = {"host": dimmable_switch} @@ -162,7 +176,9 @@ async def test_configuring_discovery_disabled(hass): with MOCK_PYHS100, patch( "homeassistant.components.tplink.async_setup_entry", return_value=mock_coro(True), - ) as mock_setup, patch("pyHS100.Discover.discover", return_value=[]) as discover: + ) as mock_setup, patch( + "homeassistant.components.tplink.common.Discover.discover", return_value=[] + ) as discover: await async_setup_component( hass, tplink.DOMAIN, {tplink.DOMAIN: {tplink.CONF_DISCOVERY: False}} ) @@ -182,8 +198,10 @@ async def test_platforms_are_initialized(hass): } } - with patch("pyHS100.Discover.discover") as discover, patch( - "pyHS100.SmartDevice._query_helper" + with patch( + "homeassistant.components.tplink.common.Discover.discover" + ) as discover, patch( + "homeassistant.components.tplink.common.SmartDevice._query_helper" ), patch( "homeassistant.components.tplink.light.async_setup_entry", return_value=mock_coro(True), @@ -191,7 +209,7 @@ async def test_platforms_are_initialized(hass): "homeassistant.components.tplink.switch.async_setup_entry", return_value=mock_coro(True), ) as switch_setup, patch( - "pyHS100.SmartPlug.is_dimmable", False + "homeassistant.components.tplink.common.SmartPlug.is_dimmable", False ): # patching is_dimmable is necessray to avoid misdetection as light. await async_setup_component(hass, tplink.DOMAIN, config) @@ -221,7 +239,9 @@ async def test_unload(hass, platform): entry = MockConfigEntry(domain=tplink.DOMAIN) entry.add_to_hass(hass) - with patch("pyHS100.SmartDevice._query_helper"), patch( + with patch( + "homeassistant.components.tplink.common.SmartDevice._query_helper" + ), patch( "homeassistant.components.tplink.{}" ".async_setup_entry".format(platform), return_value=mock_coro(True), ) as light_setup: diff --git a/tests/components/transmission/test_config_flow.py b/tests/components/transmission/test_config_flow.py index e79f5c8ac96..28fbed9ff42 100644 --- a/tests/components/transmission/test_config_flow.py +++ b/tests/components/transmission/test_config_flow.py @@ -1,4 +1,4 @@ -"""Tests for Met.no config flow.""" +"""Tests for Transmission config flow.""" from datetime import timedelta from unittest.mock import patch @@ -31,6 +31,14 @@ PASSWORD = "password" PORT = 9091 SCAN_INTERVAL = 10 +MOCK_ENTRY = { + CONF_NAME: NAME, + CONF_HOST: HOST, + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_PORT: PORT, +} + @pytest.fixture(name="api") def mock_transmission_api(): @@ -90,18 +98,10 @@ 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 + # assert result["data"]["options"][CONF_SCAN_INTERVAL] == DEFAULT_SCAN_INTERVAL # test with all provided - result = await flow.async_step_user( - { - CONF_NAME: NAME, - CONF_HOST: HOST, - CONF_USERNAME: USERNAME, - CONF_PASSWORD: PASSWORD, - CONF_PORT: PORT, - } - ) + result = await flow.async_step_user(MOCK_ENTRY) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == NAME @@ -110,7 +110,7 @@ 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 + # assert result["data"]["options"][CONF_SCAN_INTERVAL] == DEFAULT_SCAN_INTERVAL async def test_options(hass): @@ -118,14 +118,7 @@ async def test_options(hass): entry = MockConfigEntry( domain=DOMAIN, title=CONF_NAME, - data={ - "name": DEFAULT_NAME, - "host": HOST, - "username": USERNAME, - "password": PASSWORD, - "port": DEFAULT_PORT, - "options": {CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL}, - }, + data=MOCK_ENTRY, options={CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL}, ) flow = init_config_flow(hass) @@ -157,7 +150,7 @@ async def test_import(hass, api): assert result["data"][CONF_NAME] == DEFAULT_NAME assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_PORT] == DEFAULT_PORT - assert result["data"]["options"][CONF_SCAN_INTERVAL] == DEFAULT_SCAN_INTERVAL + assert result["data"][CONF_SCAN_INTERVAL] == DEFAULT_SCAN_INTERVAL # import with all result = await flow.async_step_import( @@ -177,18 +170,40 @@ async def test_import(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] == SCAN_INTERVAL + assert result["data"][CONF_SCAN_INTERVAL] == SCAN_INTERVAL -async def test_integration_already_exists(hass, api): - """Test we only allow a single config flow.""" - MockConfigEntry(domain=DOMAIN).add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} +async def test_host_already_configured(hass, api): + """Test host is already configured.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_ENTRY, + options={CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL}, ) + entry.add_to_hass(hass) + flow = init_config_flow(hass) + result = await flow.async_step_user(MOCK_ENTRY) + assert result["type"] == "abort" - assert result["reason"] == "one_instance_allowed" + assert result["reason"] == "already_configured" + + +async def test_name_already_configured(hass, api): + """Test name is already configured.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_ENTRY, + options={CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL}, + ) + entry.add_to_hass(hass) + + mock_entry = MOCK_ENTRY.copy() + mock_entry[CONF_HOST] = "0.0.0.0" + flow = init_config_flow(hass) + result = await flow.async_step_user(mock_entry) + + assert result["type"] == "form" + assert result["errors"] == {CONF_NAME: "name_exists"} async def test_error_on_wrong_credentials(hass, auth_error): diff --git a/tests/components/transmission/test_init.py b/tests/components/transmission/test_init.py new file mode 100644 index 00000000000..4baa00de7a7 --- /dev/null +++ b/tests/components/transmission/test_init.py @@ -0,0 +1,123 @@ +"""Tests for Transmission init.""" + +from unittest.mock import patch + +import pytest +from transmissionrpc.error import TransmissionError + +from homeassistant.components import transmission +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry, mock_coro + +MOCK_ENTRY = MockConfigEntry( + domain=transmission.DOMAIN, + data={ + transmission.CONF_NAME: "Transmission", + transmission.CONF_HOST: "0.0.0.0", + transmission.CONF_USERNAME: "user", + transmission.CONF_PASSWORD: "pass", + transmission.CONF_PORT: 9091, + }, +) + + +@pytest.fixture(name="api") +def mock_transmission_api(): + """Mock an api.""" + with patch("transmissionrpc.Client"): + yield + + +@pytest.fixture(name="auth_error") +def mock_api_authentication_error(): + """Mock an api.""" + with patch( + "transmissionrpc.Client", side_effect=TransmissionError("401: Unauthorized") + ): + yield + + +@pytest.fixture(name="unknown_error") +def mock_api_unknown_error(): + """Mock an api.""" + with patch("transmissionrpc.Client", side_effect=TransmissionError): + yield + + +async def test_setup_with_no_config(hass): + """Test that we do not discover anything or try to set up a Transmission client.""" + assert await async_setup_component(hass, transmission.DOMAIN, {}) is True + assert transmission.DOMAIN not in hass.data + + +async def test_setup_with_config(hass, api): + """Test that we import the config and setup the client.""" + config = { + transmission.DOMAIN: { + transmission.CONF_NAME: "Transmission", + transmission.CONF_HOST: "0.0.0.0", + transmission.CONF_USERNAME: "user", + transmission.CONF_PASSWORD: "pass", + transmission.CONF_PORT: 9091, + }, + transmission.DOMAIN: { + transmission.CONF_NAME: "Transmission2", + transmission.CONF_HOST: "0.0.0.1", + transmission.CONF_USERNAME: "user", + transmission.CONF_PASSWORD: "pass", + transmission.CONF_PORT: 9091, + }, + } + assert await async_setup_component(hass, transmission.DOMAIN, config) is True + + +async def test_successful_config_entry(hass, api): + """Test that configured transmission is configured successfully.""" + + entry = MOCK_ENTRY + entry.add_to_hass(hass) + + assert await transmission.async_setup_entry(hass, entry) is True + assert entry.options == { + transmission.CONF_SCAN_INTERVAL: transmission.DEFAULT_SCAN_INTERVAL + } + + +async def test_setup_failed(hass): + """Test transmission failed due to an error.""" + + entry = MOCK_ENTRY + entry.add_to_hass(hass) + + # test connection error raising ConfigEntryNotReady + with patch( + "transmissionrpc.Client", + side_effect=TransmissionError("111: Connection refused"), + ), pytest.raises(ConfigEntryNotReady): + + await transmission.async_setup_entry(hass, entry) + + # test Authentication error returning false + + with patch( + "transmissionrpc.Client", side_effect=TransmissionError("401: Unauthorized") + ): + + assert await transmission.async_setup_entry(hass, entry) is False + + +async def test_unload_entry(hass, api): + """Test removing transmission client.""" + entry = MOCK_ENTRY + entry.add_to_hass(hass) + + with patch.object( + hass.config_entries, "async_forward_entry_unload", return_value=mock_coro(True) + ) as unload_entry: + assert await transmission.async_setup_entry(hass, entry) + + assert await transmission.async_unload_entry(hass, entry) + assert unload_entry.call_count == 2 + assert entry.entry_id not in hass.data[transmission.DOMAIN] diff --git a/tests/components/unifi/test_config_flow.py b/tests/components/unifi/test_config_flow.py new file mode 100644 index 00000000000..aea4d565f3d --- /dev/null +++ b/tests/components/unifi/test_config_flow.py @@ -0,0 +1,265 @@ +"""Test UniFi config flow.""" +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, + CONF_PORT, + CONF_USERNAME, + CONF_VERIFY_SSL, +) + +from tests.common import MockConfigEntry + +import aiounifi + + +async def test_flow_works(hass, aioclient_mock): + """Test config flow.""" + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + + aioclient_mock.post( + "https://1.2.3.4:1234/api/login", + json={"data": "login successful", "meta": {"rc": "ok"}}, + headers={"content-type": "application/json"}, + ) + + aioclient_mock.get( + "https://1.2.3.4:1234/api/self/sites", + json={ + "data": [{"desc": "Site name", "name": "site_id", "role": "admin"}], + "meta": {"rc": "ok"}, + }, + headers={"content-type": "application/json"}, + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "1.2.3.4", + CONF_USERNAME: "username", + CONF_PASSWORD: "password", + CONF_PORT: 1234, + CONF_VERIFY_SSL: True, + }, + ) + + assert result["type"] == "create_entry" + assert result["title"] == "Site name" + assert result["data"] == { + CONF_CONTROLLER: { + CONF_HOST: "1.2.3.4", + CONF_USERNAME: "username", + CONF_PASSWORD: "password", + CONF_PORT: 1234, + CONF_SITE_ID: "site_id", + CONF_VERIFY_SSL: True, + } + } + + +async def test_flow_works_multiple_sites(hass, aioclient_mock): + """Test config flow works when finding multiple sites.""" + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + + aioclient_mock.post( + "https://1.2.3.4:1234/api/login", + json={"data": "login successful", "meta": {"rc": "ok"}}, + headers={"content-type": "application/json"}, + ) + + aioclient_mock.get( + "https://1.2.3.4:1234/api/self/sites", + json={ + "data": [ + {"name": "default", "role": "admin", "desc": "site name"}, + {"name": "site2", "role": "admin", "desc": "site2 name"}, + ], + "meta": {"rc": "ok"}, + }, + headers={"content-type": "application/json"}, + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "1.2.3.4", + CONF_USERNAME: "username", + CONF_PASSWORD: "password", + CONF_PORT: 1234, + CONF_VERIFY_SSL: True, + }, + ) + + assert result["type"] == "form" + assert result["step_id"] == "site" + assert result["data_schema"]({"site": "site name"}) + assert result["data_schema"]({"site": "site2 name"}) + + +async def test_flow_fails_site_already_configured(hass, aioclient_mock): + """Test config flow.""" + entry = MockConfigEntry( + domain=unifi.DOMAIN, data={"controller": {"host": "1.2.3.4", "site": "site_id"}} + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + + aioclient_mock.post( + "https://1.2.3.4:1234/api/login", + json={"data": "login successful", "meta": {"rc": "ok"}}, + headers={"content-type": "application/json"}, + ) + + aioclient_mock.get( + "https://1.2.3.4:1234/api/self/sites", + json={ + "data": [{"desc": "Site name", "name": "site_id", "role": "admin"}], + "meta": {"rc": "ok"}, + }, + headers={"content-type": "application/json"}, + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "1.2.3.4", + CONF_USERNAME: "username", + CONF_PASSWORD: "password", + CONF_PORT: 1234, + CONF_VERIFY_SSL: True, + }, + ) + + assert result["type"] == "abort" + + +async def test_flow_fails_user_credentials_faulty(hass, aioclient_mock): + """Test config flow.""" + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + + with patch("aiounifi.Controller.login", side_effect=aiounifi.errors.Unauthorized): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "1.2.3.4", + CONF_USERNAME: "username", + CONF_PASSWORD: "password", + CONF_PORT: 1234, + CONF_VERIFY_SSL: True, + }, + ) + + assert result["type"] == "form" + assert result["errors"] == {"base": "faulty_credentials"} + + +async def test_flow_fails_controller_unavailable(hass, aioclient_mock): + """Test config flow.""" + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + + with patch("aiounifi.Controller.login", side_effect=aiounifi.errors.RequestError): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "1.2.3.4", + CONF_USERNAME: "username", + CONF_PASSWORD: "password", + CONF_PORT: 1234, + CONF_VERIFY_SSL: True, + }, + ) + + assert result["type"] == "form" + assert result["errors"] == {"base": "service_unavailable"} + + +async def test_flow_fails_unknown_problem(hass, aioclient_mock): + """Test config flow.""" + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + + with patch("aiounifi.Controller.login", side_effect=Exception): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "1.2.3.4", + CONF_USERNAME: "username", + CONF_PASSWORD: "password", + CONF_PORT: 1234, + CONF_VERIFY_SSL: True, + }, + ) + + assert result["type"] == "abort" + + +async def test_option_flow(hass): + """Test config flow options.""" + entry = MockConfigEntry(domain=config_flow.DOMAIN, data={}, options=None) + hass.config_entries._entries.append(entry) + + flow = await hass.config_entries.options._async_create_flow( + entry.entry_id, context={"source": "test"}, data=None + ) + + result = await flow.async_step_init() + assert result["type"] == "form" + assert result["step_id"] == "device_tracker" + + result = await flow.async_step_device_tracker( + user_input={ + config_flow.CONF_TRACK_CLIENTS: False, + config_flow.CONF_TRACK_WIRED_CLIENTS: False, + config_flow.CONF_TRACK_DEVICES: False, + config_flow.CONF_DETECTION_TIME: 100, + } + ) + assert result["type"] == "form" + assert result["step_id"] == "statistics_sensors" + + result = await flow.async_step_statistics_sensors( + user_input={config_flow.CONF_ALLOW_BANDWIDTH_SENSORS: True} + ) + assert result["type"] == "create_entry" + assert result["data"] == { + config_flow.CONF_TRACK_CLIENTS: False, + config_flow.CONF_TRACK_WIRED_CLIENTS: False, + config_flow.CONF_TRACK_DEVICES: False, + config_flow.CONF_DETECTION_TIME: 100, + config_flow.CONF_ALLOW_BANDWIDTH_SENSORS: True, + } diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py index e73719205f7..2b64e56cd99 100644 --- a/tests/components/unifi/test_controller.py +++ b/tests/components/unifi/test_controller.py @@ -1,9 +1,14 @@ """Test UniFi Controller.""" -from unittest.mock import Mock, patch +from collections import deque +from datetime import timedelta + +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, CONF_SITE_ID, @@ -17,259 +22,362 @@ from homeassistant.const import ( CONF_USERNAME, CONF_VERIFY_SSL, ) -from homeassistant.components.unifi import controller, errors +import aiounifi -from tests.common import mock_coro - -CONTROLLER_SITES = {"site1": {"desc": "nice name", "name": "site", "role": "admin"}} +CONTROLLER_HOST = { + "hostname": "controller_host", + "ip": "1.2.3.4", + "is_wired": True, + "last_seen": 1562600145, + "mac": "10:00:00:00:00:01", + "name": "Controller host", + "oui": "Producer", + "sw_mac": "00:00:00:00:01:01", + "sw_port": 1, + "wired-rx_bytes": 1234000000, + "wired-tx_bytes": 5678000000, +} CONTROLLER_DATA = { CONF_HOST: "1.2.3.4", CONF_USERNAME: "username", CONF_PASSWORD: "password", CONF_PORT: 1234, - CONF_SITE_ID: "site", - CONF_VERIFY_SSL: True, + CONF_SITE_ID: "site_id", + CONF_VERIFY_SSL: False, } ENTRY_CONFIG = {CONF_CONTROLLER: CONTROLLER_DATA} +SITES = {"Site name": {"desc": "Site name", "name": "site_id", "role": "admin"}} -async def test_controller_setup(): + +async def setup_unifi_integration( + hass, + config, + options, + sites, + clients_response, + devices_response, + clients_all_response, +): + """Create the UniFi controller.""" + if UNIFI_CONFIG not in hass.data: + hass.data[UNIFI_CONFIG] = [] + hass.data[UNIFI_WIRELESS_CLIENTS] = unifi.UnifiWirelessClients(hass) + config_entry = config_entries.ConfigEntry( + version=1, + domain=unifi.DOMAIN, + title="Mock Title", + data=config, + source="test", + connection_class=config_entries.CONN_CLASS_LOCAL_POLL, + system_options={}, + options=options, + entry_id=1, + ) + + mock_client_responses = deque() + mock_client_responses.append(clients_response) + + mock_device_responses = deque() + mock_device_responses.append(devices_response) + + mock_client_all_responses = deque() + mock_client_all_responses.append(clients_all_response) + + mock_requests = [] + + async def mock_request(self, method, path, json=None): + mock_requests.append({"method": method, "path": path, "json": json}) + + if path == "s/{site}/stat/sta" and mock_client_responses: + return mock_client_responses.popleft() + if path == "s/{site}/stat/device" and mock_device_responses: + return mock_device_responses.popleft() + if path == "s/{site}/rest/user" and mock_client_all_responses: + return mock_client_all_responses.popleft() + return {} + + with patch("aiounifi.Controller.login", return_value=True), patch( + "aiounifi.Controller.sites", return_value=sites + ), patch("aiounifi.Controller.request", new=mock_request): + await unifi.async_setup_entry(hass, config_entry) + await hass.async_block_till_done() + hass.config_entries._entries.append(config_entry) + + controller_id = unifi.get_controller_id_from_config_entry(config_entry) + if controller_id not in hass.data[unifi.DOMAIN]: + return None + controller = hass.data[unifi.DOMAIN][controller_id] + + controller.mock_client_responses = mock_client_responses + controller.mock_device_responses = mock_device_responses + controller.mock_client_all_responses = mock_client_all_responses + controller.mock_requests = mock_requests + + return controller + + +async def test_controller_setup(hass): """Successful setup.""" - hass = Mock() - hass.data = { - UNIFI_CONFIG: [ - { - CONF_HOST: CONTROLLER_DATA[CONF_HOST], - CONF_SITE_ID: "nice name", - controller.CONF_BLOCK_CLIENT: ["mac"], - controller.CONF_DONT_TRACK_CLIENTS: True, - controller.CONF_DONT_TRACK_DEVICES: True, - controller.CONF_DONT_TRACK_WIRED_CLIENTS: True, - controller.CONF_DETECTION_TIME: 30, - controller.CONF_SSID_FILTER: ["ssid"], - } - ], - UNIFI_WIRELESS_CLIENTS: Mock(), - } - entry = Mock() - entry.data = ENTRY_CONFIG - entry.options = {} - api = Mock() - api.initialize.return_value = mock_coro(True) - api.sites.return_value = mock_coro(CONTROLLER_SITES) - api.clients = [] + with patch( + "homeassistant.config_entries.ConfigEntries.async_forward_entry_setup", + return_value=True, + ) as forward_entry_setup: + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={}, + sites=SITES, + clients_response=[], + devices_response=[], + clients_all_response=[], + ) - unifi_controller = controller.UniFiController(hass, entry) + entry = controller.config_entry + assert len(forward_entry_setup.mock_calls) == len( + unifi.controller.SUPPORTED_PLATFORMS + ) + assert forward_entry_setup.mock_calls[0][1] == (entry, "device_tracker") + assert forward_entry_setup.mock_calls[1][1] == (entry, "sensor") + assert forward_entry_setup.mock_calls[2][1] == (entry, "switch") - with patch.object(controller, "get_controller", return_value=mock_coro(api)): - assert await unifi_controller.async_setup() is True + assert controller.host == CONTROLLER_DATA[CONF_HOST] + assert controller.site == CONTROLLER_DATA[CONF_SITE_ID] + assert controller.site_name in SITES + assert controller.site_role == SITES[controller.site_name]["role"] - assert unifi_controller.api is api - assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 2 - assert hass.config_entries.async_forward_entry_setup.mock_calls[0][1] == ( - entry, - "device_tracker", + assert ( + controller.option_allow_bandwidth_sensors + == unifi.const.DEFAULT_ALLOW_BANDWIDTH_SENSORS ) - assert hass.config_entries.async_forward_entry_setup.mock_calls[1][1] == ( - entry, - "switch", + assert controller.option_block_clients == unifi.const.DEFAULT_BLOCK_CLIENTS + assert controller.option_track_clients == unifi.const.DEFAULT_TRACK_CLIENTS + assert controller.option_track_devices == unifi.const.DEFAULT_TRACK_DEVICES + assert ( + controller.option_track_wired_clients == unifi.const.DEFAULT_TRACK_WIRED_CLIENTS ) + assert controller.option_detection_time == timedelta( + seconds=unifi.const.DEFAULT_DETECTION_TIME + ) + assert controller.option_ssid_filter == unifi.const.DEFAULT_SSID_FILTER + + assert controller.mac is None + + assert controller.signal_update == "unifi-update-1.2.3.4-site_id" + assert controller.signal_options_update == "unifi-options-1.2.3.4-site_id" -async def test_controller_host(): - """Config entry host and controller host are the same.""" - hass = Mock() - entry = Mock() - entry.data = ENTRY_CONFIG - - unifi_controller = controller.UniFiController(hass, entry) - - assert unifi_controller.host == CONTROLLER_DATA[CONF_HOST] - - -async def test_controller_site(): - """Config entry site and controller site are the same.""" - hass = Mock() - entry = Mock() - entry.data = ENTRY_CONFIG - - unifi_controller = controller.UniFiController(hass, entry) - - assert unifi_controller.site == CONTROLLER_DATA[CONF_SITE_ID] - - -async def test_controller_mac(): +async def test_controller_mac(hass): """Test that it is possible to identify controller mac.""" - hass = Mock() - hass.data = {UNIFI_CONFIG: {}, UNIFI_WIRELESS_CLIENTS: Mock()} - hass.data[UNIFI_WIRELESS_CLIENTS].get_data.return_value = set() - entry = Mock() - entry.data = ENTRY_CONFIG - entry.options = {} - client = Mock() - client.ip = "1.2.3.4" - client.mac = "00:11:22:33:44:55" - api = Mock() - api.initialize.return_value = mock_coro(True) - api.clients = {"client1": client} - api.sites.return_value = mock_coro(CONTROLLER_SITES) - - unifi_controller = controller.UniFiController(hass, entry) - - with patch.object(controller, "get_controller", return_value=mock_coro(api)): - assert await unifi_controller.async_setup() is True - - assert unifi_controller.mac == "00:11:22:33:44:55" + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={}, + sites=SITES, + clients_response=[CONTROLLER_HOST], + devices_response=[], + clients_all_response=[], + ) + assert controller.mac == "10:00:00:00:00:01" -async def test_controller_no_mac(): - """Test that it works to not find the controllers mac.""" - hass = Mock() - hass.data = {UNIFI_CONFIG: {}, UNIFI_WIRELESS_CLIENTS: Mock()} - entry = Mock() - entry.data = ENTRY_CONFIG - entry.options = {} - client = Mock() - client.ip = "5.6.7.8" - api = Mock() - api.initialize.return_value = mock_coro(True) - api.clients = {"client1": client} - api.sites.return_value = mock_coro(CONTROLLER_SITES) - api.clients = {} +async def test_controller_import_config(hass): + """Test that import configuration.yaml instructions work.""" + hass.data[UNIFI_CONFIG] = [ + { + CONF_HOST: "1.2.3.4", + CONF_SITE_ID: "Site name", + unifi.const.CONF_ALLOW_BANDWIDTH_SENSORS: True, + unifi.CONF_BLOCK_CLIENT: ["random mac"], + unifi.CONF_DONT_TRACK_CLIENTS: True, + unifi.CONF_DONT_TRACK_DEVICES: True, + unifi.CONF_DONT_TRACK_WIRED_CLIENTS: True, + unifi.CONF_DETECTION_TIME: 150, + unifi.CONF_SSID_FILTER: ["SSID"], + } + ] + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={}, + sites=SITES, + clients_response=[], + devices_response=[], + clients_all_response=[], + ) - unifi_controller = controller.UniFiController(hass, entry) - - with patch.object(controller, "get_controller", return_value=mock_coro(api)): - assert await unifi_controller.async_setup() is True - - assert unifi_controller.mac is None + assert controller.option_allow_bandwidth_sensors is False + assert controller.option_block_clients == ["random mac"] + assert controller.option_track_clients is False + assert controller.option_track_devices is False + assert controller.option_track_wired_clients is False + assert controller.option_detection_time == timedelta(seconds=150) + assert controller.option_ssid_filter == ["SSID"] -async def test_controller_not_accessible(): +async def test_controller_not_accessible(hass): """Retry to login gets scheduled when connection fails.""" - hass = Mock() - entry = Mock() - entry.data = ENTRY_CONFIG - api = Mock() - api.initialize.return_value = mock_coro(True) - - unifi_controller = controller.UniFiController(hass, entry) - with patch.object( - controller, "get_controller", side_effect=errors.CannotConnect + unifi.controller, "get_controller", side_effect=unifi.errors.CannotConnect ), pytest.raises(ConfigEntryNotReady): - await unifi_controller.async_setup() + await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={}, + sites=SITES, + clients_response=[], + devices_response=[], + clients_all_response=[], + ) -async def test_controller_unknown_error(): +async def test_controller_unknown_error(hass): """Unknown errors are handled.""" - hass = Mock() - entry = Mock() - entry.data = ENTRY_CONFIG - api = Mock() - api.initialize.return_value = mock_coro(True) - - unifi_controller = controller.UniFiController(hass, entry) - - with patch.object(controller, "get_controller", side_effect=Exception): - assert await unifi_controller.async_setup() is False - - assert not hass.helpers.event.async_call_later.mock_calls + with patch.object(unifi.controller, "get_controller", side_effect=Exception): + await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={}, + sites=SITES, + clients_response=[], + devices_response=[], + clients_all_response=[], + ) + assert hass.data[unifi.DOMAIN] == {} -async def test_reset_if_entry_had_wrong_auth(): - """Calling reset when the entry contains wrong auth.""" - hass = Mock() - entry = Mock() - entry.data = ENTRY_CONFIG +async def test_reset_after_successful_setup(hass): + """Calling reset when the entry has been setup.""" + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={}, + sites=SITES, + clients_response=[], + devices_response=[], + clients_all_response=[], + ) - unifi_controller = controller.UniFiController(hass, entry) + assert len(controller.listeners) == 5 + + result = await controller.async_reset() + await hass.async_block_till_done() + + assert result is True + assert len(controller.listeners) == 0 + + +async def test_failed_update_failed_login(hass): + """Running update can handle a failed login.""" + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={}, + sites=SITES, + clients_response=[], + devices_response=[], + clients_all_response=[], + ) with patch.object( - controller, "get_controller", side_effect=errors.AuthenticationRequired + controller.api.clients, "update", side_effect=aiounifi.LoginRequired + ), patch.object(controller.api, "login", side_effect=aiounifi.AiounifiException): + await controller.async_update() + await hass.async_block_till_done() + + assert controller.available is False + + +async def test_failed_update_successful_login(hass): + """Running update can login when requested.""" + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={}, + sites=SITES, + clients_response=[], + devices_response=[], + clients_all_response=[], + ) + + with patch.object( + controller.api.clients, "update", side_effect=aiounifi.LoginRequired + ), patch.object(controller.api, "login", return_value=Mock(True)): + await controller.async_update() + await hass.async_block_till_done() + + assert controller.available is True + + +async def test_failed_update(hass): + """Running update can login when requested.""" + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={}, + sites=SITES, + clients_response=[], + devices_response=[], + clients_all_response=[], + ) + + with patch.object( + controller.api.clients, "update", side_effect=aiounifi.AiounifiException ): - assert await unifi_controller.async_setup() is False + await controller.async_update() + await hass.async_block_till_done() - assert not hass.async_add_job.mock_calls + assert controller.available is False - assert await unifi_controller.async_reset() - - -async def test_reset_unloads_entry_if_setup(): - """Calling reset when the entry has been setup.""" - hass = Mock() - hass.data = {UNIFI_CONFIG: {}, UNIFI_WIRELESS_CLIENTS: Mock()} - entry = Mock() - entry.data = ENTRY_CONFIG - entry.options = {} - api = Mock() - api.initialize.return_value = mock_coro(True) - api.sites.return_value = mock_coro(CONTROLLER_SITES) - api.clients = [] - - unifi_controller = controller.UniFiController(hass, entry) - - with patch.object(controller, "get_controller", return_value=mock_coro(api)): - assert await unifi_controller.async_setup() is True - - assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 2 - - hass.config_entries.async_forward_entry_unload.return_value = mock_coro(True) - assert await unifi_controller.async_reset() - - assert len(hass.config_entries.async_forward_entry_unload.mock_calls) == 2 + await controller.async_update() + await hass.async_block_till_done() + assert controller.available is True async def test_get_controller(hass): """Successful call.""" - with patch("aiounifi.Controller.login", return_value=mock_coro()): - assert await controller.get_controller(hass, **CONTROLLER_DATA) + with patch("aiounifi.Controller.login", return_value=Mock()): + assert await unifi.controller.get_controller(hass, **CONTROLLER_DATA) async def test_get_controller_verify_ssl_false(hass): """Successful call with verify ssl set to false.""" controller_data = dict(CONTROLLER_DATA) controller_data[CONF_VERIFY_SSL] = False - with patch("aiounifi.Controller.login", return_value=mock_coro()): - assert await controller.get_controller(hass, **controller_data) + with patch("aiounifi.Controller.login", return_value=Mock()): + assert await unifi.controller.get_controller(hass, **controller_data) async def test_get_controller_login_failed(hass): """Check that get_controller can handle a failed login.""" - import aiounifi - result = None with patch("aiounifi.Controller.login", side_effect=aiounifi.Unauthorized): try: - result = await controller.get_controller(hass, **CONTROLLER_DATA) - except errors.AuthenticationRequired: + result = await unifi.controller.get_controller(hass, **CONTROLLER_DATA) + except unifi.errors.AuthenticationRequired: pass assert result is None async def test_get_controller_controller_unavailable(hass): """Check that get_controller can handle controller being unavailable.""" - import aiounifi - result = None with patch("aiounifi.Controller.login", side_effect=aiounifi.RequestError): try: - result = await controller.get_controller(hass, **CONTROLLER_DATA) - except errors.CannotConnect: + result = await unifi.controller.get_controller(hass, **CONTROLLER_DATA) + except unifi.errors.CannotConnect: pass assert result is None async def test_get_controller_unknown_error(hass): """Check that get_controller can handle unkown errors.""" - import aiounifi - result = None with patch("aiounifi.Controller.login", side_effect=aiounifi.AiounifiException): try: - result = await controller.get_controller(hass, **CONTROLLER_DATA) - except errors.AuthenticationRequired: + result = await unifi.controller.get_controller(hass, **CONTROLLER_DATA) + except unifi.errors.AuthenticationRequired: pass assert result is None diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 3a2b37487af..29b16553757 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -1,42 +1,23 @@ """The tests for the UniFi device tracker platform.""" -from collections import deque from copy import copy - from datetime import timedelta -from asynctest import Mock - -import pytest - -from aiounifi.clients import Clients, ClientsAll -from aiounifi.devices import Devices - from homeassistant import config_entries from homeassistant.components import unifi from homeassistant.components.unifi.const import ( - CONF_CONTROLLER, - CONF_SITE_ID, CONF_SSID_FILTER, CONF_TRACK_DEVICES, CONF_TRACK_WIRED_CLIENTS, - CONTROLLER_ID as CONF_CONTROLLER_ID, - UNIFI_CONFIG, - UNIFI_WIRELESS_CLIENTS, -) -from homeassistant.const import ( - CONF_HOST, - CONF_PASSWORD, - CONF_PORT, - CONF_USERNAME, - CONF_VERIFY_SSL, - STATE_UNAVAILABLE, ) +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 = { @@ -93,79 +74,6 @@ DEVICE_2 = { "version": "4.0.42.10433", } -CONTROLLER_DATA = { - CONF_HOST: "mock-host", - CONF_USERNAME: "mock-user", - CONF_PASSWORD: "mock-pswd", - CONF_PORT: 1234, - CONF_SITE_ID: "mock-site", - CONF_VERIFY_SSL: False, -} - -ENTRY_CONFIG = {CONF_CONTROLLER: CONTROLLER_DATA} - -CONTROLLER_ID = CONF_CONTROLLER_ID.format(host="mock-host", site="mock-site") - - -@pytest.fixture -def mock_controller(hass): - """Mock a UniFi Controller.""" - hass.data[UNIFI_CONFIG] = {} - hass.data[UNIFI_WIRELESS_CLIENTS] = Mock() - controller = unifi.UniFiController(hass, None) - controller.wireless_clients = set() - - controller.api = Mock() - controller.mock_requests = [] - - controller.mock_client_responses = deque() - controller.mock_device_responses = deque() - controller.mock_client_all_responses = deque() - - async def mock_request(method, path, **kwargs): - kwargs["method"] = method - kwargs["path"] = path - controller.mock_requests.append(kwargs) - if path == "s/{site}/stat/sta": - return controller.mock_client_responses.popleft() - if path == "s/{site}/stat/device": - return controller.mock_device_responses.popleft() - if path == "s/{site}/rest/user": - return controller.mock_client_all_responses.popleft() - return None - - controller.api.clients = Clients({}, mock_request) - controller.api.devices = Devices({}, mock_request) - controller.api.clients_all = ClientsAll({}, mock_request) - - return controller - - -async def setup_controller(hass, mock_controller, options={}): - """Load the UniFi switch platform with the provided controller.""" - hass.config.components.add(unifi.DOMAIN) - hass.data[unifi.DOMAIN] = {CONTROLLER_ID: mock_controller} - config_entry = config_entries.ConfigEntry( - 1, - unifi.DOMAIN, - "Mock Title", - ENTRY_CONFIG, - "test", - config_entries.CONN_CLASS_LOCAL_POLL, - entry_id=1, - system_options={}, - options=options, - ) - hass.config_entries._entries.append(config_entry) - mock_controller.config_entry = config_entry - - await mock_controller.async_update() - await hass.config_entries.async_forward_entry_setup( - config_entry, device_tracker.DOMAIN - ) - - await hass.async_block_till_done() - async def test_platform_manually_configured(hass): """Test that nothing happens when configuring unifi through device tracker platform.""" @@ -178,24 +86,32 @@ async def test_platform_manually_configured(hass): assert unifi.DOMAIN not in hass.data -async def test_no_clients(hass, mock_controller): +async def test_no_clients(hass): """Test the update_clients function when no clients are found.""" - mock_controller.mock_client_responses.append({}) - mock_controller.mock_device_responses.append({}) + await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={}, + sites=SITES, + clients_response=[], + devices_response=[], + clients_all_response=[], + ) - await setup_controller(hass, mock_controller) - assert len(mock_controller.mock_requests) == 2 assert len(hass.states.async_all()) == 2 -async def test_tracked_devices(hass, mock_controller): +async def test_tracked_devices(hass): """Test the update_items function with some clients.""" - mock_controller.mock_client_responses.append([CLIENT_1, CLIENT_2, CLIENT_3]) - mock_controller.mock_device_responses.append([DEVICE_1, DEVICE_2]) - options = {CONF_SSID_FILTER: ["ssid"]} - - await setup_controller(hass, mock_controller, options) - assert len(mock_controller.mock_requests) == 2 + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={CONF_SSID_FILTER: ["ssid"]}, + sites=SITES, + clients_response=[CLIENT_1, CLIENT_2, CLIENT_3], + devices_response=[DEVICE_1, DEVICE_2], + clients_all_response={}, + ) assert len(hass.states.async_all()) == 5 client_1 = hass.states.get("device_tracker.client_1") @@ -217,9 +133,9 @@ async def test_tracked_devices(hass, mock_controller): client_1_copy["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) device_1_copy = copy(DEVICE_1) device_1_copy["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) - mock_controller.mock_client_responses.append([client_1_copy]) - mock_controller.mock_device_responses.append([device_1_copy]) - await mock_controller.async_update() + controller.mock_client_responses.append([client_1_copy]) + controller.mock_device_responses.append([device_1_copy]) + await controller.async_update() await hass.async_block_till_done() client_1 = hass.states.get("device_tracker.client_1") @@ -230,19 +146,17 @@ async def test_tracked_devices(hass, mock_controller): device_1_copy = copy(DEVICE_1) device_1_copy["disabled"] = True - mock_controller.mock_client_responses.append({}) - mock_controller.mock_device_responses.append([device_1_copy]) - await mock_controller.async_update() + controller.mock_client_responses.append({}) + controller.mock_device_responses.append([device_1_copy]) + await controller.async_update() await hass.async_block_till_done() device_1 = hass.states.get("device_tracker.device_1") assert device_1.state == STATE_UNAVAILABLE - mock_controller.config_entry.add_update_listener( - mock_controller.async_options_updated - ) + controller.config_entry.add_update_listener(controller.async_options_updated) hass.config_entries.async_update_entry( - mock_controller.config_entry, + controller.config_entry, options={ CONF_SSID_FILTER: [], CONF_TRACK_WIRED_CLIENTS: False, @@ -258,18 +172,23 @@ async def test_tracked_devices(hass, mock_controller): assert device_1 is None -async def test_wireless_client_go_wired_issue(hass, mock_controller): +async def test_wireless_client_go_wired_issue(hass): """Test the solution to catch wireless device go wired UniFi issue. UniFi has a known issue that when a wireless device goes away it sometimes gets marked as wired. """ client_1_client = copy(CLIENT_1) client_1_client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) - mock_controller.mock_client_responses.append([client_1_client]) - mock_controller.mock_device_responses.append({}) - await setup_controller(hass, mock_controller) - assert len(mock_controller.mock_requests) == 2 + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={}, + sites=SITES, + clients_response=[client_1_client], + devices_response=[], + clients_all_response=[], + ) assert len(hass.states.async_all()) == 3 client_1 = hass.states.get("device_tracker.client_1") @@ -278,9 +197,9 @@ async def test_wireless_client_go_wired_issue(hass, mock_controller): client_1_client["is_wired"] = True client_1_client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) - mock_controller.mock_client_responses.append([client_1_client]) - mock_controller.mock_device_responses.append({}) - await mock_controller.async_update() + 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") @@ -288,65 +207,71 @@ async def test_wireless_client_go_wired_issue(hass, mock_controller): client_1_client["is_wired"] = False client_1_client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) - mock_controller.mock_client_responses.append([client_1_client]) - mock_controller.mock_device_responses.append({}) - await mock_controller.async_update() + 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 == "home" -async def test_restoring_client(hass, mock_controller): +async def test_restoring_client(hass): """Test the update_items function with some clients.""" - mock_controller.mock_client_responses.append([CLIENT_2]) - mock_controller.mock_device_responses.append({}) - mock_controller.mock_client_all_responses.append([CLIENT_1]) - options = {unifi.CONF_BLOCK_CLIENT: True} - config_entry = config_entries.ConfigEntry( - 1, - unifi.DOMAIN, - "Mock Title", - ENTRY_CONFIG, - "test", - config_entries.CONN_CLASS_LOCAL_POLL, - entry_id=1, + version=1, + domain=unifi.DOMAIN, + title="Mock Title", + data=ENTRY_CONFIG, + source="test", + connection_class=config_entries.CONN_CLASS_LOCAL_POLL, system_options={}, + options={}, + entry_id=1, ) registry = await entity_registry.async_get_registry(hass) registry.async_get_or_create( device_tracker.DOMAIN, unifi.DOMAIN, - "{}-mock-site".format(CLIENT_1["mac"]), + "{}-site_id".format(CLIENT_1["mac"]), suggested_object_id=CLIENT_1["hostname"], config_entry=config_entry, ) registry.async_get_or_create( device_tracker.DOMAIN, unifi.DOMAIN, - "{}-mock-site".format(CLIENT_2["mac"]), + "{}-site_id".format(CLIENT_2["mac"]), suggested_object_id=CLIENT_2["hostname"], config_entry=config_entry, ) - await setup_controller(hass, mock_controller, options) - assert len(mock_controller.mock_requests) == 3 + await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={unifi.CONF_BLOCK_CLIENT: True}, + sites=SITES, + clients_response=[CLIENT_2], + devices_response=[], + clients_all_response=[CLIENT_1], + ) assert len(hass.states.async_all()) == 4 device_1 = hass.states.get("device_tracker.client_1") assert device_1 is not None -async def test_dont_track_clients(hass, mock_controller): +async def test_dont_track_clients(hass): """Test dont track clients config works.""" - mock_controller.mock_client_responses.append([CLIENT_1]) - mock_controller.mock_device_responses.append([DEVICE_1]) - options = {unifi.controller.CONF_TRACK_CLIENTS: False} - - await setup_controller(hass, mock_controller, options) - assert len(mock_controller.mock_requests) == 2 + await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={unifi.controller.CONF_TRACK_CLIENTS: False}, + sites=SITES, + clients_response=[CLIENT_1], + devices_response=[DEVICE_1], + clients_all_response=[], + ) assert len(hass.states.async_all()) == 3 client_1 = hass.states.get("device_tracker.client_1") @@ -357,14 +282,17 @@ async def test_dont_track_clients(hass, mock_controller): assert device_1.state == "not_home" -async def test_dont_track_devices(hass, mock_controller): +async def test_dont_track_devices(hass): """Test dont track devices config works.""" - mock_controller.mock_client_responses.append([CLIENT_1]) - mock_controller.mock_device_responses.append([DEVICE_1]) - options = {unifi.controller.CONF_TRACK_DEVICES: False} - - await setup_controller(hass, mock_controller, options) - assert len(mock_controller.mock_requests) == 2 + await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={unifi.controller.CONF_TRACK_DEVICES: False}, + sites=SITES, + clients_response=[CLIENT_1], + devices_response=[DEVICE_1], + clients_all_response=[], + ) assert len(hass.states.async_all()) == 3 client_1 = hass.states.get("device_tracker.client_1") @@ -375,14 +303,17 @@ async def test_dont_track_devices(hass, mock_controller): assert device_1 is None -async def test_dont_track_wired_clients(hass, mock_controller): +async def test_dont_track_wired_clients(hass): """Test dont track wired clients config works.""" - mock_controller.mock_client_responses.append([CLIENT_1, CLIENT_2]) - mock_controller.mock_device_responses.append({}) - options = {unifi.controller.CONF_TRACK_WIRED_CLIENTS: False} - - await setup_controller(hass, mock_controller, options) - assert len(mock_controller.mock_requests) == 2 + await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={unifi.controller.CONF_TRACK_WIRED_CLIENTS: False}, + sites=SITES, + clients_response=[CLIENT_1, CLIENT_2], + devices_response=[], + clients_all_response=[], + ) assert len(hass.states.async_all()) == 3 client_1 = hass.states.get("device_tracker.client_1") diff --git a/tests/components/unifi/test_init.py b/tests/components/unifi/test_init.py index ffd6d97e5b3..6b17b803390 100644 --- a/tests/components/unifi/test_init.py +++ b/tests/components/unifi/test_init.py @@ -2,20 +2,9 @@ from unittest.mock import Mock, patch from homeassistant.components import unifi -from homeassistant.components.unifi import config_flow + from homeassistant.setup import async_setup_component -from homeassistant.components.unifi.const import ( - CONF_CONTROLLER, - CONF_SITE_ID, - CONTROLLER_ID as CONF_CONTROLLER_ID, -) -from homeassistant.const import ( - CONF_HOST, - CONF_PASSWORD, - CONF_PORT, - CONF_USERNAME, - CONF_VERIFY_SSL, -) + from tests.common import mock_coro, MockConfigEntry @@ -117,8 +106,7 @@ async def test_controller_fail_setup(hass): mock_cntrlr.return_value.async_setup.return_value = mock_coro(False) assert await unifi.async_setup_entry(hass, entry) is False - controller_id = CONF_CONTROLLER_ID.format(host="0.0.0.0", site="default") - assert controller_id in hass.data[unifi.DOMAIN] + assert hass.data[unifi.DOMAIN] == {} async def test_controller_no_mac(hass): @@ -184,164 +172,3 @@ async def test_unload_entry(hass): assert await unifi.async_unload_entry(hass, entry) assert len(mock_controller.return_value.async_reset.mock_calls) == 1 assert hass.data[unifi.DOMAIN] == {} - - -async def test_flow_works(hass, aioclient_mock): - """Test config flow.""" - flow = config_flow.UnifiFlowHandler() - flow.hass = hass - - with patch("aiounifi.Controller") as mock_controller: - - def mock_constructor( - host, username, password, port, site, websession, sslcontext - ): - """Fake the controller constructor.""" - mock_controller.host = host - mock_controller.username = username - mock_controller.password = password - mock_controller.port = port - mock_controller.site = site - return mock_controller - - mock_controller.side_effect = mock_constructor - mock_controller.login.return_value = mock_coro() - mock_controller.sites.return_value = mock_coro( - {"site1": {"name": "default", "role": "admin", "desc": "site name"}} - ) - - await flow.async_step_user( - user_input={ - CONF_HOST: "1.2.3.4", - CONF_USERNAME: "username", - CONF_PASSWORD: "password", - CONF_PORT: 1234, - CONF_VERIFY_SSL: True, - } - ) - - result = await flow.async_step_site(user_input={}) - - assert mock_controller.host == "1.2.3.4" - assert len(mock_controller.login.mock_calls) == 1 - assert len(mock_controller.sites.mock_calls) == 1 - - assert result["type"] == "create_entry" - assert result["title"] == "site name" - assert result["data"] == { - CONF_CONTROLLER: { - CONF_HOST: "1.2.3.4", - CONF_USERNAME: "username", - CONF_PASSWORD: "password", - CONF_PORT: 1234, - CONF_SITE_ID: "default", - CONF_VERIFY_SSL: True, - } - } - - -async def test_controller_multiple_sites(hass): - """Test config flow.""" - flow = config_flow.UnifiFlowHandler() - flow.hass = hass - - flow.config = { - CONF_HOST: "1.2.3.4", - CONF_USERNAME: "username", - CONF_PASSWORD: "password", - } - flow.sites = { - "site1": {"name": "default", "role": "admin", "desc": "site name"}, - "site2": {"name": "site2", "role": "admin", "desc": "site2 name"}, - } - - result = await flow.async_step_site() - - assert result["type"] == "form" - assert result["step_id"] == "site" - - assert result["data_schema"]({"site": "site name"}) - assert result["data_schema"]({"site": "site2 name"}) - - -async def test_controller_site_already_configured(hass): - """Test config flow.""" - flow = config_flow.UnifiFlowHandler() - flow.hass = hass - - entry = MockConfigEntry( - domain=unifi.DOMAIN, data={"controller": {"host": "1.2.3.4", "site": "default"}} - ) - entry.add_to_hass(hass) - - flow.config = { - CONF_HOST: "1.2.3.4", - CONF_USERNAME: "username", - CONF_PASSWORD: "password", - } - flow.desc = "site name" - flow.sites = {"site1": {"name": "default", "role": "admin", "desc": "site name"}} - - result = await flow.async_step_site() - - assert result["type"] == "abort" - - -async def test_user_credentials_faulty(hass, aioclient_mock): - """Test config flow.""" - flow = config_flow.UnifiFlowHandler() - flow.hass = hass - - with patch.object( - config_flow, "get_controller", side_effect=unifi.errors.AuthenticationRequired - ): - result = await flow.async_step_user( - { - CONF_HOST: "1.2.3.4", - CONF_USERNAME: "username", - CONF_PASSWORD: "password", - CONF_SITE_ID: "default", - } - ) - - assert result["type"] == "form" - assert result["errors"] == {"base": "faulty_credentials"} - - -async def test_controller_is_unavailable(hass, aioclient_mock): - """Test config flow.""" - flow = config_flow.UnifiFlowHandler() - flow.hass = hass - - with patch.object( - config_flow, "get_controller", side_effect=unifi.errors.CannotConnect - ): - result = await flow.async_step_user( - { - CONF_HOST: "1.2.3.4", - CONF_USERNAME: "username", - CONF_PASSWORD: "password", - CONF_SITE_ID: "default", - } - ) - - assert result["type"] == "form" - assert result["errors"] == {"base": "service_unavailable"} - - -async def test_controller_unkown_problem(hass, aioclient_mock): - """Test config flow.""" - flow = config_flow.UnifiFlowHandler() - flow.hass = hass - - with patch.object(config_flow, "get_controller", side_effect=Exception): - result = await flow.async_step_user( - { - CONF_HOST: "1.2.3.4", - CONF_USERNAME: "username", - CONF_PASSWORD: "password", - CONF_SITE_ID: "default", - } - ) - - assert result["type"] == "abort" diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py new file mode 100644 index 00000000000..f591801a966 --- /dev/null +++ b/tests/components/unifi/test_sensor.py @@ -0,0 +1,112 @@ +"""UniFi sensor platform tests.""" +from copy import deepcopy + +from homeassistant.components import unifi +from homeassistant.setup import async_setup_component + +import homeassistant.components.sensor as sensor + +from .test_controller import ENTRY_CONFIG, SITES, setup_unifi_integration + +CLIENTS = [ + { + "hostname": "Wired client hostname", + "ip": "10.0.0.1", + "is_wired": True, + "last_seen": 1562600145, + "mac": "00:00:00:00:00:01", + "name": "Wired client name", + "oui": "Producer", + "sw_mac": "00:00:00:00:01:01", + "sw_port": 1, + "wired-rx_bytes": 1234000000, + "wired-tx_bytes": 5678000000, + }, + { + "hostname": "Wireless client hostname", + "ip": "10.0.0.2", + "is_wired": False, + "last_seen": 1562600145, + "mac": "00:00:00:00:00:02", + "name": "Wireless client name", + "oui": "Producer", + "sw_mac": "00:00:00:00:01:01", + "sw_port": 2, + "rx_bytes": 1234000000, + "tx_bytes": 5678000000, + }, +] + + +async def test_platform_manually_configured(hass): + """Test that we do not discover anything or try to set up a controller.""" + assert ( + await async_setup_component( + hass, sensor.DOMAIN, {sensor.DOMAIN: {"platform": "unifi"}} + ) + is True + ) + assert unifi.DOMAIN not in hass.data + + +async def test_no_clients(hass): + """Test the update_clients function when no clients are found.""" + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={unifi.const.CONF_ALLOW_BANDWIDTH_SENSORS: True}, + sites=SITES, + clients_response=[], + devices_response=[], + clients_all_response=[], + ) + + assert len(controller.mock_requests) == 3 + assert len(hass.states.async_all()) == 2 + + +async def test_sensors(hass): + """Test the update_items function with some clients.""" + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={ + unifi.const.CONF_ALLOW_BANDWIDTH_SENSORS: True, + unifi.const.CONF_TRACK_CLIENTS: False, + unifi.const.CONF_TRACK_DEVICES: False, + }, + sites=SITES, + clients_response=CLIENTS, + devices_response=[], + clients_all_response=[], + ) + + assert len(controller.mock_requests) == 3 + assert len(hass.states.async_all()) == 6 + + wired_client_rx = hass.states.get("sensor.wired_client_name_rx") + assert wired_client_rx.state == "1234.0" + + wired_client_tx = hass.states.get("sensor.wired_client_name_tx") + assert wired_client_tx.state == "5678.0" + + wireless_client_rx = hass.states.get("sensor.wireless_client_name_rx") + assert wireless_client_rx.state == "1234.0" + + wireless_client_tx = hass.states.get("sensor.wireless_client_name_tx") + assert wireless_client_tx.state == "5678.0" + + clients = deepcopy(CLIENTS) + clients[0]["is_wired"] = False + clients[1]["rx_bytes"] = 2345000000 + clients[1]["tx_bytes"] = 6789000000 + + controller.mock_client_responses.append(clients) + await controller.async_update() + await hass.async_block_till_done() + + wireless_client_rx = hass.states.get("sensor.wireless_client_name_rx") + assert wireless_client_rx.state == "2345.0" + + wireless_client_tx = hass.states.get("sensor.wireless_client_name_tx") + assert wireless_client_tx.state == "6789.0" diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py index 7ea5e0680b9..3d754fb5dff 100644 --- a/tests/components/unifi/test_switch.py +++ b/tests/components/unifi/test_switch.py @@ -1,40 +1,25 @@ """UniFi POE control platform tests.""" -from collections import deque -from unittest.mock import Mock - -import pytest - -from tests.common import mock_coro - -import aiounifi -from aiounifi.clients import Clients, ClientsAll -from aiounifi.devices import Devices +from copy import deepcopy from homeassistant import config_entries from homeassistant.components import unifi -from homeassistant.components.unifi.const import ( - CONF_CONTROLLER, - CONF_SITE_ID, - CONTROLLER_ID as CONF_CONTROLLER_ID, - UNIFI_CONFIG, - UNIFI_WIRELESS_CLIENTS, -) from homeassistant.helpers import entity_registry from homeassistant.setup import async_setup_component -from homeassistant.const import ( - CONF_HOST, - CONF_PASSWORD, - CONF_PORT, - CONF_USERNAME, - CONF_VERIFY_SSL, -) import homeassistant.components.switch as switch +from .test_controller import ( + CONTROLLER_HOST, + ENTRY_CONFIG, + SITES, + setup_unifi_integration, +) + CLIENT_1 = { "hostname": "client_1", "ip": "10.0.0.1", "is_wired": True, + "last_seen": 1562600145, "mac": "00:00:00:00:00:01", "name": "POE Client 1", "oui": "Producer", @@ -47,6 +32,7 @@ CLIENT_2 = { "hostname": "client_2", "ip": "10.0.0.2", "is_wired": True, + "last_seen": 1562600145, "mac": "00:00:00:00:00:02", "name": "POE Client 2", "oui": "Producer", @@ -59,6 +45,7 @@ CLIENT_3 = { "hostname": "client_3", "ip": "10.0.0.3", "is_wired": True, + "last_seen": 1562600145, "mac": "00:00:00:00:00:03", "name": "Non-POE Client 3", "oui": "Producer", @@ -71,6 +58,7 @@ CLIENT_4 = { "hostname": "client_4", "ip": "10.0.0.4", "is_wired": True, + "last_seen": 1562600145, "mac": "00:00:00:00:00:04", "name": "Non-POE Client 4", "oui": "Producer", @@ -79,23 +67,12 @@ CLIENT_4 = { "wired-rx_bytes": 1234000000, "wired-tx_bytes": 5678000000, } -CLOUDKEY = { - "hostname": "client_1", - "ip": "mock-host", - "is_wired": True, - "mac": "10:00:00:00:00:01", - "name": "Cloud key", - "oui": "Producer", - "sw_mac": "00:00:00:00:01:01", - "sw_port": 1, - "wired-rx_bytes": 1234000000, - "wired-tx_bytes": 5678000000, -} POE_SWITCH_CLIENTS = [ { "hostname": "client_1", "ip": "10.0.0.1", "is_wired": True, + "last_seen": 1562600145, "mac": "00:00:00:00:00:01", "name": "POE Client 1", "oui": "Producer", @@ -108,6 +85,7 @@ POE_SWITCH_CLIENTS = [ "hostname": "client_2", "ip": "10.0.0.2", "is_wired": True, + "last_seen": 1562600145, "mac": "00:00:00:00:00:02", "name": "POE Client 2", "oui": "Producer", @@ -122,7 +100,8 @@ DEVICE_1 = { "device_id": "mock-id", "ip": "10.0.1.1", "mac": "00:00:00:00:01:01", - "type": "usw", + "last_seen": 1562600145, + "model": "US16P150", "name": "mock-name", "port_overrides": [], "port_table": [ @@ -179,6 +158,9 @@ DEVICE_1 = { "up": True, }, ], + "state": 1, + "type": "usw", + "version": "4.0.42.10433", } BLOCKED = { @@ -204,139 +186,102 @@ UNBLOCKED = { "oui": "Producer", } -CONTROLLER_DATA = { - CONF_HOST: "mock-host", - CONF_USERNAME: "mock-user", - CONF_PASSWORD: "mock-pswd", - CONF_PORT: 1234, - CONF_SITE_ID: "mock-site", - CONF_VERIFY_SSL: True, -} - -ENTRY_CONFIG = {CONF_CONTROLLER: CONTROLLER_DATA} - -CONTROLLER_ID = CONF_CONTROLLER_ID.format(host="mock-host", site="mock-site") - - -@pytest.fixture -def mock_controller(hass): - """Mock a UniFi Controller.""" - hass.data[UNIFI_CONFIG] = {} - hass.data[UNIFI_WIRELESS_CLIENTS] = Mock() - controller = unifi.UniFiController(hass, None) - controller.wireless_clients = set() - - controller._site_role = "admin" - - controller.api = Mock() - controller.mock_requests = [] - - controller.mock_client_responses = deque() - controller.mock_device_responses = deque() - controller.mock_client_all_responses = deque() - - async def mock_request(method, path, **kwargs): - kwargs["method"] = method - kwargs["path"] = path - controller.mock_requests.append(kwargs) - if path == "s/{site}/stat/sta": - return controller.mock_client_responses.popleft() - if path == "s/{site}/stat/device": - return controller.mock_device_responses.popleft() - if path == "s/{site}/rest/user": - return controller.mock_client_all_responses.popleft() - return None - - controller.api.clients = Clients({}, mock_request) - controller.api.devices = Devices({}, mock_request) - controller.api.clients_all = ClientsAll({}, mock_request) - - return controller - - -async def setup_controller(hass, mock_controller, options={}): - """Load the UniFi switch platform with the provided controller.""" - hass.config.components.add(unifi.DOMAIN) - hass.data[unifi.DOMAIN] = {CONTROLLER_ID: mock_controller} - config_entry = config_entries.ConfigEntry( - 1, - unifi.DOMAIN, - "Mock Title", - ENTRY_CONFIG, - "test", - config_entries.CONN_CLASS_LOCAL_POLL, - entry_id=1, - system_options={}, - options=options, - ) - mock_controller.config_entry = config_entry - - await mock_controller.async_update() - await hass.config_entries.async_forward_entry_setup(config_entry, "switch") - # To flush out the service call to update the group - await hass.async_block_till_done() - async def test_platform_manually_configured(hass): - """Test that we do not discover anything or try to set up a bridge.""" + """Test that we do not discover anything or try to set up a controller.""" assert ( await async_setup_component( - hass, switch.DOMAIN, {"switch": {"platform": "unifi"}} + hass, switch.DOMAIN, {switch.DOMAIN: {"platform": "unifi"}} ) is True ) assert unifi.DOMAIN not in hass.data -async def test_no_clients(hass, mock_controller): +async def test_no_clients(hass): """Test the update_clients function when no clients are found.""" - mock_controller.mock_client_responses.append({}) - mock_controller.mock_device_responses.append({}) - await setup_controller(hass, mock_controller) - assert len(mock_controller.mock_requests) == 2 - assert not hass.states.async_all() + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={ + unifi.const.CONF_TRACK_CLIENTS: False, + unifi.const.CONF_TRACK_DEVICES: False, + }, + sites=SITES, + clients_response=[], + devices_response=[], + clients_all_response=[], + ) + + assert len(controller.mock_requests) == 3 + assert len(hass.states.async_all()) == 2 -async def test_controller_not_client(hass, mock_controller): +async def test_controller_not_client(hass): """Test that the controller doesn't become a switch.""" - mock_controller.mock_client_responses.append([CLOUDKEY]) - mock_controller.mock_device_responses.append([DEVICE_1]) - await setup_controller(hass, mock_controller) - assert len(mock_controller.mock_requests) == 2 - assert not hass.states.async_all() + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={ + unifi.const.CONF_TRACK_CLIENTS: False, + unifi.const.CONF_TRACK_DEVICES: False, + }, + sites=SITES, + clients_response=[CONTROLLER_HOST], + devices_response=[DEVICE_1], + clients_all_response=[], + ) + + assert len(controller.mock_requests) == 3 + assert len(hass.states.async_all()) == 2 cloudkey = hass.states.get("switch.cloud_key") assert cloudkey is None -async def test_not_admin(hass, mock_controller): +async def test_not_admin(hass): """Test that switch platform only work on an admin account.""" - mock_controller.mock_client_responses.append([CLIENT_1]) - mock_controller.mock_device_responses.append([]) + sites = deepcopy(SITES) + sites["Site name"]["role"] = "not admin" + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={ + unifi.const.CONF_TRACK_CLIENTS: False, + unifi.const.CONF_TRACK_DEVICES: False, + }, + sites=sites, + clients_response=[CLIENT_1], + devices_response=[DEVICE_1], + clients_all_response=[], + ) - mock_controller._site_role = "viewer" - - await setup_controller(hass, mock_controller) - assert len(mock_controller.mock_requests) == 2 - assert len(hass.states.async_all()) == 0 + assert len(controller.mock_requests) == 3 + assert len(hass.states.async_all()) == 2 -async def test_switches(hass, mock_controller): +async def test_switches(hass): """Test the update_items function with some clients.""" - mock_controller.mock_client_responses.append([CLIENT_1, CLIENT_4]) - mock_controller.mock_device_responses.append([DEVICE_1]) - mock_controller.mock_client_all_responses.append([BLOCKED, UNBLOCKED, CLIENT_1]) - options = {unifi.CONF_BLOCK_CLIENT: [BLOCKED["mac"], UNBLOCKED["mac"]]} + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={ + unifi.CONF_BLOCK_CLIENT: [BLOCKED["mac"], UNBLOCKED["mac"]], + unifi.const.CONF_TRACK_CLIENTS: False, + unifi.const.CONF_TRACK_DEVICES: False, + }, + sites=SITES, + clients_response=[CLIENT_1, CLIENT_4], + devices_response=[DEVICE_1], + clients_all_response=[BLOCKED, UNBLOCKED, CLIENT_1], + ) - await setup_controller(hass, mock_controller, options) - assert len(mock_controller.mock_requests) == 3 - assert len(hass.states.async_all()) == 4 + assert len(controller.mock_requests) == 3 + assert len(hass.states.async_all()) == 6 switch_1 = hass.states.get("switch.poe_client_1") assert switch_1 is not None assert switch_1.state == "on" assert switch_1.attributes["power"] == "2.56" - assert switch_1.attributes["received"] == 1234 - assert switch_1.attributes["sent"] == 5678 assert switch_1.attributes["switch"] == "00:00:00:00:01:01" assert switch_1.attributes["port"] == 1 assert switch_1.attributes["poe_mode"] == "auto" @@ -353,25 +298,78 @@ async def test_switches(hass, mock_controller): assert unblocked.state == "on" -async def test_new_client_discovered(hass, mock_controller): +async def test_new_client_discovered_on_block_control(hass): """Test if 2nd update has a new client.""" - mock_controller.mock_client_responses.append([CLIENT_1]) - mock_controller.mock_device_responses.append([DEVICE_1]) + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={ + unifi.CONF_BLOCK_CLIENT: [BLOCKED["mac"]], + unifi.const.CONF_TRACK_CLIENTS: False, + unifi.const.CONF_TRACK_DEVICES: False, + }, + sites=SITES, + clients_response=[], + devices_response=[], + clients_all_response=[BLOCKED], + ) - await setup_controller(hass, mock_controller) - assert len(mock_controller.mock_requests) == 2 - assert len(hass.states.async_all()) == 2 + assert len(controller.mock_requests) == 3 + assert len(hass.states.async_all()) == 4 - mock_controller.mock_client_responses.append([CLIENT_1, CLIENT_2]) - mock_controller.mock_device_responses.append([DEVICE_1]) + controller.mock_client_all_responses.append([BLOCKED]) + + # Calling a service will trigger the updates to run + await hass.services.async_call( + "switch", "turn_off", {"entity_id": "switch.block_client_1"}, blocking=True + ) + assert len(controller.mock_requests) == 7 + assert len(hass.states.async_all()) == 4 + assert controller.mock_requests[3] == { + "json": {"mac": "00:00:00:00:01:01", "cmd": "block-sta"}, + "method": "post", + "path": "s/{site}/cmd/stamgr/", + } + + await hass.services.async_call( + "switch", "turn_on", {"entity_id": "switch.block_client_1"}, blocking=True + ) + assert len(controller.mock_requests) == 11 + assert controller.mock_requests[7] == { + "json": {"mac": "00:00:00:00:01:01", "cmd": "unblock-sta"}, + "method": "post", + "path": "s/{site}/cmd/stamgr/", + } + + +async def test_new_client_discovered_on_poe_control(hass): + """Test if 2nd update has a new client.""" + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={ + unifi.const.CONF_TRACK_CLIENTS: False, + unifi.const.CONF_TRACK_DEVICES: False, + }, + sites=SITES, + clients_response=[CLIENT_1], + devices_response=[DEVICE_1], + clients_all_response=[], + ) + + assert len(controller.mock_requests) == 3 + assert len(hass.states.async_all()) == 4 + + controller.mock_client_responses.append([CLIENT_1, CLIENT_2]) + controller.mock_device_responses.append([DEVICE_1]) # Calling a service will trigger the updates to run await hass.services.async_call( "switch", "turn_off", {"entity_id": "switch.poe_client_1"}, blocking=True ) - assert len(mock_controller.mock_requests) == 5 - assert len(hass.states.async_all()) == 3 - assert mock_controller.mock_requests[2] == { + assert len(controller.mock_requests) == 6 + assert len(hass.states.async_all()) == 5 + assert controller.mock_requests[3] == { "json": { "port_overrides": [{"port_idx": 1, "portconf_id": "1a1", "poe_mode": "off"}] }, @@ -382,8 +380,8 @@ async def test_new_client_discovered(hass, mock_controller): await hass.services.async_call( "switch", "turn_on", {"entity_id": "switch.poe_client_1"}, blocking=True ) - assert len(mock_controller.mock_requests) == 7 - assert mock_controller.mock_requests[5] == { + assert len(controller.mock_requests) == 9 + assert controller.mock_requests[3] == { "json": { "port_overrides": [ {"port_idx": 1, "portconf_id": "1a1", "poe_mode": "auto"} @@ -398,66 +396,24 @@ async def test_new_client_discovered(hass, mock_controller): assert switch_2.state == "on" -async def test_failed_update_successful_login(hass, mock_controller): - """Running update can login when requested.""" - mock_controller.available = False - mock_controller.api.clients.update = Mock() - mock_controller.api.clients.update.side_effect = aiounifi.LoginRequired - mock_controller.api.login = Mock() - mock_controller.api.login.return_value = mock_coro() - - await setup_controller(hass, mock_controller) - assert len(mock_controller.mock_requests) == 0 - - assert mock_controller.available is True - - -async def test_failed_update_failed_login(hass, mock_controller): - """Running update can handle a failed login.""" - mock_controller.api.clients.update = Mock() - mock_controller.api.clients.update.side_effect = aiounifi.LoginRequired - mock_controller.api.login = Mock() - mock_controller.api.login.side_effect = aiounifi.AiounifiException - - await setup_controller(hass, mock_controller) - assert len(mock_controller.mock_requests) == 0 - - assert mock_controller.available is False - - -async def test_failed_update_unreachable_controller(hass, mock_controller): - """Running update can handle a unreachable controller.""" - mock_controller.mock_client_responses.append([CLIENT_1, CLIENT_2]) - mock_controller.mock_device_responses.append([DEVICE_1]) - - await setup_controller(hass, mock_controller) - - mock_controller.api.clients.update = Mock() - mock_controller.api.clients.update.side_effect = aiounifi.AiounifiException - - # Calling a service will trigger the updates to run - await hass.services.async_call( - "switch", "turn_off", {"entity_id": "switch.poe_client_1"}, blocking=True - ) - - assert len(mock_controller.mock_requests) == 3 - assert len(hass.states.async_all()) == 3 - - assert mock_controller.available is False - - -async def test_ignore_multiple_poe_clients_on_same_port(hass, mock_controller): +async def test_ignore_multiple_poe_clients_on_same_port(hass): """Ignore when there are multiple POE driven clients on same port. If there is a non-UniFi switch powered by POE, clients will be transparently marked as having POE as well. """ - mock_controller.mock_client_responses.append(POE_SWITCH_CLIENTS) - mock_controller.mock_device_responses.append([DEVICE_1]) - await setup_controller(hass, mock_controller) - assert len(mock_controller.mock_requests) == 2 - # 1 All Lights group, 2 lights - assert len(hass.states.async_all()) == 0 + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={}, + sites=SITES, + clients_response=POE_SWITCH_CLIENTS, + devices_response=[DEVICE_1], + clients_all_response=[], + ) + + assert len(controller.mock_requests) == 3 + assert len(hass.states.async_all()) == 5 switch_1 = hass.states.get("switch.poe_client_1") switch_2 = hass.states.get("switch.poe_client_2") @@ -465,22 +421,18 @@ async def test_ignore_multiple_poe_clients_on_same_port(hass, mock_controller): assert switch_2 is None -async def test_restoring_client(hass, mock_controller): +async def test_restoring_client(hass): """Test the update_items function with some clients.""" - mock_controller.mock_client_responses.append([CLIENT_2]) - mock_controller.mock_device_responses.append([DEVICE_1]) - mock_controller.mock_client_all_responses.append([CLIENT_1]) - options = {unifi.CONF_BLOCK_CLIENT: ["random mac"]} - config_entry = config_entries.ConfigEntry( - 1, - unifi.DOMAIN, - "Mock Title", - ENTRY_CONFIG, - "test", - config_entries.CONN_CLASS_LOCAL_POLL, - entry_id=1, + version=1, + domain=unifi.DOMAIN, + title="Mock Title", + data=ENTRY_CONFIG, + source="test", + connection_class=config_entries.CONN_CLASS_LOCAL_POLL, system_options={}, + options={}, + entry_id=1, ) registry = await entity_registry.async_get_registry(hass) @@ -499,9 +451,22 @@ async def test_restoring_client(hass, mock_controller): config_entry=config_entry, ) - await setup_controller(hass, mock_controller, options) - assert len(mock_controller.mock_requests) == 3 - assert len(hass.states.async_all()) == 3 + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={ + unifi.CONF_BLOCK_CLIENT: ["random mac"], + unifi.const.CONF_TRACK_CLIENTS: False, + unifi.const.CONF_TRACK_DEVICES: False, + }, + sites=SITES, + clients_response=[CLIENT_2], + devices_response=[DEVICE_1], + clients_all_response=[CLIENT_1], + ) + + assert len(controller.mock_requests) == 3 + assert len(hass.states.async_all()) == 5 device_1 = hass.states.get("switch.client_1") assert device_1 is not None diff --git a/tests/components/upnp/test_init.py b/tests/components/upnp/test_init.py index 5f17606146b..5e2106ff208 100644 --- a/tests/components/upnp/test_init.py +++ b/tests/components/upnp/test_init.py @@ -59,17 +59,20 @@ async def test_async_setup_entry_default(hass): } with MockDependency("netdisco.discovery"), patch( "homeassistant.components.upnp.get_local_ip", return_value="192.168.1.10" - ): + ), patch.object(Device, "async_create_device") as create_device, patch.object( + Device, "async_create_device" + ) as create_device, patch.object( + Device, "async_discover", return_value=mock_coro([]) + ) as async_discover: await async_setup_component(hass, "http", config) await async_setup_component(hass, "upnp", config) await hass.async_block_till_done() - # mock homeassistant.components.upnp.device.Device - mock_device = MockDevice(udn) - discovery_infos = [{"udn": udn, "ssdp_description": "http://192.168.1.1/desc.xml"}] - with patch.object(Device, "async_create_device") as create_device, patch.object( - Device, "async_discover" - ) as async_discover: # noqa:E125 + # mock homeassistant.components.upnp.device.Device + mock_device = MockDevice(udn) + discovery_infos = [ + {"udn": udn, "ssdp_description": "http://192.168.1.1/desc.xml"} + ] create_device.return_value = mock_coro(return_value=mock_device) async_discover.return_value = mock_coro(return_value=discovery_infos) @@ -100,16 +103,17 @@ async def test_async_setup_entry_port_mapping(hass): } with MockDependency("netdisco.discovery"), patch( "homeassistant.components.upnp.get_local_ip", return_value="192.168.1.10" - ): + ), patch.object(Device, "async_create_device") as create_device, patch.object( + Device, "async_discover", return_value=mock_coro([]) + ) as async_discover: await async_setup_component(hass, "http", config) await async_setup_component(hass, "upnp", config) await hass.async_block_till_done() - mock_device = MockDevice(udn) - discovery_infos = [{"udn": udn, "ssdp_description": "http://192.168.1.1/desc.xml"}] - with patch.object(Device, "async_create_device") as create_device, patch.object( - Device, "async_discover" - ) as async_discover: # noqa:E125 + mock_device = MockDevice(udn) + discovery_infos = [ + {"udn": udn, "ssdp_description": "http://192.168.1.1/desc.xml"} + ] create_device.return_value = mock_coro(return_value=mock_device) async_discover.return_value = mock_coro(return_value=discovery_infos) diff --git a/tests/components/vacuum/test_reproduce_state.py b/tests/components/vacuum/test_reproduce_state.py new file mode 100644 index 00000000000..d5a7051e6a6 --- /dev/null +++ b/tests/components/vacuum/test_reproduce_state.py @@ -0,0 +1,139 @@ +"""Test reproduce state for Vacuum.""" +from homeassistant.components.vacuum import ( + ATTR_FAN_SPEED, + SERVICE_PAUSE, + SERVICE_RETURN_TO_BASE, + SERVICE_SET_FAN_SPEED, + SERVICE_START, + SERVICE_STOP, + STATE_CLEANING, + STATE_DOCKED, + STATE_RETURNING, +) +from homeassistant.const import ( + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_IDLE, + STATE_OFF, + STATE_ON, + STATE_PAUSED, +) +from homeassistant.core import State + +from tests.common import async_mock_service + +FAN_SPEED_LOW = "low" +FAN_SPEED_HIGH = "high" + + +async def test_reproducing_states(hass, caplog): + """Test reproducing Vacuum states.""" + hass.states.async_set("vacuum.entity_off", STATE_OFF, {}) + hass.states.async_set("vacuum.entity_on", STATE_ON, {}) + hass.states.async_set( + "vacuum.entity_on_fan", STATE_ON, {ATTR_FAN_SPEED: FAN_SPEED_LOW} + ) + hass.states.async_set("vacuum.entity_cleaning", STATE_CLEANING, {}) + hass.states.async_set("vacuum.entity_docked", STATE_DOCKED, {}) + hass.states.async_set("vacuum.entity_idle", STATE_IDLE, {}) + hass.states.async_set("vacuum.entity_returning", STATE_RETURNING, {}) + hass.states.async_set("vacuum.entity_paused", STATE_PAUSED, {}) + + turn_on_calls = async_mock_service(hass, "vacuum", SERVICE_TURN_ON) + turn_off_calls = async_mock_service(hass, "vacuum", SERVICE_TURN_OFF) + start_calls = async_mock_service(hass, "vacuum", SERVICE_START) + pause_calls = async_mock_service(hass, "vacuum", SERVICE_PAUSE) + stop_calls = async_mock_service(hass, "vacuum", SERVICE_STOP) + return_calls = async_mock_service(hass, "vacuum", SERVICE_RETURN_TO_BASE) + fan_speed_calls = async_mock_service(hass, "vacuum", SERVICE_SET_FAN_SPEED) + + # These calls should do nothing as entities already in desired state + await hass.helpers.state.async_reproduce_state( + [ + State("vacuum.entity_off", STATE_OFF), + State("vacuum.entity_on", STATE_ON), + State("vacuum.entity_on_fan", STATE_ON, {ATTR_FAN_SPEED: FAN_SPEED_LOW}), + State("vacuum.entity_cleaning", STATE_CLEANING), + State("vacuum.entity_docked", STATE_DOCKED), + State("vacuum.entity_idle", STATE_IDLE), + State("vacuum.entity_returning", STATE_RETURNING), + State("vacuum.entity_paused", STATE_PAUSED), + ], + blocking=True, + ) + + assert len(turn_on_calls) == 0 + assert len(turn_off_calls) == 0 + assert len(start_calls) == 0 + assert len(pause_calls) == 0 + assert len(stop_calls) == 0 + assert len(return_calls) == 0 + assert len(fan_speed_calls) == 0 + + # Test invalid state is handled + await hass.helpers.state.async_reproduce_state( + [State("vacuum.entity_off", "not_supported")], blocking=True + ) + + assert "not_supported" in caplog.text + assert len(turn_on_calls) == 0 + assert len(turn_off_calls) == 0 + assert len(start_calls) == 0 + assert len(pause_calls) == 0 + assert len(stop_calls) == 0 + assert len(return_calls) == 0 + assert len(fan_speed_calls) == 0 + + # Make sure correct services are called + await hass.helpers.state.async_reproduce_state( + [ + State("vacuum.entity_off", STATE_ON), + State("vacuum.entity_on", STATE_OFF), + State("vacuum.entity_on_fan", STATE_ON, {ATTR_FAN_SPEED: FAN_SPEED_HIGH}), + State("vacuum.entity_cleaning", STATE_PAUSED), + State("vacuum.entity_docked", STATE_CLEANING), + State("vacuum.entity_idle", STATE_DOCKED), + State("vacuum.entity_returning", STATE_CLEANING), + State("vacuum.entity_paused", STATE_IDLE), + # Should not raise + State("vacuum.non_existing", STATE_ON), + ], + blocking=True, + ) + + assert len(turn_on_calls) == 1 + assert turn_on_calls[0].domain == "vacuum" + assert turn_on_calls[0].data == {"entity_id": "vacuum.entity_off"} + + assert len(turn_off_calls) == 1 + assert turn_off_calls[0].domain == "vacuum" + assert turn_off_calls[0].data == {"entity_id": "vacuum.entity_on"} + + assert len(start_calls) == 2 + entities = [ + {"entity_id": "vacuum.entity_docked"}, + {"entity_id": "vacuum.entity_returning"}, + ] + for call in start_calls: + assert call.domain == "vacuum" + assert call.data in entities + entities.remove(call.data) + + assert len(pause_calls) == 1 + assert pause_calls[0].domain == "vacuum" + assert pause_calls[0].data == {"entity_id": "vacuum.entity_cleaning"} + + assert len(stop_calls) == 1 + assert stop_calls[0].domain == "vacuum" + assert stop_calls[0].data == {"entity_id": "vacuum.entity_paused"} + + assert len(return_calls) == 1 + assert return_calls[0].domain == "vacuum" + assert return_calls[0].data == {"entity_id": "vacuum.entity_idle"} + + assert len(fan_speed_calls) == 1 + assert fan_speed_calls[0].domain == "vacuum" + assert fan_speed_calls[0].data == { + "entity_id": "vacuum.entity_on_fan", + ATTR_FAN_SPEED: FAN_SPEED_HIGH, + } diff --git a/tests/components/wake_on_lan/test_init.py b/tests/components/wake_on_lan/test_init.py index d71f15e6109..c2ee0930895 100644 --- a/tests/components/wake_on_lan/test_init.py +++ b/tests/components/wake_on_lan/test_init.py @@ -1,52 +1,46 @@ """Tests for Wake On LAN component.""" -import asyncio -from unittest import mock - import pytest import voluptuous as vol -from homeassistant.setup import async_setup_component +from homeassistant.components import wake_on_lan from homeassistant.components.wake_on_lan import DOMAIN, SERVICE_SEND_MAGIC_PACKET +from homeassistant.setup import async_setup_component + +from tests.common import MockDependency -@pytest.fixture -def mock_wakeonlan(): - """Mock mock_wakeonlan.""" - module = mock.MagicMock() - with mock.patch.dict("sys.modules", {"wakeonlan": module}): - yield module - - -@asyncio.coroutine -def test_send_magic_packet(hass, caplog, mock_wakeonlan): +async def test_send_magic_packet(hass): """Test of send magic packet service call.""" - mac = "aa:bb:cc:dd:ee:ff" - bc_ip = "192.168.255.255" + with MockDependency("wakeonlan") as mocked_wakeonlan: + mac = "aa:bb:cc:dd:ee:ff" + bc_ip = "192.168.255.255" - yield from async_setup_component(hass, DOMAIN, {}) + wake_on_lan.wakeonlan = mocked_wakeonlan - yield from hass.services.async_call( - DOMAIN, - SERVICE_SEND_MAGIC_PACKET, - {"mac": mac, "broadcast_address": bc_ip}, - blocking=True, - ) - assert len(mock_wakeonlan.mock_calls) == 1 - assert mock_wakeonlan.mock_calls[-1][1][0] == mac - assert mock_wakeonlan.mock_calls[-1][2]["ip_address"] == bc_ip + await async_setup_component(hass, DOMAIN, {}) - with pytest.raises(vol.Invalid): - yield from hass.services.async_call( + await hass.services.async_call( DOMAIN, SERVICE_SEND_MAGIC_PACKET, - {"broadcast_address": bc_ip}, + {"mac": mac, "broadcast_address": bc_ip}, blocking=True, ) - assert len(mock_wakeonlan.mock_calls) == 1 + assert len(mocked_wakeonlan.mock_calls) == 1 + assert mocked_wakeonlan.mock_calls[-1][1][0] == mac + assert mocked_wakeonlan.mock_calls[-1][2]["ip_address"] == bc_ip - yield from hass.services.async_call( - DOMAIN, SERVICE_SEND_MAGIC_PACKET, {"mac": mac}, blocking=True - ) - assert len(mock_wakeonlan.mock_calls) == 2 - assert mock_wakeonlan.mock_calls[-1][1][0] == mac - assert not mock_wakeonlan.mock_calls[-1][2] + with pytest.raises(vol.Invalid): + await hass.services.async_call( + DOMAIN, + SERVICE_SEND_MAGIC_PACKET, + {"broadcast_address": bc_ip}, + blocking=True, + ) + assert len(mocked_wakeonlan.mock_calls) == 1 + + await hass.services.async_call( + DOMAIN, SERVICE_SEND_MAGIC_PACKET, {"mac": mac}, blocking=True + ) + assert len(mocked_wakeonlan.mock_calls) == 2 + assert mocked_wakeonlan.mock_calls[-1][1][0] == mac + assert not mocked_wakeonlan.mock_calls[-1][2] diff --git a/tests/components/websocket_api/__init__.py b/tests/components/websocket_api/__init__.py index 56def1b7fd9..4904270cc72 100644 --- a/tests/components/websocket_api/__init__.py +++ b/tests/components/websocket_api/__init__.py @@ -1,2 +1 @@ """Tests for the websocket API.""" -API_PASSWORD = "test-password" diff --git a/tests/components/websocket_api/conftest.py b/tests/components/websocket_api/conftest.py index 2ee28c0cb20..382de3142e8 100644 --- a/tests/components/websocket_api/conftest.py +++ b/tests/components/websocket_api/conftest.py @@ -5,8 +5,6 @@ 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 . import API_PASSWORD - @pytest.fixture def websocket_client(hass, hass_ws_client, hass_access_token): @@ -17,11 +15,7 @@ def websocket_client(hass, hass_ws_client, hass_access_token): @pytest.fixture def no_auth_websocket_client(hass, loop, aiohttp_client): """Websocket connection that requires authentication.""" - assert loop.run_until_complete( - async_setup_component( - hass, "websocket_api", {"http": {"api_password": API_PASSWORD}} - ) - ) + assert loop.run_until_complete(async_setup_component(hass, "websocket_api", {})) client = loop.run_until_complete(aiohttp_client(hass.http.app)) ws = loop.run_until_complete(client.ws_connect(URL)) diff --git a/tests/components/websocket_api/test_auth.py b/tests/components/websocket_api/test_auth.py index 19b9cbb2196..00387506020 100644 --- a/tests/components/websocket_api/test_auth.py +++ b/tests/components/websocket_api/test_auth.py @@ -17,21 +17,10 @@ from homeassistant.setup import async_setup_component from tests.common import mock_coro -from . import API_PASSWORD - -async def test_auth_via_msg(no_auth_websocket_client, legacy_auth): - """Test authenticating.""" - await no_auth_websocket_client.send_json( - {"type": TYPE_AUTH, "api_password": API_PASSWORD} - ) - - msg = await no_auth_websocket_client.receive_json() - - assert msg["type"] == TYPE_AUTH_OK - - -async def test_auth_events(hass, no_auth_websocket_client, legacy_auth): +async def test_auth_events( + hass, no_auth_websocket_client, legacy_auth, hass_access_token +): """Test authenticating.""" connected_evt = [] hass.helpers.dispatcher.async_dispatcher_connect( @@ -42,7 +31,7 @@ async def test_auth_events(hass, no_auth_websocket_client, legacy_auth): SIGNAL_WEBSOCKET_DISCONNECTED, lambda: disconnected_evt.append(1) ) - await test_auth_via_msg(no_auth_websocket_client, legacy_auth) + await test_auth_active_with_token(hass, no_auth_websocket_client, hass_access_token) assert len(connected_evt) == 1 assert not disconnected_evt @@ -60,7 +49,7 @@ async def test_auth_via_msg_incorrect_pass(no_auth_websocket_client): return_value=mock_coro(), ) as mock_process_wrong_login: await no_auth_websocket_client.send_json( - {"type": TYPE_AUTH, "api_password": API_PASSWORD + "wrong"} + {"type": TYPE_AUTH, "api_password": "wrong"} ) msg = await no_auth_websocket_client.receive_json() @@ -110,31 +99,25 @@ async def test_pre_auth_only_auth_allowed(no_auth_websocket_client): assert msg["message"].startswith("Auth message incorrectly formatted") -async def test_auth_active_with_token(hass, aiohttp_client, hass_access_token): +async def test_auth_active_with_token( + hass, no_auth_websocket_client, hass_access_token +): """Test authenticating with a token.""" - assert await async_setup_component( - hass, "websocket_api", {"http": {"api_password": API_PASSWORD}} + assert await async_setup_component(hass, "websocket_api", {}) + + await no_auth_websocket_client.send_json( + {"type": TYPE_AUTH, "access_token": hass_access_token} ) - client = await aiohttp_client(hass.http.app) - - async with client.ws_connect(URL) as ws: - auth_msg = await ws.receive_json() - assert auth_msg["type"] == TYPE_AUTH_REQUIRED - - await ws.send_json({"type": TYPE_AUTH, "access_token": hass_access_token}) - - auth_msg = await ws.receive_json() - assert auth_msg["type"] == TYPE_AUTH_OK + auth_msg = await no_auth_websocket_client.receive_json() + assert auth_msg["type"] == TYPE_AUTH_OK async def test_auth_active_user_inactive(hass, aiohttp_client, hass_access_token): """Test authenticating with a token.""" refresh_token = await hass.auth.async_validate_access_token(hass_access_token) refresh_token.user.is_active = False - assert await async_setup_component( - hass, "websocket_api", {"http": {"api_password": API_PASSWORD}} - ) + assert await async_setup_component(hass, "websocket_api", {}) client = await aiohttp_client(hass.http.app) @@ -150,9 +133,7 @@ async def test_auth_active_user_inactive(hass, aiohttp_client, hass_access_token async def test_auth_active_with_password_not_allow(hass, aiohttp_client): """Test authenticating with a token.""" - assert await async_setup_component( - hass, "websocket_api", {"http": {"api_password": API_PASSWORD}} - ) + assert await async_setup_component(hass, "websocket_api", {}) client = await aiohttp_client(hass.http.app) @@ -160,7 +141,7 @@ async def test_auth_active_with_password_not_allow(hass, aiohttp_client): auth_msg = await ws.receive_json() assert auth_msg["type"] == TYPE_AUTH_REQUIRED - await ws.send_json({"type": TYPE_AUTH, "api_password": API_PASSWORD}) + await ws.send_json({"type": TYPE_AUTH, "api_password": "some-password"}) auth_msg = await ws.receive_json() assert auth_msg["type"] == TYPE_AUTH_INVALID @@ -168,28 +149,23 @@ async def test_auth_active_with_password_not_allow(hass, aiohttp_client): async def test_auth_legacy_support_with_password(hass, aiohttp_client, legacy_auth): """Test authenticating with a token.""" - assert await async_setup_component( - hass, "websocket_api", {"http": {"api_password": API_PASSWORD}} - ) + assert await async_setup_component(hass, "websocket_api", {}) client = await aiohttp_client(hass.http.app) async with client.ws_connect(URL) as ws: - with patch("homeassistant.auth.AuthManager.support_legacy", return_value=True): - auth_msg = await ws.receive_json() - assert auth_msg["type"] == TYPE_AUTH_REQUIRED + auth_msg = await ws.receive_json() + assert auth_msg["type"] == TYPE_AUTH_REQUIRED - await ws.send_json({"type": TYPE_AUTH, "api_password": API_PASSWORD}) + await ws.send_json({"type": TYPE_AUTH, "api_password": "some-password"}) - auth_msg = await ws.receive_json() - assert auth_msg["type"] == TYPE_AUTH_OK + auth_msg = await ws.receive_json() + assert auth_msg["type"] == TYPE_AUTH_INVALID async def test_auth_with_invalid_token(hass, aiohttp_client): """Test authenticating with a token.""" - assert await async_setup_component( - hass, "websocket_api", {"http": {"api_password": API_PASSWORD}} - ) + assert await async_setup_component(hass, "websocket_api", {}) client = await aiohttp_client(hass.http.app) diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index a39a0a0e7a6..1de5b8bb2c1 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -14,8 +14,6 @@ from homeassistant.setup import async_setup_component from tests.common import async_mock_service -from . import API_PASSWORD - async def test_call_service(hass, websocket_client): """Test call service command.""" @@ -250,9 +248,7 @@ async def test_ping(websocket_client): async def test_call_service_context_with_user(hass, aiohttp_client, hass_access_token): """Test that the user is set in the service call context.""" - assert await async_setup_component( - hass, "websocket_api", {"http": {"api_password": API_PASSWORD}} - ) + assert await async_setup_component(hass, "websocket_api", {}) calls = async_mock_service(hass, "domain_test", "test_service") client = await aiohttp_client(hass.http.app) diff --git a/tests/components/websocket_api/test_sensor.py b/tests/components/websocket_api/test_sensor.py index 873b9e7269c..84b73060698 100644 --- a/tests/components/websocket_api/test_sensor.py +++ b/tests/components/websocket_api/test_sensor.py @@ -3,10 +3,12 @@ from homeassistant.bootstrap import async_setup_component from tests.common import assert_setup_component -from .test_auth import test_auth_via_msg +from .test_auth import test_auth_active_with_token -async def test_websocket_api(hass, no_auth_websocket_client, legacy_auth): +async def test_websocket_api( + hass, no_auth_websocket_client, hass_access_token, legacy_auth +): """Test API streams.""" with assert_setup_component(1): await async_setup_component( @@ -16,7 +18,7 @@ async def test_websocket_api(hass, no_auth_websocket_client, legacy_auth): state = hass.states.get("sensor.connected_clients") assert state.state == "0" - await test_auth_via_msg(no_auth_websocket_client, legacy_auth) + await test_auth_active_with_token(hass, no_auth_websocket_client, hass_access_token) state = hass.states.get("sensor.connected_clients") assert state.state == "1" diff --git a/tests/components/yandex_transport/test_yandex_transport_sensor.py b/tests/components/yandex_transport/test_yandex_transport_sensor.py index 50d945e7fae..7997d01bd13 100644 --- a/tests/components/yandex_transport/test_yandex_transport_sensor.py +++ b/tests/components/yandex_transport/test_yandex_transport_sensor.py @@ -38,14 +38,14 @@ TEST_CONFIG = { } FILTERED_ATTRS = { - "т36": ["21:43", "21:47", "22:02"], - "т47": ["21:40", "22:01"], - "м10": ["21:48", "22:00"], + "т36": ["16:10", "16:17", "16:26"], + "т47": ["16:09", "16:10"], + "м10": ["16:12", "16:20"], "stop_name": "7-й автобусный парк", "attribution": "Data provided by maps.yandex.ru", } -RESULT_STATE = dt_util.utc_from_timestamp(1568659253).isoformat(timespec="seconds") +RESULT_STATE = dt_util.utc_from_timestamp(1570972183).isoformat(timespec="seconds") async def assert_setup_sensor(hass, config, count=1): diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index fc29e4012cd..788faaaec73 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -3,6 +3,12 @@ import time from unittest.mock import Mock, patch from asynctest import CoroutineMock +import zigpy.profiles.zha +import zigpy.types +import zigpy.zcl +import zigpy.zcl.clusters.general +import zigpy.zcl.foundation as zcl_f +import zigpy.zdo.types from homeassistant.components.zha.core.const import ( DATA_ZHA, @@ -10,7 +16,6 @@ from homeassistant.components.zha.core.const import ( DATA_ZHA_CONFIG, DATA_ZHA_DISPATCHERS, ) -from homeassistant.components.zha.core.helpers import convert_ieee from homeassistant.util import slugify from tests.common import mock_coro @@ -21,7 +26,7 @@ class FakeApplication: def __init__(self): """Init fake application.""" - self.ieee = convert_ieee("00:15:8d:00:02:32:4f:32") + self.ieee = zigpy.types.EUI64.convert("00:15:8d:00:02:32:4f:32") self.nwk = 0x087D @@ -33,8 +38,6 @@ class FakeEndpoint: def __init__(self, manufacturer, model): """Init fake endpoint.""" - from zigpy.profiles.zha import PROFILE_ID - self.device = None self.endpoint_id = 1 self.in_clusters = {} @@ -43,14 +46,12 @@ class FakeEndpoint: self.status = 1 self.manufacturer = manufacturer self.model = model - self.profile_id = PROFILE_ID + self.profile_id = zigpy.profiles.zha.PROFILE_ID self.device_type = None def add_input_cluster(self, cluster_id): """Add an input cluster.""" - from zigpy.zcl import Cluster - - cluster = Cluster.from_id(self, cluster_id, is_server=True) + cluster = zigpy.zcl.Cluster.from_id(self, cluster_id, is_server=True) patch_cluster(cluster) self.in_clusters[cluster_id] = cluster if hasattr(cluster, "ep_attribute"): @@ -58,9 +59,7 @@ class FakeEndpoint: def add_output_cluster(self, cluster_id): """Add an output cluster.""" - from zigpy.zcl import Cluster - - cluster = Cluster.from_id(self, cluster_id, is_server=False) + cluster = zigpy.zcl.Cluster.from_id(self, cluster_id, is_server=False) patch_cluster(cluster) self.out_clusters[cluster_id] = cluster @@ -71,7 +70,6 @@ def patch_cluster(cluster): cluster.configure_reporting = CoroutineMock(return_value=[0]) cluster.deserialize = Mock() cluster.handle_cluster_request = Mock() - cluster.handle_cluster_general_request = Mock() cluster.read_attributes = CoroutineMock() cluster.read_attributes_raw = Mock() cluster.unbind = CoroutineMock(return_value=[0]) @@ -83,7 +81,7 @@ class FakeDevice: def __init__(self, ieee, manufacturer, model): """Init fake device.""" self._application = APPLICATION - self.ieee = convert_ieee(ieee) + self.ieee = zigpy.types.EUI64.convert(ieee) self.nwk = 0xB79C self.zdo = Mock() self.endpoints = {0: self.zdo} @@ -94,9 +92,7 @@ class FakeDevice: self.initializing = False self.manufacturer = manufacturer self.model = model - from zigpy.zdo.types import NodeDescriptor - - self.node_desc = NodeDescriptor() + self.node_desc = zigpy.zdo.types.NodeDescriptor() def make_device( @@ -150,11 +146,9 @@ async def async_init_zigpy_device( def make_attribute(attrid, value, status=0): """Make an attribute.""" - from zigpy.zcl.foundation import Attribute, TypeValue - - attr = Attribute() + attr = zcl_f.Attribute() attr.attrid = attrid - attr.value = TypeValue() + attr.value = zcl_f.TypeValue() attr.value.value = value return attr @@ -174,7 +168,7 @@ def make_entity_id(domain, device, cluster, use_suffix=True): machine so that we can test state changes. """ ieee = device.ieee - ieeetail = "".join(["%02x" % (o,) for o in ieee[-4:]]) + ieeetail = "".join([f"{o:02x}" for o in ieee[:4]]) entity_id = "{}.{}_{}_{}_{}{}".format( domain, slugify(device.manufacturer), @@ -202,21 +196,18 @@ async def async_test_device_join( simulate pairing a new device to the network so that code pathways that only trigger during device joins can be tested. """ - from zigpy.zcl.foundation import Status - from zigpy.zcl.clusters.general import Basic - # create zigpy device mocking out the zigbee network operations with patch( "zigpy.zcl.Cluster.configure_reporting", - return_value=mock_coro([Status.SUCCESS, Status.SUCCESS]), + return_value=mock_coro([zcl_f.Status.SUCCESS, zcl_f.Status.SUCCESS]), ): with patch( "zigpy.zcl.Cluster.bind", - return_value=mock_coro([Status.SUCCESS, Status.SUCCESS]), + return_value=mock_coro([zcl_f.Status.SUCCESS, zcl_f.Status.SUCCESS]), ): zigpy_device = await async_init_zigpy_device( hass, - [cluster_id, Basic.cluster_id], + [cluster_id, zigpy.zcl.clusters.general.Basic.cluster_id], [], device_type, zha_gateway, @@ -230,3 +221,12 @@ async def async_test_device_join( domain, zigpy_device, cluster, use_suffix=device_type is None ) assert hass.states.get(entity_id) is not None + + +def make_zcl_header(command_id: int, global_command: bool = True) -> zcl_f.ZCLHeader: + """Cluster.handle_message() ZCL Header helper.""" + if global_command: + frc = zcl_f.FrameControl(zcl_f.FrameType.GLOBAL_COMMAND) + else: + frc = zcl_f.FrameControl(zcl_f.FrameType.CLUSTER_COMMAND) + return zcl_f.ZCLHeader(frc, tsn=1, command_id=command_id) diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index b836c55df17..e34ad208744 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -1,13 +1,16 @@ """Test configuration for the ZHA component.""" from unittest.mock import patch + import pytest + from homeassistant import config_entries -from homeassistant.components.zha.core.const import DOMAIN, DATA_ZHA, COMPONENTS -from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg +from homeassistant.components.zha.core.const import COMPONENTS, DATA_ZHA, DOMAIN from homeassistant.components.zha.core.gateway import ZHAGateway from homeassistant.components.zha.core.registries import establish_device_mappings -from .common import async_setup_entry from homeassistant.components.zha.core.store import async_get_registry +from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg + +from .common import async_setup_entry @pytest.fixture(name="config_entry") diff --git a/tests/components/zha/test_api.py b/tests/components/zha/test_api.py index ae8e460b613..3fea9dfe088 100644 --- a/tests/components/zha/test_api.py +++ b/tests/components/zha/test_api.py @@ -1,33 +1,39 @@ """Test ZHA API.""" import pytest +import zigpy.zcl.clusters.general as general + from homeassistant.components.switch import DOMAIN -from homeassistant.components.zha.api import async_load_api, TYPE, ID +from homeassistant.components.websocket_api import const +from homeassistant.components.zha.api import ID, TYPE, async_load_api from homeassistant.components.zha.core.const import ( ATTR_CLUSTER_ID, ATTR_CLUSTER_TYPE, - CLUSTER_TYPE_IN, + ATTR_ENDPOINT_ID, ATTR_IEEE, + ATTR_MANUFACTURER, ATTR_MODEL, ATTR_NAME, ATTR_QUIRK_APPLIED, - ATTR_MANUFACTURER, - ATTR_ENDPOINT_ID, + CLUSTER_TYPE_IN, ) -from homeassistant.components.websocket_api import const + from .common import async_init_zigpy_device @pytest.fixture async def zha_client(hass, config_entry, zha_gateway, hass_ws_client): """Test zha switch platform.""" - from zigpy.zcl.clusters.general import OnOff, Basic # load the ZHA API async_load_api(hass) # create zigpy device await async_init_zigpy_device( - hass, [OnOff.cluster_id, Basic.cluster_id], [], None, zha_gateway + hass, + [general.OnOff.cluster_id, general.Basic.cluster_id], + [], + None, + zha_gateway, ) # load up switch domain diff --git a/tests/components/zha/test_binary_sensor.py b/tests/components/zha/test_binary_sensor.py index 47f81787acd..89dc1ae25a6 100644 --- a/tests/components/zha/test_binary_sensor.py +++ b/tests/components/zha/test_binary_sensor.py @@ -1,29 +1,37 @@ """Test zha binary sensor.""" +import zigpy.zcl.clusters.general as general +import zigpy.zcl.clusters.measurement as measurement +import zigpy.zcl.clusters.security as security +import zigpy.zcl.foundation as zcl_f + from homeassistant.components.binary_sensor import DOMAIN -from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNAVAILABLE +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE + from .common import ( + async_enable_traffic, async_init_zigpy_device, + async_test_device_join, make_attribute, make_entity_id, - async_test_device_join, - async_enable_traffic, + make_zcl_header, ) async def test_binary_sensor(hass, config_entry, zha_gateway): """Test zha binary_sensor platform.""" - from zigpy.zcl.clusters.security import IasZone - from zigpy.zcl.clusters.measurement import OccupancySensing - from zigpy.zcl.clusters.general import Basic # create zigpy devices zigpy_device_zone = await async_init_zigpy_device( - hass, [IasZone.cluster_id, Basic.cluster_id], [], None, zha_gateway + hass, + [security.IasZone.cluster_id, general.Basic.cluster_id], + [], + None, + zha_gateway, ) zigpy_device_occupancy = await async_init_zigpy_device( hass, - [OccupancySensing.cluster_id, Basic.cluster_id], + [measurement.OccupancySensing.cluster_id, general.Basic.cluster_id], [], None, zha_gateway, @@ -67,20 +75,24 @@ async def test_binary_sensor(hass, config_entry, zha_gateway): await async_test_iaszone_on_off(hass, zone_cluster, zone_entity_id) # test new sensor join - await async_test_device_join(hass, zha_gateway, OccupancySensing.cluster_id, DOMAIN) + await async_test_device_join( + hass, zha_gateway, measurement.OccupancySensing.cluster_id, DOMAIN + ) async def async_test_binary_sensor_on_off(hass, cluster, entity_id): """Test getting on and off messages for binary sensors.""" # binary sensor on attr = make_attribute(0, 1) - cluster.handle_message(1, 0x0A, [[attr]]) + hdr = make_zcl_header(zcl_f.Command.Report_Attributes) + + cluster.handle_message(hdr, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_ON # binary sensor off attr.value.value = 0 - cluster.handle_message(0, 0x0A, [[attr]]) + cluster.handle_message(hdr, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_OFF diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index 25b0910931a..5e6bf51afd6 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -1,7 +1,9 @@ """Tests for ZHA config flow.""" from asynctest import patch + from homeassistant.components.zha import config_flow from homeassistant.components.zha.core.const import DOMAIN + from tests.common import MockConfigEntry diff --git a/tests/components/zha/test_device_action.py b/tests/components/zha/test_device_action.py index 6e7bc6ab4b1..62884fe72ae 100644 --- a/tests/components/zha/test_device_action.py +++ b/tests/components/zha/test_device_action.py @@ -2,6 +2,9 @@ from unittest.mock import patch import pytest +import zigpy.zcl.clusters.general as general +import zigpy.zcl.clusters.security as security +import zigpy.zcl.foundation as zcl_f import homeassistant.components.automation as automation from homeassistant.components.device_automation import ( @@ -29,19 +32,21 @@ def calls(hass): async def test_get_actions(hass, config_entry, zha_gateway): """Test we get the expected actions from a zha device.""" - from zigpy.zcl.clusters.general import Basic - from zigpy.zcl.clusters.security import IasZone, IasWd # create zigpy device zigpy_device = await async_init_zigpy_device( hass, - [Basic.cluster_id, IasZone.cluster_id, IasWd.cluster_id], + [ + general.Basic.cluster_id, + security.IasZone.cluster_id, + security.IasWd.cluster_id, + ], [], None, zha_gateway, ) - await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) + await hass.config_entries.async_forward_entry_setup(config_entry, "binary_sensor") await hass.async_block_till_done() hass.config_entries._entries.append(config_entry) @@ -64,15 +69,15 @@ async def test_get_actions(hass, config_entry, zha_gateway): async def test_action(hass, config_entry, zha_gateway, calls): """Test for executing a zha device action.""" - from zigpy.zcl.clusters.general import Basic, OnOff - from zigpy.zcl.clusters.security import IasZone, IasWd - from zigpy.zcl.foundation import Status - # create zigpy device zigpy_device = await async_init_zigpy_device( hass, - [Basic.cluster_id, IasZone.cluster_id, IasWd.cluster_id], - [OnOff.cluster_id], + [ + general.Basic.cluster_id, + security.IasZone.cluster_id, + security.IasWd.cluster_id, + ], + [general.OnOff.cluster_id], None, zha_gateway, ) @@ -96,7 +101,8 @@ async def test_action(hass, config_entry, zha_gateway, calls): await async_enable_traffic(hass, zha_gateway, [zha_device]) with patch( - "zigpy.zcl.Cluster.request", return_value=mock_coro([0x00, Status.SUCCESS]) + "zigpy.zcl.Cluster.request", + return_value=mock_coro([0x00, zcl_f.Status.SUCCESS]), ): assert await async_setup_component( hass, diff --git a/tests/components/zha/test_device_tracker.py b/tests/components/zha/test_device_tracker.py index 6a7638d9f86..446920eb2f9 100644 --- a/tests/components/zha/test_device_tracker.py +++ b/tests/components/zha/test_device_tracker.py @@ -1,44 +1,43 @@ """Test ZHA Device Tracker.""" from datetime import timedelta import time + +import zigpy.zcl.clusters.general as general +import zigpy.zcl.foundation as zcl_f + from homeassistant.components.device_tracker import DOMAIN, SOURCE_TYPE_ROUTER -from homeassistant.const import STATE_HOME, STATE_NOT_HOME, STATE_UNAVAILABLE from homeassistant.components.zha.core.registries import ( SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE, ) +from homeassistant.const import STATE_HOME, STATE_NOT_HOME, STATE_UNAVAILABLE import homeassistant.util.dt as dt_util + from .common import ( + async_enable_traffic, async_init_zigpy_device, + async_test_device_join, make_attribute, make_entity_id, - async_test_device_join, - async_enable_traffic, + make_zcl_header, ) + from tests.common import async_fire_time_changed async def test_device_tracker(hass, config_entry, zha_gateway): """Test zha device tracker platform.""" - from zigpy.zcl.clusters.general import ( - Basic, - PowerConfiguration, - BinaryInput, - Identify, - Ota, - PollControl, - ) # create zigpy device zigpy_device = await async_init_zigpy_device( hass, [ - Basic.cluster_id, - PowerConfiguration.cluster_id, - Identify.cluster_id, - PollControl.cluster_id, - BinaryInput.cluster_id, + general.Basic.cluster_id, + general.PowerConfiguration.cluster_id, + general.Identify.cluster_id, + general.PollControl.cluster_id, + general.BinaryInput.cluster_id, ], - [Identify.cluster_id, Ota.cluster_id], + [general.Identify.cluster_id, general.Ota.cluster_id], SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE, zha_gateway, ) @@ -67,10 +66,11 @@ async def test_device_tracker(hass, config_entry, zha_gateway): # turn state flip attr = make_attribute(0x0020, 23) - cluster.handle_message(1, 0x0A, [[attr]]) + hdr = make_zcl_header(zcl_f.Command.Report_Attributes) + cluster.handle_message(hdr, [[attr]]) attr = make_attribute(0x0021, 200) - cluster.handle_message(1, 0x0A, [[attr]]) + cluster.handle_message(hdr, [[attr]]) zigpy_device.last_seen = time.time() + 10 next_update = dt_util.utcnow() + timedelta(seconds=30) @@ -89,7 +89,7 @@ async def test_device_tracker(hass, config_entry, zha_gateway): await async_test_device_join( hass, zha_gateway, - PowerConfiguration.cluster_id, + general.PowerConfiguration.cluster_id, DOMAIN, SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE, ) diff --git a/tests/components/zha/test_device_trigger.py b/tests/components/zha/test_device_trigger.py index 2f4ddb6b8b2..75e8538c5bf 100644 --- a/tests/components/zha/test_device_trigger.py +++ b/tests/components/zha/test_device_trigger.py @@ -1,7 +1,6 @@ """ZHA device automation trigger tests.""" -from unittest.mock import patch - import pytest +import zigpy.zcl.clusters.general as general import homeassistant.components.automation as automation from homeassistant.components.switch import DOMAIN @@ -11,7 +10,7 @@ from homeassistant.setup import async_setup_component from .common import async_enable_traffic, async_init_zigpy_device -from tests.common import async_mock_service, async_get_device_automations +from tests.common import async_get_device_automations, async_mock_service ON = 1 OFF = 0 @@ -45,11 +44,10 @@ def calls(hass): async def test_triggers(hass, config_entry, zha_gateway): """Test zha device triggers.""" - from zigpy.zcl.clusters.general import OnOff, Basic # create zigpy device zigpy_device = await async_init_zigpy_device( - hass, [Basic.cluster_id], [OnOff.cluster_id], None, zha_gateway + hass, [general.Basic.cluster_id], [general.OnOff.cluster_id], None, zha_gateway ) zigpy_device.device_automation_triggers = { @@ -114,11 +112,10 @@ async def test_triggers(hass, config_entry, zha_gateway): async def test_no_triggers(hass, config_entry, zha_gateway): """Test zha device with no triggers.""" - from zigpy.zcl.clusters.general import OnOff, Basic # create zigpy device zigpy_device = await async_init_zigpy_device( - hass, [Basic.cluster_id], [OnOff.cluster_id], None, zha_gateway + hass, [general.Basic.cluster_id], [general.OnOff.cluster_id], None, zha_gateway ) await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) @@ -137,11 +134,10 @@ async def test_no_triggers(hass, config_entry, zha_gateway): async def test_if_fires_on_event(hass, config_entry, zha_gateway, calls): """Test for remote triggers firing.""" - from zigpy.zcl.clusters.general import OnOff, Basic # create zigpy device zigpy_device = await async_init_zigpy_device( - hass, [Basic.cluster_id], [OnOff.cluster_id], None, zha_gateway + hass, [general.Basic.cluster_id], [general.OnOff.cluster_id], None, zha_gateway ) zigpy_device.device_automation_triggers = { @@ -197,13 +193,12 @@ async def test_if_fires_on_event(hass, config_entry, zha_gateway, calls): assert calls[0].data["message"] == "service called" -async def test_exception_no_triggers(hass, config_entry, zha_gateway, calls): +async def test_exception_no_triggers(hass, config_entry, zha_gateway, calls, caplog): """Test for exception on event triggers firing.""" - from zigpy.zcl.clusters.general import OnOff, Basic # create zigpy device zigpy_device = await async_init_zigpy_device( - hass, [Basic.cluster_id], [OnOff.cluster_id], None, zha_gateway + hass, [general.Basic.cluster_id], [general.OnOff.cluster_id], None, zha_gateway ) await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) @@ -219,39 +214,37 @@ async def test_exception_no_triggers(hass, config_entry, zha_gateway, calls): ha_device_registry = await async_get_registry(hass) reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}, set()) - with patch("logging.Logger.error") as mock: - await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: [ - { - "trigger": { - "device_id": reg_device.id, - "domain": "zha", - "platform": "device", - "type": "junk", - "subtype": "junk", - }, - "action": { - "service": "test.automation", - "data": {"message": "service called"}, - }, - } - ] - }, - ) - await hass.async_block_till_done() - mock.assert_called_with("Error setting up trigger %s", "automation 0") + await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "device_id": reg_device.id, + "domain": "zha", + "platform": "device", + "type": "junk", + "subtype": "junk", + }, + "action": { + "service": "test.automation", + "data": {"message": "service called"}, + }, + } + ] + }, + ) + await hass.async_block_till_done() + assert "Invalid config for [automation]" in caplog.text -async def test_exception_bad_trigger(hass, config_entry, zha_gateway, calls): +async def test_exception_bad_trigger(hass, config_entry, zha_gateway, calls, caplog): """Test for exception on event triggers firing.""" - from zigpy.zcl.clusters.general import OnOff, Basic # create zigpy device zigpy_device = await async_init_zigpy_device( - hass, [Basic.cluster_id], [OnOff.cluster_id], None, zha_gateway + hass, [general.Basic.cluster_id], [general.OnOff.cluster_id], None, zha_gateway ) zigpy_device.device_automation_triggers = { @@ -275,27 +268,26 @@ async def test_exception_bad_trigger(hass, config_entry, zha_gateway, calls): ha_device_registry = await async_get_registry(hass) reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}, set()) - with patch("logging.Logger.error") as mock: - await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: [ - { - "trigger": { - "device_id": reg_device.id, - "domain": "zha", - "platform": "device", - "type": "junk", - "subtype": "junk", - }, - "action": { - "service": "test.automation", - "data": {"message": "service called"}, - }, - } - ] - }, - ) - await hass.async_block_till_done() - mock.assert_called_with("Error setting up trigger %s", "automation 0") + await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "device_id": reg_device.id, + "domain": "zha", + "platform": "device", + "type": "junk", + "subtype": "junk", + }, + "action": { + "service": "test.automation", + "data": {"message": "service called"}, + }, + } + ] + }, + ) + await hass.async_block_till_done() + assert "Invalid config for [automation]" in caplog.text diff --git a/tests/components/zha/test_fan.py b/tests/components/zha/test_fan.py index 3fe5e7937c8..a196ba50ba7 100644 --- a/tests/components/zha/test_fan.py +++ b/tests/components/zha/test_fan.py @@ -1,28 +1,39 @@ """Test zha fan.""" from unittest.mock import call, patch + +import zigpy.zcl.clusters.general as general +import zigpy.zcl.clusters.hvac as hvac +import zigpy.zcl.foundation as zcl_f + from homeassistant.components import fan -from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNAVAILABLE from homeassistant.components.fan import ATTR_SPEED, DOMAIN, SERVICE_SET_SPEED -from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF -from tests.common import mock_coro +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, +) + from .common import ( + async_enable_traffic, async_init_zigpy_device, + async_test_device_join, make_attribute, make_entity_id, - async_test_device_join, - async_enable_traffic, + make_zcl_header, ) +from tests.common import mock_coro + async def test_fan(hass, config_entry, zha_gateway): """Test zha fan platform.""" - from zigpy.zcl.clusters.hvac import Fan - from zigpy.zcl.clusters.general import Basic - from zigpy.zcl.foundation import Status # create zigpy device zigpy_device = await async_init_zigpy_device( - hass, [Fan.cluster_id, Basic.cluster_id], [], None, zha_gateway + hass, [hvac.Fan.cluster_id, general.Basic.cluster_id], [], None, zha_gateway ) # load up fan domain @@ -44,20 +55,21 @@ async def test_fan(hass, config_entry, zha_gateway): # turn on at fan attr = make_attribute(0, 1) - cluster.handle_message(1, 0x0A, [[attr]]) + hdr = make_zcl_header(zcl_f.Command.Report_Attributes) + cluster.handle_message(hdr, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_ON # turn off at fan attr.value.value = 0 - cluster.handle_message(0, 0x0A, [[attr]]) + cluster.handle_message(hdr, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_OFF # turn on from HA with patch( "zigpy.zcl.Cluster.write_attributes", - return_value=mock_coro([Status.SUCCESS, Status.SUCCESS]), + return_value=mock_coro([zcl_f.Status.SUCCESS, zcl_f.Status.SUCCESS]), ): # turn on via UI await async_turn_on(hass, entity_id) @@ -67,7 +79,7 @@ async def test_fan(hass, config_entry, zha_gateway): # turn off from HA with patch( "zigpy.zcl.Cluster.write_attributes", - return_value=mock_coro([Status.SUCCESS, Status.SUCCESS]), + return_value=mock_coro([zcl_f.Status.SUCCESS, zcl_f.Status.SUCCESS]), ): # turn off via UI await async_turn_off(hass, entity_id) @@ -77,7 +89,7 @@ async def test_fan(hass, config_entry, zha_gateway): # change speed from HA with patch( "zigpy.zcl.Cluster.write_attributes", - return_value=mock_coro([Status.SUCCESS, Status.SUCCESS]), + return_value=mock_coro([zcl_f.Status.SUCCESS, zcl_f.Status.SUCCESS]), ): # turn on via UI await async_set_speed(hass, entity_id, speed=fan.SPEED_HIGH) @@ -85,7 +97,7 @@ async def test_fan(hass, config_entry, zha_gateway): assert cluster.write_attributes.call_args == call({"fan_mode": 3}) # test adding new fan to the network and HA - await async_test_device_join(hass, zha_gateway, Fan.cluster_id, DOMAIN) + await async_test_device_join(hass, zha_gateway, hvac.Fan.cluster_id, DOMAIN) async def async_turn_on(hass, entity_id, speed=None): diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index 08c6cfe18cf..f0d9d4913e6 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -2,6 +2,11 @@ import asyncio from unittest.mock import MagicMock, call, patch, sentinel +import zigpy.profiles.zha +import zigpy.types +import zigpy.zcl.clusters.general as general +import zigpy.zcl.foundation as zcl_f + from homeassistant.components.light import DOMAIN from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE @@ -11,6 +16,7 @@ from .common import ( async_test_device_join, make_attribute, make_entity_id, + make_zcl_header, ) from tests.common import mock_coro @@ -21,24 +27,25 @@ OFF = 0 async def test_light(hass, config_entry, zha_gateway, monkeypatch): """Test zha light platform.""" - from zigpy.zcl.clusters.general import OnOff, LevelControl, Basic - from zigpy.zcl.foundation import Status - from zigpy.profiles.zha import DeviceType # create zigpy devices zigpy_device_on_off = await async_init_zigpy_device( hass, - [OnOff.cluster_id, Basic.cluster_id], + [general.OnOff.cluster_id, general.Basic.cluster_id], [], - DeviceType.ON_OFF_LIGHT, + zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT, zha_gateway, ) zigpy_device_level = await async_init_zigpy_device( hass, - [OnOff.cluster_id, LevelControl.cluster_id, Basic.cluster_id], + [ + general.OnOff.cluster_id, + general.LevelControl.cluster_id, + general.Basic.cluster_id, + ], [], - DeviceType.ON_OFF_LIGHT, + zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT, zha_gateway, ieee="00:0d:6f:11:0a:90:69:e7", manufacturer="FakeLevelManufacturer", @@ -61,12 +68,12 @@ async def test_light(hass, config_entry, zha_gateway, monkeypatch): level_device_level_cluster = zigpy_device_level.endpoints.get(1).level on_off_mock = MagicMock( side_effect=asyncio.coroutine( - MagicMock(return_value=[sentinel.data, Status.SUCCESS]) + MagicMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]) ) ) level_mock = MagicMock( side_effect=asyncio.coroutine( - MagicMock(return_value=[sentinel.data, Status.SUCCESS]) + MagicMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]) ) ) monkeypatch.setattr(level_device_on_off_cluster, "request", on_off_mock) @@ -115,7 +122,11 @@ async def test_light(hass, config_entry, zha_gateway, monkeypatch): # test adding a new light to the network and HA await async_test_device_join( - hass, zha_gateway, OnOff.cluster_id, DOMAIN, device_type=DeviceType.ON_OFF_LIGHT + hass, + zha_gateway, + general.OnOff.cluster_id, + DOMAIN, + device_type=zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT, ) @@ -123,13 +134,14 @@ async def async_test_on_off_from_light(hass, cluster, entity_id): """Test on off functionality from the light.""" # turn on at light attr = make_attribute(0, 1) - cluster.handle_message(1, 0x0A, [[attr]]) + hdr = make_zcl_header(zcl_f.Command.Report_Attributes) + cluster.handle_message(hdr, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_ON # turn off at light attr.value.value = 0 - cluster.handle_message(0, 0x0A, [[attr]]) + cluster.handle_message(hdr, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_OFF @@ -138,17 +150,17 @@ async def async_test_on_from_light(hass, cluster, entity_id): """Test on off functionality from the light.""" # turn on at light attr = make_attribute(0, 1) - cluster.handle_message(1, 0x0A, [[attr]]) + hdr = make_zcl_header(zcl_f.Command.Report_Attributes) + cluster.handle_message(hdr, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_ON async def async_test_on_off_from_hass(hass, cluster, entity_id): """Test on off functionality from hass.""" - from zigpy.zcl.foundation import Status - with patch( - "zigpy.zcl.Cluster.request", return_value=mock_coro([0x00, Status.SUCCESS]) + "zigpy.zcl.Cluster.request", + return_value=mock_coro([0x00, zcl_f.Status.SUCCESS]), ): # turn on via UI await hass.services.async_call( @@ -164,10 +176,9 @@ async def async_test_on_off_from_hass(hass, cluster, entity_id): async def async_test_off_from_hass(hass, cluster, entity_id): """Test turning off the light from homeassistant.""" - from zigpy.zcl.foundation import Status - with patch( - "zigpy.zcl.Cluster.request", return_value=mock_coro([0x01, Status.SUCCESS]) + "zigpy.zcl.Cluster.request", + return_value=mock_coro([0x01, zcl_f.Status.SUCCESS]), ): # turn off via UI await hass.services.async_call( @@ -183,7 +194,6 @@ async def async_test_level_on_off_from_hass( hass, on_off_cluster, level_cluster, entity_id ): """Test on off functionality from hass.""" - from zigpy import types # turn on via UI await hass.services.async_call( @@ -208,7 +218,7 @@ async def async_test_level_on_off_from_hass( assert level_cluster.request.call_args == call( False, 4, - (types.uint8_t, types.uint16_t), + (zigpy.types.uint8_t, zigpy.types.uint16_t), 254, 100.0, expect_reply=True, @@ -228,7 +238,7 @@ async def async_test_level_on_off_from_hass( assert level_cluster.request.call_args == call( False, 4, - (types.uint8_t, types.uint16_t), + (zigpy.types.uint8_t, zigpy.types.uint16_t), 10, 0, expect_reply=True, @@ -243,7 +253,8 @@ async def async_test_level_on_off_from_hass( async def async_test_dimmer_from_light(hass, cluster, entity_id, level, expected_state): """Test dimmer functionality from the light.""" attr = make_attribute(0, level) - cluster.handle_message(1, 0x0A, [[attr]]) + hdr = make_zcl_header(zcl_f.Command.Report_Attributes) + cluster.handle_message(hdr, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == expected_state # hass uses None for brightness of 0 in state attributes diff --git a/tests/components/zha/test_lock.py b/tests/components/zha/test_lock.py index 7381b557310..118526a1d85 100644 --- a/tests/components/zha/test_lock.py +++ b/tests/components/zha/test_lock.py @@ -1,27 +1,37 @@ """Test zha lock.""" from unittest.mock import patch -from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED, STATE_UNAVAILABLE + +import zigpy.zcl.clusters.closures as closures +import zigpy.zcl.clusters.general as general +import zigpy.zcl.foundation as zcl_f + from homeassistant.components.lock import DOMAIN -from tests.common import mock_coro +from homeassistant.const import STATE_LOCKED, STATE_UNAVAILABLE, STATE_UNLOCKED + from .common import ( + async_enable_traffic, async_init_zigpy_device, make_attribute, make_entity_id, - async_enable_traffic, + make_zcl_header, ) +from tests.common import mock_coro + LOCK_DOOR = 0 UNLOCK_DOOR = 1 async def test_lock(hass, config_entry, zha_gateway): """Test zha lock platform.""" - from zigpy.zcl.clusters.closures import DoorLock - from zigpy.zcl.clusters.general import Basic # create zigpy device zigpy_device = await async_init_zigpy_device( - hass, [DoorLock.cluster_id, Basic.cluster_id], [], None, zha_gateway + hass, + [closures.DoorLock.cluster_id, general.Basic.cluster_id], + [], + None, + zha_gateway, ) # load up lock domain @@ -43,13 +53,14 @@ async def test_lock(hass, config_entry, zha_gateway): # set state to locked attr = make_attribute(0, 1) - cluster.handle_message(1, 0x0A, [[attr]]) + hdr = make_zcl_header(zcl_f.Command.Report_Attributes) + cluster.handle_message(hdr, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_LOCKED # set state to unlocked attr.value.value = 2 - cluster.handle_message(0, 0x0A, [[attr]]) + cluster.handle_message(hdr, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_UNLOCKED @@ -62,9 +73,9 @@ async def test_lock(hass, config_entry, zha_gateway): async def async_lock(hass, cluster, entity_id): """Test lock functionality from hass.""" - from zigpy.zcl.foundation import Status - - with patch("zigpy.zcl.Cluster.request", return_value=mock_coro([Status.SUCCESS])): + with patch( + "zigpy.zcl.Cluster.request", return_value=mock_coro([zcl_f.Status.SUCCESS]) + ): # lock via UI await hass.services.async_call( DOMAIN, "lock", {"entity_id": entity_id}, blocking=True @@ -76,9 +87,9 @@ async def async_lock(hass, cluster, entity_id): async def async_unlock(hass, cluster, entity_id): """Test lock functionality from hass.""" - from zigpy.zcl.foundation import Status - - with patch("zigpy.zcl.Cluster.request", return_value=mock_coro([Status.SUCCESS])): + with patch( + "zigpy.zcl.Cluster.request", return_value=mock_coro([zcl_f.Status.SUCCESS]) + ): # lock via UI await hass.services.async_call( DOMAIN, "unlock", {"entity_id": entity_id}, blocking=True diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index faa44f34927..dec551f8d62 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -1,34 +1,34 @@ """Test zha sensor.""" +import zigpy.zcl.clusters.general as general +import zigpy.zcl.clusters.homeautomation as homeautomation +import zigpy.zcl.clusters.measurement as measurement +import zigpy.zcl.clusters.smartenergy as smartenergy +import zigpy.zcl.foundation as zcl_f + from homeassistant.components.sensor import DOMAIN -from homeassistant.const import STATE_UNKNOWN, STATE_UNAVAILABLE +from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN + from .common import ( + async_enable_traffic, async_init_zigpy_device, + async_test_device_join, make_attribute, make_entity_id, - async_test_device_join, - async_enable_traffic, + make_zcl_header, ) async def test_sensor(hass, config_entry, zha_gateway): """Test zha sensor platform.""" - from zigpy.zcl.clusters.measurement import ( - RelativeHumidity, - TemperatureMeasurement, - PressureMeasurement, - IlluminanceMeasurement, - ) - from zigpy.zcl.clusters.smartenergy import Metering - from zigpy.zcl.clusters.homeautomation import ElectricalMeasurement # list of cluster ids to create devices and sensor entities for cluster_ids = [ - RelativeHumidity.cluster_id, - TemperatureMeasurement.cluster_id, - PressureMeasurement.cluster_id, - IlluminanceMeasurement.cluster_id, - Metering.cluster_id, - ElectricalMeasurement.cluster_id, + measurement.RelativeHumidity.cluster_id, + measurement.TemperatureMeasurement.cluster_id, + measurement.PressureMeasurement.cluster_id, + measurement.IlluminanceMeasurement.cluster_id, + smartenergy.Metering.cluster_id, + homeautomation.ElectricalMeasurement.cluster_id, ] # devices that were created from cluster_ids list above @@ -59,33 +59,33 @@ async def test_sensor(hass, config_entry, zha_gateway): assert hass.states.get(entity_id).state == STATE_UNKNOWN # get the humidity device info and test the associated sensor logic - device_info = zigpy_device_infos[RelativeHumidity.cluster_id] + device_info = zigpy_device_infos[measurement.RelativeHumidity.cluster_id] await async_test_humidity(hass, device_info) # get the temperature device info and test the associated sensor logic - device_info = zigpy_device_infos[TemperatureMeasurement.cluster_id] + device_info = zigpy_device_infos[measurement.TemperatureMeasurement.cluster_id] await async_test_temperature(hass, device_info) # get the pressure device info and test the associated sensor logic - device_info = zigpy_device_infos[PressureMeasurement.cluster_id] + device_info = zigpy_device_infos[measurement.PressureMeasurement.cluster_id] await async_test_pressure(hass, device_info) # get the illuminance device info and test the associated sensor logic - device_info = zigpy_device_infos[IlluminanceMeasurement.cluster_id] + device_info = zigpy_device_infos[measurement.IlluminanceMeasurement.cluster_id] await async_test_illuminance(hass, device_info) # get the metering device info and test the associated sensor logic - device_info = zigpy_device_infos[Metering.cluster_id] + device_info = zigpy_device_infos[smartenergy.Metering.cluster_id] await async_test_metering(hass, device_info) # get the electrical_measurement device info and test the associated # sensor logic - device_info = zigpy_device_infos[ElectricalMeasurement.cluster_id] + device_info = zigpy_device_infos[homeautomation.ElectricalMeasurement.cluster_id] await async_test_electrical_measurement(hass, device_info) # test joining a new temperature sensor to the network await async_test_device_join( - hass, zha_gateway, TemperatureMeasurement.cluster_id, DOMAIN + hass, zha_gateway, measurement.TemperatureMeasurement.cluster_id, DOMAIN ) @@ -98,7 +98,6 @@ async def async_build_devices(hass, zha_gateway, config_entry, cluster_ids): A dict containing relevant device info for testing is returned. It contains the entity id, zigpy device, and the zigbee cluster for the sensor. """ - from zigpy.zcl.clusters.general import Basic device_infos = {} counter = 0 @@ -107,7 +106,7 @@ async def async_build_devices(hass, zha_gateway, config_entry, cluster_ids): device_infos[cluster_id] = {"zigpy_device": None} device_infos[cluster_id]["zigpy_device"] = await async_init_zigpy_device( hass, - [cluster_id, Basic.cluster_id], + [cluster_id, general.Basic.cluster_id], [], None, zha_gateway, @@ -177,7 +176,8 @@ async def send_attribute_report(hass, cluster, attrid, value): device is paired to the zigbee network. """ attr = make_attribute(attrid, value) - cluster.handle_message(1, 0x0A, [[attr]]) + hdr = make_zcl_header(zcl_f.Command.Report_Attributes) + cluster.handle_message(hdr, [[attr]]) await hass.async_block_till_done() diff --git a/tests/components/zha/test_switch.py b/tests/components/zha/test_switch.py index ac6bc73b809..bf4ff3ed628 100644 --- a/tests/components/zha/test_switch.py +++ b/tests/components/zha/test_switch.py @@ -1,28 +1,37 @@ """Test zha switch.""" from unittest.mock import call, patch + +import zigpy.zcl.clusters.general as general +import zigpy.zcl.foundation as zcl_f + from homeassistant.components.switch import DOMAIN -from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNAVAILABLE -from tests.common import mock_coro +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE + from .common import ( + async_enable_traffic, async_init_zigpy_device, + async_test_device_join, make_attribute, make_entity_id, - async_test_device_join, - async_enable_traffic, + make_zcl_header, ) +from tests.common import mock_coro + ON = 1 OFF = 0 async def test_switch(hass, config_entry, zha_gateway): """Test zha switch platform.""" - from zigpy.zcl.clusters.general import OnOff, Basic - from zigpy.zcl.foundation import Status # create zigpy device zigpy_device = await async_init_zigpy_device( - hass, [OnOff.cluster_id, Basic.cluster_id], [], None, zha_gateway + hass, + [general.OnOff.cluster_id, general.Basic.cluster_id], + [], + None, + zha_gateway, ) # load up switch domain @@ -44,19 +53,21 @@ async def test_switch(hass, config_entry, zha_gateway): # turn on at switch attr = make_attribute(0, 1) - cluster.handle_message(1, 0x0A, [[attr]]) + hdr = make_zcl_header(zcl_f.Command.Report_Attributes) + cluster.handle_message(hdr, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_ON # turn off at switch attr.value.value = 0 - cluster.handle_message(0, 0x0A, [[attr]]) + cluster.handle_message(hdr, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_OFF # turn on from HA with patch( - "zigpy.zcl.Cluster.request", return_value=mock_coro([0x00, Status.SUCCESS]) + "zigpy.zcl.Cluster.request", + return_value=mock_coro([0x00, zcl_f.Status.SUCCESS]), ): # turn on via UI await hass.services.async_call( @@ -69,7 +80,8 @@ async def test_switch(hass, config_entry, zha_gateway): # turn off from HA with patch( - "zigpy.zcl.Cluster.request", return_value=mock_coro([0x01, Status.SUCCESS]) + "zigpy.zcl.Cluster.request", + return_value=mock_coro([0x01, zcl_f.Status.SUCCESS]), ): # turn off via UI await hass.services.async_call( @@ -81,4 +93,4 @@ async def test_switch(hass, config_entry, zha_gateway): ) # test joining a new switch to the network and HA - await async_test_device_join(hass, zha_gateway, OnOff.cluster_id, DOMAIN) + await async_test_device_join(hass, zha_gateway, general.OnOff.cluster_id, DOMAIN) diff --git a/tests/fixtures/airly_no_station.json b/tests/fixtures/airly_no_station.json new file mode 100644 index 00000000000..cc64934938f --- /dev/null +++ b/tests/fixtures/airly_no_station.json @@ -0,0 +1,642 @@ +{ + "current": { + "fromDateTime": "2019-10-02T05:53:00.608Z", + "tillDateTime": "2019-10-02T06:53:00.608Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, + "history": [{ + "fromDateTime": "2019-10-01T06:00:00.000Z", + "tillDateTime": "2019-10-01T07:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-01T07:00:00.000Z", + "tillDateTime": "2019-10-01T08:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-01T08:00:00.000Z", + "tillDateTime": "2019-10-01T09:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-01T09:00:00.000Z", + "tillDateTime": "2019-10-01T10:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-01T10:00:00.000Z", + "tillDateTime": "2019-10-01T11:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-01T11:00:00.000Z", + "tillDateTime": "2019-10-01T12:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-01T12:00:00.000Z", + "tillDateTime": "2019-10-01T13:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-01T13:00:00.000Z", + "tillDateTime": "2019-10-01T14:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-01T14:00:00.000Z", + "tillDateTime": "2019-10-01T15:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-01T15:00:00.000Z", + "tillDateTime": "2019-10-01T16:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-01T16:00:00.000Z", + "tillDateTime": "2019-10-01T17:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-01T17:00:00.000Z", + "tillDateTime": "2019-10-01T18:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-01T18:00:00.000Z", + "tillDateTime": "2019-10-01T19:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-01T19:00:00.000Z", + "tillDateTime": "2019-10-01T20:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-01T20:00:00.000Z", + "tillDateTime": "2019-10-01T21:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-01T21:00:00.000Z", + "tillDateTime": "2019-10-01T22:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-01T22:00:00.000Z", + "tillDateTime": "2019-10-01T23:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-01T23:00:00.000Z", + "tillDateTime": "2019-10-02T00:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T00:00:00.000Z", + "tillDateTime": "2019-10-02T01:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T01:00:00.000Z", + "tillDateTime": "2019-10-02T02:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T02:00:00.000Z", + "tillDateTime": "2019-10-02T03:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T03:00:00.000Z", + "tillDateTime": "2019-10-02T04:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T04:00:00.000Z", + "tillDateTime": "2019-10-02T05:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T05:00:00.000Z", + "tillDateTime": "2019-10-02T06:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }], + "forecast": [{ + "fromDateTime": "2019-10-02T06:00:00.000Z", + "tillDateTime": "2019-10-02T07:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T07:00:00.000Z", + "tillDateTime": "2019-10-02T08:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T08:00:00.000Z", + "tillDateTime": "2019-10-02T09:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T09:00:00.000Z", + "tillDateTime": "2019-10-02T10:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T10:00:00.000Z", + "tillDateTime": "2019-10-02T11:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T11:00:00.000Z", + "tillDateTime": "2019-10-02T12:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T12:00:00.000Z", + "tillDateTime": "2019-10-02T13:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T13:00:00.000Z", + "tillDateTime": "2019-10-02T14:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T14:00:00.000Z", + "tillDateTime": "2019-10-02T15:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T15:00:00.000Z", + "tillDateTime": "2019-10-02T16:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T16:00:00.000Z", + "tillDateTime": "2019-10-02T17:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T17:00:00.000Z", + "tillDateTime": "2019-10-02T18:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T18:00:00.000Z", + "tillDateTime": "2019-10-02T19:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T19:00:00.000Z", + "tillDateTime": "2019-10-02T20:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T20:00:00.000Z", + "tillDateTime": "2019-10-02T21:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T21:00:00.000Z", + "tillDateTime": "2019-10-02T22:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T22:00:00.000Z", + "tillDateTime": "2019-10-02T23:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T23:00:00.000Z", + "tillDateTime": "2019-10-03T00:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-03T00:00:00.000Z", + "tillDateTime": "2019-10-03T01:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-03T01:00:00.000Z", + "tillDateTime": "2019-10-03T02:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-03T02:00:00.000Z", + "tillDateTime": "2019-10-03T03:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-03T03:00:00.000Z", + "tillDateTime": "2019-10-03T04:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-03T04:00:00.000Z", + "tillDateTime": "2019-10-03T05:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-03T05:00:00.000Z", + "tillDateTime": "2019-10-03T06:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }] +} \ No newline at end of file diff --git a/tests/fixtures/airly_valid_station.json b/tests/fixtures/airly_valid_station.json new file mode 100644 index 00000000000..656c62c04c2 --- /dev/null +++ b/tests/fixtures/airly_valid_station.json @@ -0,0 +1,1726 @@ +{ + "current": { + "fromDateTime": "2019-10-02T05:54:57.204Z", + "tillDateTime": "2019-10-02T06:54:57.204Z", + "values": [{ + "name": "PM1", + "value": 9.23 + }, { + "name": "PM25", + "value": 13.71 + }, { + "name": "PM10", + "value": 18.58 + }, { + "name": "PRESSURE", + "value": 1000.87 + }, { + "name": "HUMIDITY", + "value": 92.84 + }, { + "name": "TEMPERATURE", + "value": 14.23 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 22.85, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Great air!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 54.84 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 37.17 + }] + }, + "history": [{ + "fromDateTime": "2019-10-01T06:00:00.000Z", + "tillDateTime": "2019-10-01T07:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 5.95 + }, { + "name": "PM25", + "value": 8.54 + }, { + "name": "PM10", + "value": 11.46 + }, { + "name": "PRESSURE", + "value": 1009.61 + }, { + "name": "HUMIDITY", + "value": 97.6 + }, { + "name": "TEMPERATURE", + "value": 9.71 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 14.24, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Green equals clean!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 34.18 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 22.91 + }] + }, { + "fromDateTime": "2019-10-01T07:00:00.000Z", + "tillDateTime": "2019-10-01T08:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 4.2 + }, { + "name": "PM25", + "value": 5.88 + }, { + "name": "PM10", + "value": 7.88 + }, { + "name": "PRESSURE", + "value": 1009.13 + }, { + "name": "HUMIDITY", + "value": 90.84 + }, { + "name": "TEMPERATURE", + "value": 12.65 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 9.81, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Dear me, how wonderful!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 23.53 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 15.75 + }] + }, { + "fromDateTime": "2019-10-01T08:00:00.000Z", + "tillDateTime": "2019-10-01T09:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 3.63 + }, { + "name": "PM25", + "value": 5.56 + }, { + "name": "PM10", + "value": 7.71 + }, { + "name": "PRESSURE", + "value": 1008.27 + }, { + "name": "HUMIDITY", + "value": 84.61 + }, { + "name": "TEMPERATURE", + "value": 15.57 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 9.26, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Dear me, how wonderful!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 22.23 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 15.42 + }] + }, { + "fromDateTime": "2019-10-01T09:00:00.000Z", + "tillDateTime": "2019-10-01T10:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 2.9 + }, { + "name": "PM25", + "value": 3.93 + }, { + "name": "PM10", + "value": 5.24 + }, { + "name": "PRESSURE", + "value": 1007.57 + }, { + "name": "HUMIDITY", + "value": 79.52 + }, { + "name": "TEMPERATURE", + "value": 16.57 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 6.56, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Breathe deep! The air is clean!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 15.74 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 10.48 + }] + }, { + "fromDateTime": "2019-10-01T10:00:00.000Z", + "tillDateTime": "2019-10-01T11:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 2.45 + }, { + "name": "PM25", + "value": 3.33 + }, { + "name": "PM10", + "value": 4.52 + }, { + "name": "PRESSURE", + "value": 1006.75 + }, { + "name": "HUMIDITY", + "value": 74.09 + }, { + "name": "TEMPERATURE", + "value": 16.95 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 5.55, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "The air is grand today. ;)", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 13.31 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 9.04 + }] + }, { + "fromDateTime": "2019-10-01T11:00:00.000Z", + "tillDateTime": "2019-10-01T12:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 2.0 + }, { + "name": "PM25", + "value": 2.93 + }, { + "name": "PM10", + "value": 3.98 + }, { + "name": "PRESSURE", + "value": 1005.71 + }, { + "name": "HUMIDITY", + "value": 69.06 + }, { + "name": "TEMPERATURE", + "value": 17.31 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 4.89, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Green equals clean!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 11.74 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 7.96 + }] + }, { + "fromDateTime": "2019-10-01T12:00:00.000Z", + "tillDateTime": "2019-10-01T13:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 1.92 + }, { + "name": "PM25", + "value": 2.69 + }, { + "name": "PM10", + "value": 3.68 + }, { + "name": "PRESSURE", + "value": 1005.03 + }, { + "name": "HUMIDITY", + "value": 65.08 + }, { + "name": "TEMPERATURE", + "value": 17.47 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 4.49, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Enjoy life!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 10.77 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 7.36 + }] + }, { + "fromDateTime": "2019-10-01T13:00:00.000Z", + "tillDateTime": "2019-10-01T14:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 1.79 + }, { + "name": "PM25", + "value": 2.57 + }, { + "name": "PM10", + "value": 3.53 + }, { + "name": "PRESSURE", + "value": 1004.26 + }, { + "name": "HUMIDITY", + "value": 63.72 + }, { + "name": "TEMPERATURE", + "value": 17.91 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 4.29, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Great air!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 10.29 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 7.06 + }] + }, { + "fromDateTime": "2019-10-01T14:00:00.000Z", + "tillDateTime": "2019-10-01T15:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 2.06 + }, { + "name": "PM25", + "value": 3.08 + }, { + "name": "PM10", + "value": 4.23 + }, { + "name": "PRESSURE", + "value": 1003.46 + }, { + "name": "HUMIDITY", + "value": 64.44 + }, { + "name": "TEMPERATURE", + "value": 17.84 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 5.14, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "The air is grand today. ;)", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 12.33 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 8.47 + }] + }, { + "fromDateTime": "2019-10-01T15:00:00.000Z", + "tillDateTime": "2019-10-01T16:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 3.17 + }, { + "name": "PM25", + "value": 4.61 + }, { + "name": "PM10", + "value": 6.25 + }, { + "name": "PRESSURE", + "value": 1003.18 + }, { + "name": "HUMIDITY", + "value": 65.32 + }, { + "name": "TEMPERATURE", + "value": 18.08 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 7.68, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Green, green, green!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 18.44 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 12.5 + }] + }, { + "fromDateTime": "2019-10-01T16:00:00.000Z", + "tillDateTime": "2019-10-01T17:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 4.17 + }, { + "name": "PM25", + "value": 5.91 + }, { + "name": "PM10", + "value": 8.06 + }, { + "name": "PRESSURE", + "value": 1003.05 + }, { + "name": "HUMIDITY", + "value": 66.14 + }, { + "name": "TEMPERATURE", + "value": 17.04 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 9.84, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Enjoy life!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 23.62 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 16.11 + }] + }, { + "fromDateTime": "2019-10-01T17:00:00.000Z", + "tillDateTime": "2019-10-01T18:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 6.4 + }, { + "name": "PM25", + "value": 10.93 + }, { + "name": "PM10", + "value": 15.7 + }, { + "name": "PRESSURE", + "value": 1002.85 + }, { + "name": "HUMIDITY", + "value": 68.31 + }, { + "name": "TEMPERATURE", + "value": 16.33 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 18.22, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "It couldn't be better ;)", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 43.74 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 31.4 + }] + }, { + "fromDateTime": "2019-10-01T18:00:00.000Z", + "tillDateTime": "2019-10-01T19:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 4.79 + }, { + "name": "PM25", + "value": 7.41 + }, { + "name": "PM10", + "value": 10.31 + }, { + "name": "PRESSURE", + "value": 1002.52 + }, { + "name": "HUMIDITY", + "value": 69.88 + }, { + "name": "TEMPERATURE", + "value": 15.98 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 12.35, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Enjoy life!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 29.65 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 20.63 + }] + }, { + "fromDateTime": "2019-10-01T19:00:00.000Z", + "tillDateTime": "2019-10-01T20:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 5.99 + }, { + "name": "PM25", + "value": 9.45 + }, { + "name": "PM10", + "value": 13.22 + }, { + "name": "PRESSURE", + "value": 1002.32 + }, { + "name": "HUMIDITY", + "value": 70.47 + }, { + "name": "TEMPERATURE", + "value": 15.76 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 15.74, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Breathe deeply!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 37.78 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 26.44 + }] + }, { + "fromDateTime": "2019-10-01T20:00:00.000Z", + "tillDateTime": "2019-10-01T21:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 9.35 + }, { + "name": "PM25", + "value": 14.67 + }, { + "name": "PM10", + "value": 20.57 + }, { + "name": "PRESSURE", + "value": 1002.46 + }, { + "name": "HUMIDITY", + "value": 72.61 + }, { + "name": "TEMPERATURE", + "value": 15.47 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 24.45, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "It couldn't be better ;)", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 58.68 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 41.13 + }] + }, { + "fromDateTime": "2019-10-01T21:00:00.000Z", + "tillDateTime": "2019-10-01T22:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 9.95 + }, { + "name": "PM25", + "value": 15.37 + }, { + "name": "PM10", + "value": 21.33 + }, { + "name": "PRESSURE", + "value": 1002.59 + }, { + "name": "HUMIDITY", + "value": 75.09 + }, { + "name": "TEMPERATURE", + "value": 15.17 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 25.62, + "level": "LOW", + "description": "Air is quite good.", + "advice": "Take a breath!", + "color": "#D1CF1E" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 61.48 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 42.66 + }] + }, { + "fromDateTime": "2019-10-01T22:00:00.000Z", + "tillDateTime": "2019-10-01T23:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 10.16 + }, { + "name": "PM25", + "value": 15.78 + }, { + "name": "PM10", + "value": 21.97 + }, { + "name": "PRESSURE", + "value": 1002.59 + }, { + "name": "HUMIDITY", + "value": 77.68 + }, { + "name": "TEMPERATURE", + "value": 14.9 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 26.31, + "level": "LOW", + "description": "Air is quite good.", + "advice": "Great air for a walk to the park!", + "color": "#D1CF1E" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 63.14 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 43.93 + }] + }, { + "fromDateTime": "2019-10-01T23:00:00.000Z", + "tillDateTime": "2019-10-02T00:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 9.86 + }, { + "name": "PM25", + "value": 15.14 + }, { + "name": "PM10", + "value": 21.07 + }, { + "name": "PRESSURE", + "value": 1002.49 + }, { + "name": "HUMIDITY", + "value": 79.86 + }, { + "name": "TEMPERATURE", + "value": 14.56 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 25.24, + "level": "LOW", + "description": "Air is quite good.", + "advice": "Leave the mask at home today!", + "color": "#D1CF1E" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 60.57 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 42.14 + }] + }, { + "fromDateTime": "2019-10-02T00:00:00.000Z", + "tillDateTime": "2019-10-02T01:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 9.77 + }, { + "name": "PM25", + "value": 15.04 + }, { + "name": "PM10", + "value": 20.97 + }, { + "name": "PRESSURE", + "value": 1002.18 + }, { + "name": "HUMIDITY", + "value": 81.77 + }, { + "name": "TEMPERATURE", + "value": 14.13 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 25.07, + "level": "LOW", + "description": "Air is quite good.", + "advice": "Time for a walk with friends or activities with your family - because the air is clean!", + "color": "#D1CF1E" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 60.18 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 41.94 + }] + }, { + "fromDateTime": "2019-10-02T01:00:00.000Z", + "tillDateTime": "2019-10-02T02:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 9.67 + }, { + "name": "PM25", + "value": 14.9 + }, { + "name": "PM10", + "value": 20.67 + }, { + "name": "PRESSURE", + "value": 1002.01 + }, { + "name": "HUMIDITY", + "value": 84.5 + }, { + "name": "TEMPERATURE", + "value": 13.7 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 24.84, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Great air!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 59.62 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 41.33 + }] + }, { + "fromDateTime": "2019-10-02T02:00:00.000Z", + "tillDateTime": "2019-10-02T03:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 7.17 + }, { + "name": "PM25", + "value": 10.7 + }, { + "name": "PM10", + "value": 14.58 + }, { + "name": "PRESSURE", + "value": 1001.56 + }, { + "name": "HUMIDITY", + "value": 88.55 + }, { + "name": "TEMPERATURE", + "value": 13.44 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 17.83, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Catch your breath!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 42.8 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 29.17 + }] + }, { + "fromDateTime": "2019-10-02T03:00:00.000Z", + "tillDateTime": "2019-10-02T04:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 6.99 + }, { + "name": "PM25", + "value": 10.23 + }, { + "name": "PM10", + "value": 13.66 + }, { + "name": "PRESSURE", + "value": 1001.34 + }, { + "name": "HUMIDITY", + "value": 90.82 + }, { + "name": "TEMPERATURE", + "value": 13.3 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 17.05, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Perfect air for exercising! Go for it!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 40.91 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 27.33 + }] + }, { + "fromDateTime": "2019-10-02T04:00:00.000Z", + "tillDateTime": "2019-10-02T05:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 7.82 + }, { + "name": "PM25", + "value": 11.59 + }, { + "name": "PM10", + "value": 15.77 + }, { + "name": "PRESSURE", + "value": 1000.92 + }, { + "name": "HUMIDITY", + "value": 91.8 + }, { + "name": "TEMPERATURE", + "value": 13.34 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 19.32, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Dear me, how wonderful!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 46.36 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 31.54 + }] + }, { + "fromDateTime": "2019-10-02T05:00:00.000Z", + "tillDateTime": "2019-10-02T06:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 10.16 + }, { + "name": "PM25", + "value": 15.35 + }, { + "name": "PM10", + "value": 21.45 + }, { + "name": "PRESSURE", + "value": 1000.82 + }, { + "name": "HUMIDITY", + "value": 92.15 + }, { + "name": "TEMPERATURE", + "value": 13.74 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 25.59, + "level": "LOW", + "description": "Air is quite good.", + "advice": "How about going for a walk?", + "color": "#D1CF1E" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 61.42 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 42.9 + }] + }], + "forecast": [{ + "fromDateTime": "2019-10-02T06:00:00.000Z", + "tillDateTime": "2019-10-02T07:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 13.28 + }, { + "name": "PM10", + "value": 18.37 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 22.14, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "It couldn't be better ;)", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 53.13 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 36.73 + }] + }, { + "fromDateTime": "2019-10-02T07:00:00.000Z", + "tillDateTime": "2019-10-02T08:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 11.19 + }, { + "name": "PM10", + "value": 15.65 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 18.65, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Enjoy life!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 44.76 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 31.31 + }] + }, { + "fromDateTime": "2019-10-02T08:00:00.000Z", + "tillDateTime": "2019-10-02T09:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 8.79 + }, { + "name": "PM10", + "value": 12.8 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 14.65, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Breathe deep! The air is clean!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 35.15 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 25.59 + }] + }, { + "fromDateTime": "2019-10-02T09:00:00.000Z", + "tillDateTime": "2019-10-02T10:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 5.46 + }, { + "name": "PM10", + "value": 8.91 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 9.11, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Breathe to fill your lungs!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 21.86 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 17.83 + }] + }, { + "fromDateTime": "2019-10-02T10:00:00.000Z", + "tillDateTime": "2019-10-02T11:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 2.26 + }, { + "name": "PM10", + "value": 5.02 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 5.02, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Enjoy life!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 9.06 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 10.05 + }] + }, { + "fromDateTime": "2019-10-02T11:00:00.000Z", + "tillDateTime": "2019-10-02T12:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 1.06 + }, { + "name": "PM10", + "value": 2.52 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 2.52, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "The air is great!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 4.22 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 5.05 + }] + }, { + "fromDateTime": "2019-10-02T12:00:00.000Z", + "tillDateTime": "2019-10-02T13:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 0.48 + }, { + "name": "PM10", + "value": 1.94 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 1.94, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Breathe as much as you can!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 1.94 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 3.89 + }] + }, { + "fromDateTime": "2019-10-02T13:00:00.000Z", + "tillDateTime": "2019-10-02T14:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 0.63 + }, { + "name": "PM10", + "value": 2.26 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 2.26, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Enjoy life!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 2.53 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 4.52 + }] + }, { + "fromDateTime": "2019-10-02T14:00:00.000Z", + "tillDateTime": "2019-10-02T15:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 1.47 + }, { + "name": "PM10", + "value": 3.39 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 3.39, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Breathe as much as you can!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 5.87 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 6.78 + }] + }, { + "fromDateTime": "2019-10-02T15:00:00.000Z", + "tillDateTime": "2019-10-02T16:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 2.62 + }, { + "name": "PM10", + "value": 5.02 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 5.02, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Great air!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 10.5 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 10.05 + }] + }, { + "fromDateTime": "2019-10-02T16:00:00.000Z", + "tillDateTime": "2019-10-02T17:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 3.89 + }, { + "name": "PM10", + "value": 8.02 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 8.02, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Dear me, how wonderful!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 15.56 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 16.04 + }] + }, { + "fromDateTime": "2019-10-02T17:00:00.000Z", + "tillDateTime": "2019-10-02T18:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 6.26 + }, { + "name": "PM10", + "value": 11.41 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 11.41, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "The air is great!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 25.05 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 22.83 + }] + }, { + "fromDateTime": "2019-10-02T18:00:00.000Z", + "tillDateTime": "2019-10-02T19:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 8.69 + }, { + "name": "PM10", + "value": 14.48 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 14.48, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Zero dust - zero worries!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 34.76 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 28.96 + }] + }, { + "fromDateTime": "2019-10-02T19:00:00.000Z", + "tillDateTime": "2019-10-02T20:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 10.78 + }, { + "name": "PM10", + "value": 16.86 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 17.97, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Zero dust - zero worries!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 43.13 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 33.72 + }] + }, { + "fromDateTime": "2019-10-02T20:00:00.000Z", + "tillDateTime": "2019-10-02T21:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 12.22 + }, { + "name": "PM10", + "value": 18.19 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 20.36, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Breathe to fill your lungs!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 48.88 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 36.38 + }] + }, { + "fromDateTime": "2019-10-02T21:00:00.000Z", + "tillDateTime": "2019-10-02T22:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 13.06 + }, { + "name": "PM10", + "value": 18.62 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 21.77, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Dear me, how wonderful!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 52.25 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 37.24 + }] + }, { + "fromDateTime": "2019-10-02T22:00:00.000Z", + "tillDateTime": "2019-10-02T23:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 13.51 + }, { + "name": "PM10", + "value": 18.49 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 22.52, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "The air is great!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 54.06 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 36.98 + }] + }, { + "fromDateTime": "2019-10-02T23:00:00.000Z", + "tillDateTime": "2019-10-03T00:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 13.46 + }, { + "name": "PM10", + "value": 17.63 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 22.44, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Green, green, green!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 53.85 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 35.26 + }] + }, { + "fromDateTime": "2019-10-03T00:00:00.000Z", + "tillDateTime": "2019-10-03T01:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 13.05 + }, { + "name": "PM10", + "value": 16.36 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 21.74, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Catch your breath!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 52.19 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 32.73 + }] + }, { + "fromDateTime": "2019-10-03T01:00:00.000Z", + "tillDateTime": "2019-10-03T02:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 12.47 + }, { + "name": "PM10", + "value": 15.16 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 20.79, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Green, green, green!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 49.9 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 30.32 + }] + }, { + "fromDateTime": "2019-10-03T02:00:00.000Z", + "tillDateTime": "2019-10-03T03:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 11.99 + }, { + "name": "PM10", + "value": 14.07 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 19.98, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Breathe as much as you can!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 47.94 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 28.14 + }] + }, { + "fromDateTime": "2019-10-03T03:00:00.000Z", + "tillDateTime": "2019-10-03T04:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 11.74 + }, { + "name": "PM10", + "value": 13.67 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 19.56, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Dear me, how wonderful!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 46.95 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 27.34 + }] + }, { + "fromDateTime": "2019-10-03T04:00:00.000Z", + "tillDateTime": "2019-10-03T05:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 11.44 + }, { + "name": "PM10", + "value": 13.51 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 19.06, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Breathe to fill your lungs!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 45.74 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 27.02 + }] + }, { + "fromDateTime": "2019-10-03T05:00:00.000Z", + "tillDateTime": "2019-10-03T06:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 10.88 + }, { + "name": "PM10", + "value": 13.38 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 18.13, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Breathe as much as you can!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 43.52 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 26.76 + }] + }] +} \ No newline at end of file diff --git a/tests/fixtures/homematicip_cloud.json b/tests/fixtures/homematicip_cloud.json new file mode 100644 index 00000000000..e17df9c2039 --- /dev/null +++ b/tests/fixtures/homematicip_cloud.json @@ -0,0 +1,5530 @@ +{ + "clients": { + "00000000-0000-0000-0000-000000000000": { + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000000", + "label": "TEST-Client", + "clientType": "APP" + } + }, + "devices": { + "3014F7110000000000000031": { + "availableFirmwareVersion": "1.2.1", + "firmwareVersion": "1.2.1", + "firmwareVersionInteger": 66049, + "functionalChannels": { + "0": { + "coProFaulty": false, + "coProRestartNeeded": false, + "coProUpdateFailure": false, + "configPending": false, + "deviceId": "3014F7110000000000000031", + "deviceOverheated": false, + "deviceOverloaded": false, + "deviceUndervoltage": false, + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -88, + "rssiPeerValue": null, + "supportedOptionalFeatures": { + "IFeatureDeviceCoProError": false, + "IFeatureDeviceCoProRestart": false, + "IFeatureDeviceCoProUpdate": false, + "IFeatureDeviceOverheated": false, + "IFeatureDeviceOverloaded": false, + "IFeatureDeviceTemperatureOutOfRange": false, + "IFeatureDeviceUndervoltage": false + }, + "temperatureOutOfRange": false, + "unreach": false + }, + "1": { + "accelerationSensorEventFilterPeriod": 3.0, + "accelerationSensorMode": "FLAT_DECT", + "accelerationSensorNeutralPosition": "VERTICAL", + "accelerationSensorSensitivity": "SENSOR_RANGE_4G", + "accelerationSensorTriggerAngle": 45, + "accelerationSensorTriggered": true, + "deviceId": "3014F7110000000000000031", + "functionalChannelType": "ACCELERATION_SENSOR_CHANNEL", + "groupIndex": 1, + "groups": [], + "index": 1, + "label": "", + "notificationSoundTypeHighToLow": "SOUND_LONG", + "notificationSoundTypeLowToHigh": "SOUND_LONG" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000031", + "label": "Garagentor", + "lastStatusUpdate": 1567850423788, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 315, + "modelType": "HmIP-SAM", + "oem": "eQ-3", + "permanentlyReachable": false, + "serializedGlobalTradeItemNumber": "3014F7110000000000000031", + "type": "ACCELERATION_SENSOR", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000052": { + "availableFirmwareVersion": "1.0.5", + "firmwareVersion": "1.0.5", + "firmwareVersionInteger": 65541, + "functionalChannels": { + "0": { + "coProFaulty": false, + "coProRestartNeeded": false, + "coProUpdateFailure": false, + "configPending": false, + "deviceId": "3014F7110000000000000052", + "deviceOverheated": false, + "deviceOverloaded": false, + "deviceUndervoltage": false, + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -73, + "rssiPeerValue": null, + "supportedOptionalFeatures": { + "IFeatureDeviceCoProError": false, + "IFeatureDeviceCoProRestart": false, + "IFeatureDeviceCoProUpdate": false, + "IFeatureDeviceOverheated": false, + "IFeatureDeviceOverloaded": false, + "IFeatureDeviceTemperatureOutOfRange": false, + "IFeatureDeviceUndervoltage": false + }, + "temperatureOutOfRange": false, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000052", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 1, + "groups": [], + "index": 1, + "label": "" + }, + "2": { + "deviceId": "3014F7110000000000000052", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 1, + "groups": [ + ], + "index": 2, + "label": "" + }, + "3": { + "deviceId": "3014F7110000000000000052", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 2, + "groups": [], + "index": 3, + "label": "" + }, + "4": { + "deviceId": "3014F7110000000000000052", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 2, + "groups": [], + "index": 4, + "label": "" + }, + "5": { + "deviceId": "3014F7110000000000000052", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 3, + "groups": [], + "index": 5, + "label": "" + }, + "6": { + "deviceId": "3014F7110000000000000052", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 3, + "groups": [], + "index": 6, + "label": "" + }, + "7": { + "deviceId": "3014F7110000000000000052", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 4, + "groups": [], + "index": 7, + "label": "" + }, + "8": { + "deviceId": "3014F7110000000000000052", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 4, + "groups": [], + "index": 8, + "label": "" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000052", + "label": "Alarm-Melder", + "lastStatusUpdate": 1564733931898, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 336, + "modelType": "HmIP-MOD-RC8", + "oem": "eQ-3", + "permanentlyReachable": false, + "serializedGlobalTradeItemNumber": "3014F7110000000000000052", + "type": "REMOTE_CONTROL_8_MODULE", + "updateState": "UP_TO_DATE" + }, + "3014F71100000000FAL24C10": { + "availableFirmwareVersion": "1.6.2", + "firmwareVersion": "1.6.2", + "firmwareVersionInteger": 67074, + "functionalChannels": { + "0": { + "configPending": false, + "coolingEmergencyValue": 0.0, + "deviceId": "3014F71100000000FAL24C10", + "dutyCycle": false, + "frostProtectionTemperature": 8.0, + "functionalChannelType": "DEVICE_GLOBAL_PUMP_CONTROL", + "globalPumpControl": true, + "groupIndex": 0, + "groups": [ + ], + "heatingEmergencyValue": 0.25, + "heatingLoadType": "LOAD_BALANCING", + "heatingValveType": "NORMALLY_CLOSE", + "index": 0, + "label": "", + "lowBat": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -73, + "rssiPeerValue": -74, + "unreach": false, + "valveProtectionDuration": 5, + "valveProtectionSwitchingInterval": 14 + }, + "1": { + "deviceId": "3014F71100000000FAL24C10", + "functionalChannelType": "FLOOR_TERMINAL_BLOCK_LOCAL_PUMP_CHANNEL", + "groupIndex": 1, + "groups": [], + "index": 1, + "label": "", + "pumpFollowUpTime": 2, + "pumpLeadTime": 2, + "pumpProtectionDuration": 1, + "pumpProtectionSwitchingInterval": 14 + }, + "10": { + "deviceId": "3014F71100000000FAL24C10", + "functionalChannelType": "FLOOR_TERMINAL_BLOCK_CHANNEL", + "groupIndex": 10, + "groups": [ + ], + "index": 10, + "label": "" + }, + "11": { + "deviceId": "3014F71100000000FAL24C10", + "functionalChannelType": "HEAT_DEMAND_CHANNEL", + "groupIndex": 0, + "groups": [ + ], + "index": 11, + "label": "" + }, + "12": { + "deviceId": "3014F71100000000FAL24C10", + "functionalChannelType": "DEHUMIDIFIER_DEMAND_CHANNEL", + "groupIndex": 0, + "groups": [ + ], + "index": 12, + "label": "" + }, + "2": { + "deviceId": "3014F71100000000FAL24C10", + "functionalChannelType": "FLOOR_TERMINAL_BLOCK_CHANNEL", + "groupIndex": 2, + "groups": [ + ], + "index": 2, + "label": "" + }, + "3": { + "deviceId": "3014F71100000000FAL24C10", + "functionalChannelType": "FLOOR_TERMINAL_BLOCK_CHANNEL", + "groupIndex": 3, + "groups": [ + ], + "index": 3, + "label": "" + }, + "4": { + "deviceId": "3014F71100000000FAL24C10", + "functionalChannelType": "FLOOR_TERMINAL_BLOCK_CHANNEL", + "groupIndex": 4, + "groups": [ + ], + "index": 4, + "label": "" + }, + "5": { + "deviceId": "3014F71100000000FAL24C10", + "functionalChannelType": "FLOOR_TERMINAL_BLOCK_CHANNEL", + "groupIndex": 5, + "groups": [ + ], + "index": 5, + "label": "" + }, + "6": { + "deviceId": "3014F71100000000FAL24C10", + "functionalChannelType": "FLOOR_TERMINAL_BLOCK_CHANNEL", + "groupIndex": 6, + "groups": [ + ], + "index": 6, + "label": "" + }, + "7": { + "deviceId": "3014F71100000000FAL24C10", + "functionalChannelType": "FLOOR_TERMINAL_BLOCK_CHANNEL", + "groupIndex": 7, + "groups": [ + ], + "index": 7, + "label": "" + }, + "8": { + "deviceId": "3014F71100000000FAL24C10", + "functionalChannelType": "FLOOR_TERMINAL_BLOCK_CHANNEL", + "groupIndex": 8, + "groups": [ + ], + "index": 8, + "label": "" + }, + "9": { + "deviceId": "3014F71100000000FAL24C10", + "functionalChannelType": "FLOOR_TERMINAL_BLOCK_CHANNEL", + "groupIndex": 9, + "groups": [ + ], + "index": 9, + "label": "" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F71100000000FAL24C10", + "label": "Fu\u00dfbodenheizungsaktor", + "lastStatusUpdate": 1558461135830, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 280, + "modelType": "HmIP-FAL24-C10", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F71100000000FAL24C10", + "type": "FLOOR_TERMINAL_BLOCK_10", + "updateState": "UP_TO_DATE" + }, + "3014F71100000000000BBL24": { + "availableFirmwareVersion": "1.6.2", + "firmwareVersion": "1.6.2", + "firmwareVersionInteger": 67074, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F71100000000000BBL24", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000034" + ], + "index": 0, + "label": "", + "lowBat": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -64, + "rssiPeerValue": -76, + "unreach": false + }, + "1": { + "blindModeActive": true, + "bottomToTopReferenceTime": 54.88, + "changeOverDelay": 0.5, + "delayCompensationValue": 12.7, + "deviceId": "3014F71100000000000BBL24", + "endpositionAutoDetectionEnabled": true, + "functionalChannelType": "BLIND_CHANNEL", + "groupIndex": 1, + "groups": [ + ], + "index": 1, + "label": "", + "previousShutterLevel": null, + "previousSlatsLevel": null, + "processing": false, + "profileMode": "AUTOMATIC", + "selfCalibrationInProgress": null, + "shutterLevel": 0.885, + "slatsLevel": 1.0, + "slatsReferenceTime": 1.6, + "supportingDelayCompensation": true, + "supportingEndpositionAutoDetection": true, + "supportingSelfCalibration": true, + "topToBottomReferenceTime": 53.68, + "userDesiredProfileMode": "MANUAL" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F71100000000000BBL24", + "label": "Jalousie Schiebet\u00fcr", + "lastStatusUpdate": 1558464454532, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 332, + "modelType": "HmIP-BBL", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F71100000000000BBL24", + "type": "BRAND_BLIND", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000BCBB11": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.10.10", + "firmwareVersionInteger": 68106, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000BCBB11", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + ], + "index": 0, + "label": "", + "lowBat": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -53, + "rssiPeerValue": -56, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000BCBB11", + "functionalChannelType": "SWITCH_CHANNEL", + "groupIndex": 1, + "groups": [ + ], + "index": 1, + "label": "", + "on": false, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + }, + "2": { + "deviceId": "3014F7110000000000BCBB11", + "functionalChannelType": "SWITCH_CHANNEL", + "groupIndex": 2, + "groups": [ + "00000000-0000-0000-0000-000000000038" + ], + "index": 2, + "label": "", + "on": false, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000BCBB11", + "label": "Jalousien - 1 KiZi, 2 SchlaZi", + "lastStatusUpdate": 1555621612744, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 357, + "modelType": "HmIP-PCBS2", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000BCBB11", + "type": "PRINTED_CIRCUIT_BOARD_SWITCH_2", + "updateState": "UP_TO_DATE" + }, + "3014F711ABCD0ABCD000002": { + "availableFirmwareVersion": "1.6.4", + "firmwareVersion": "1.6.4", + "firmwareVersionInteger": 67076, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F711ABCD0ABCD000002", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000027" + ], + "index": 0, + "label": "", + "lowBat": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -79, + "rssiPeerValue": null, + "unreach": false + }, + "1": { + "deviceId": "3014F711ABCD0ABCD000002", + "functionalChannelType": "SWITCH_CHANNEL", + "groupIndex": 1, + "groups": [ + ], + "index": 1, + "label": "", + "on": true, + "profileMode": null, + "userDesiredProfileMode": "AUTOMATIC" + }, + "2": { + "deviceId": "3014F711ABCD0ABCD000002", + "functionalChannelType": "SWITCH_CHANNEL", + "groupIndex": 2, + "groups": [], + "index": 2, + "label": "", + "on": false, + "profileMode": null, + "userDesiredProfileMode": "AUTOMATIC" + }, + "3": { + "deviceId": "3014F711ABCD0ABCD000002", + "functionalChannelType": "GENERIC_INPUT_CHANNEL", + "groupIndex": 3, + "groups": [], + "index": 3, + "label": "" + }, + "4": { + "deviceId": "3014F711ABCD0ABCD000002", + "functionalChannelType": "GENERIC_INPUT_CHANNEL", + "groupIndex": 4, + "groups": [], + "index": 4, + "label": "" + }, + "5": { + "analogOutputLevel": 12.5, + "deviceId": "3014F711ABCD0ABCD000002", + "functionalChannelType": "ANALOG_OUTPUT_CHANNEL", + "groupIndex": 0, + "groups": [], + "index": 5, + "label": "" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711ABCD0ABCD000002", + "label": "Multi IO Box", + "lastStatusUpdate": 1552508702220, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 283, + "modelType": "HmIP-MIOB", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F711ABCD0ABCD000002", + "type": "MULTI_IO_BOX", + "updateState": "UP_TO_DATE" + }, + "3014F71100000000ABCDEF10": { + "automaticValveAdaptionNeeded": false, + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.0.6", + "firmwareVersionInteger": 65542, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F71100000000ABCDEF10", + "dutyCycle": false, + "functionalChannelType": "DEVICE_SABOTAGE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000010" + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -47, + "rssiPeerValue": -50, + "sabotage": null, + "unreach": false + }, + "1": { + "deviceId": "3014F71100000000ABCDEF10", + "functionalChannelType": "HEATING_THERMOSTAT_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000013" + ], + "index": 1, + "label": "", + "setPointTemperature": 21.0, + "temperatureOffset": 0.0, + "valveActualTemperature": 21.6, + "valvePosition": 0.0, + "valveState": "ADAPTION_DONE" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F71100000000ABCDEF10", + "label": "Wohnzimmer 3", + "lastStatusUpdate": 1550912664486, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 325, + "modelType": "HmIP-eTRV-C", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F71100000000ABCDEF10", + "type": "HEATING_THERMOSTAT_COMPACT", + "updateState": "UP_TO_DATE" + }, + "3014F71100000000000TEST1": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.8.8", + "firmwareVersionInteger": 67592, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F71100000000000TEST1", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + ], + "index": 0, + "label": "", + "lowBat": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -51, + "rssiPeerValue": null, + "unreach": false + }, + "1": { + "deviceId": "3014F71100000000000TEST1", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 1, + "groups": [ + ], + "index": 1, + "label": "" + }, + "2": { + "deviceId": "3014F71100000000000TEST1", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 1, + "groups": [ + ], + "index": 2, + "label": "" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F71100000000000TEST1", + "label": "Remote", + "lastStatusUpdate": 1550512733995, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 358, + "modelType": "HmIP-BRC2", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F71100000000000TEST1", + "type": "BRAND_PUSH_BUTTON", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000064": { + "availableFirmwareVersion": "1.0.6", + "firmwareVersion": "1.0.6", + "firmwareVersionInteger": 65542, + "functionalChannels": { + "0": { + "coProFaulty": false, + "coProRestartNeeded": false, + "coProUpdateFailure": false, + "configPending": false, + "deviceId": "3014F7110000000000000064", + "deviceOverheated": true, + "deviceOverloaded": false, + "deviceUndervoltage": false, + "dutyCycle": false, + "functionalChannelType": "DEVICE_SABOTAGE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000032", + "00000000-0000-0000-0000-000000000013" + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -42, + "rssiPeerValue": null, + "sabotage": false, + "supportedOptionalFeatures": { + "IFeatureDeviceCoProError": false, + "IFeatureDeviceCoProRestart": false, + "IFeatureDeviceCoProUpdate": false, + "IFeatureDeviceOverheated": true, + "IFeatureDeviceOverloaded": false, + "IFeatureDeviceTemperatureOutOfRange": false, + "IFeatureDeviceUndervoltage": false + }, + "temperatureOutOfRange": false, + "unreach": false + }, + "1": { + "alarmContactType": "WINDOW_DOOR_CONTACT", + "contactType": "NORMALLY_CLOSE", + "deviceId": "3014F7110000000000000064", + "eventDelay": 0, + "functionalChannelType": "CONTACT_INTERFACE_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000033", + "00000000-0000-0000-0000-000000000010", + "00000000-0000-0000-0000-000000000013" + ], + "index": 1, + "label": "", + "windowState": "CLOSED" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000064", + "label": "Schlie\u00dfer Magnet", + "lastStatusUpdate": 1524515854304, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 375, + "modelType": "HmIP-SCI", + "oem": "eQ-3", + "permanentlyReachable": false, + "serializedGlobalTradeItemNumber": "3014F7110000000000000064", + "type": "SHUTTER_CONTACT_INTERFACE", + "updateState": "UP_TO_DATE" + }, + "3014F711BADCAFE000000001": { + "availableFirmwareVersion": "1.2.0", + "firmwareVersion": "1.2.0", + "firmwareVersionInteger": 66048, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F711BADCAFE000000001", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + ], + "index": 0, + "label": "", + "lowBat": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -73, + "rssiPeerValue": -78, + "unreach": false + }, + "1": { + "blindModeActive": true, + "bottomToTopReferenceTime": 41.0, + "changeOverDelay": 0.5, + "delayCompensationValue": 1.0, + "deviceId": "3014F711BADCAFE000000001", + "endpositionAutoDetectionEnabled": false, + "functionalChannelType": "BLIND_CHANNEL", + "groupIndex": 1, + "groups": [ + ], + "index": 1, + "label": "", + "previousShutterLevel": null, + "previousSlatsLevel": null, + "processing": false, + "profileMode": "AUTOMATIC", + "selfCalibrationInProgress": null, + "shutterLevel": 1.0, + "slatsLevel": 1.0, + "slatsReferenceTime": 2.0, + "supportingDelayCompensation": false, + "supportingEndpositionAutoDetection": false, + "supportingSelfCalibration": false, + "topToBottomReferenceTime": 41.0, + "userDesiredProfileMode": "AUTOMATIC" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711BADCAFE000000001", + "label": "Sofa links", + "lastStatusUpdate": 1548616026922, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 333, + "modelType": "HmIP-FBL", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F711BADCAFE000000001", + "type": "FULL_FLUSH_BLIND", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000055": { + "availableFirmwareVersion": "1.2.4", + "firmwareVersion": "1.2.4", + "firmwareVersionInteger": 66052, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000055", + "dutyCycle": false, + "functionalChannelType": "DEVICE_OPERATIONLOCK", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000034" + ], + "index": 0, + "label": "", + "lowBat": null, + "operationLockActive": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -76, + "rssiPeerValue": -77, + "unreach": false + }, + "1": { + "actualTemperature": 21.0, + "deviceId": "3014F7110000000000000055", + "display": "SETPOINT", + "functionalChannelType": "WALL_MOUNTED_THERMOSTAT_PRO_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000035" + ], + "humidity": 40, + "index": 1, + "label": "", + "vaporAmount": 6.177718198711658, + "valveActualTemperature": 20.0, + "setPointTemperature": 21.5, + "temperatureOffset": 0.0 + }, + "2": { + "deviceId": "3014F7110000000000000055", + "frostProtectionTemperature": 8.0, + "functionalChannelType": "INTERNAL_SWITCH_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000035" + ], + "heatingValveType": "NORMALLY_CLOSE", + "index": 2, + "internalSwitchOutputEnabled": true, + "label": "", + "valveProtectionDuration": 5, + "valveProtectionSwitchingInterval": 14 + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000055", + "label": "BWTH 1", + "lastStatusUpdate": 1547283716818, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 305, + "modelType": "HmIP-BWTH", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000055", + "type": "BRAND_WALL_MOUNTED_THERMOSTAT", + "updateState": "UP_TO_DATE" + }, + "3014F711ABCDEF0000000014": { + "availableFirmwareVersion": "1.4.2", + "firmwareVersion": "1.4.2", + "firmwareVersionInteger": 66562, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F711ABCDEF0000000014", + "dutyCycle": null, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000033" + ], + "index": 0, + "label": "", + "lowBat": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": null, + "rssiPeerValue": null, + "unreach": null + }, + "1": { + "deviceId": "3014F711ABCDEF0000000014", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 1, + "groups": [], + "index": 1, + "label": "" + }, + "2": { + "deviceId": "3014F711ABCDEF0000000014", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 1, + "groups": [], + "index": 2, + "label": "" + }, + "3": { + "deviceId": "3014F711ABCDEF0000000014", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 2, + "groups": [], + "index": 3, + "label": "" + }, + "4": { + "deviceId": "3014F711ABCDEF0000000014", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 2, + "groups": [], + "index": 4, + "label": "" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711ABCDEF0000000014", + "label": "FFB 1", + "lastStatusUpdate": 0, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 266, + "modelType": "HmIP-KRC4", + "oem": "eQ-3", + "permanentlyReachable": false, + "serializedGlobalTradeItemNumber": "3014F711ABCDEF0000000014", + "type": "KEY_REMOTE_CONTROL_4", + "updateState": "UP_TO_DATE" + }, + "3014F711BSL0000000000050": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.0.2", + "firmwareVersionInteger": 65538, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F711BSL0000000000050", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [], + "index": 0, + "label": "", + "lowBat": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -67, + "rssiPeerValue": -70, + "unreach": false + }, + "1": { + "deviceId": "3014F711BSL0000000000050", + "functionalChannelType": "SWITCH_CHANNEL", + "groupIndex": 1, + "groups": [], + "index": 1, + "label": "", + "on": true, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + }, + "2": { + "deviceId": "3014F711BSL0000000000050", + "dimLevel": 0.0, + "functionalChannelType": "NOTIFICATION_LIGHT_CHANNEL", + "groupIndex": 2, + "groups": [], + "index": 2, + "label": "", + "on": null, + "profileMode": "AUTOMATIC", + "simpleRGBColorState": "RED", + "userDesiredProfileMode": "AUTOMATIC" + }, + "3": { + "deviceId": "3014F711BSL0000000000050", + "dimLevel": 1.0, + "functionalChannelType": "NOTIFICATION_LIGHT_CHANNEL", + "groupIndex": 3, + "groups": [], + "index": 3, + "label": "", + "on": true, + "profileMode": "AUTOMATIC", + "simpleRGBColorState": "GREEN", + "userDesiredProfileMode": "AUTOMATIC" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711BSL0000000000050", + "label": "Treppe", + "lastStatusUpdate": 1548431183264, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 360, + "modelType": "HmIP-BSL", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F711BSL0000000000050", + "type": "BRAND_SWITCH_NOTIFICATION_LIGHT", + "updateState": "UP_TO_DATE" + }, + "3014F711SLO0000000000026": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.0.16", + "firmwareVersionInteger": 65552, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F711SLO0000000000026", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -60, + "rssiPeerValue": null, + "unreach": false + }, + "1": { + "averageIllumination": 807.3, + "currentIllumination": 785.2, + "deviceId": "3014F711SLO0000000000026", + "functionalChannelType": "LIGHT_SENSOR_CHANNEL", + "groupIndex": 1, + "groups": [], + "highestIllumination": 837.1, + "index": 1, + "label": "", + "lowestIllumination": 785.2 + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711SLO0000000000026", + "label": "Lichtsensor Nord", + "lastStatusUpdate": 1548494235548, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 308, + "modelType": "HmIP-SLO", + "oem": "eQ-3", + "permanentlyReachable": false, + "serializedGlobalTradeItemNumber": "3014F711SLO0000000000026", + "type": "LIGHT_SENSOR", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000054": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.0.0", + "firmwareVersionInteger": 65536, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000054", + "dutyCycle": false, + "functionalChannelType": "DEVICE_SABOTAGE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000053" + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -76, + "rssiPeerValue": null, + "sabotage": false, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000054", + "functionalChannelType": "PASSAGE_DETECTOR_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000055" + ], + "index": 1, + "label": "", + "leftCounter": 966, + "leftRightCounterDelta": 164, + "passageBlindtime": 1.5, + "passageDirection": "LEFT", + "passageSensorSensitivity": 50.0, + "passageTimeout": 0.5, + "rightCounter": 802 + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000054", + "label": "SPDR_1", + "lastStatusUpdate": 1547282742305, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 304, + "modelType": "HmIP-SPDR", + "oem": "eQ-3", + "permanentlyReachable": false, + "serializedGlobalTradeItemNumber": "3014F7110000000000000054", + "type": "PASSAGE_DETECTOR", + "updateState": "UP_TO_DATE" + }, + "3014F711000000000AAAAA25": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.0.12", + "firmwareVersionInteger": 65548, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F711000000000AAAAA25", + "dutyCycle": false, + "functionalChannelType": "DEVICE_PERMANENT_FULL_RX", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000010" + ], + "index": 0, + "label": "", + "lowBat": false, + "permanentFullRx": true, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -46, + "rssiPeerValue": null, + "unreach": false + }, + "1": { + "deviceId": "3014F711000000000AAAAA25", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000048", + "00000000-0000-0000-0000-000000000034" + ], + "index": 1, + "label": "" + }, + "2": { + "deviceId": "3014F711000000000AAAAA25", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000048", + "00000000-0000-0000-0000-000000000034" + ], + "index": 2, + "label": "" + }, + "3": { + "currentIllumination": null, + "deviceId": "3014F711000000000AAAAA25", + "functionalChannelType": "MOTION_DETECTION_CHANNEL", + "groupIndex": 2, + "groups": [], + "illumination": 14.2, + "index": 3, + "label": "", + "motionBufferActive": true, + "motionDetected": false, + "motionDetectionSendInterval": "SECONDS_240", + "numberOfBrightnessMeasurements": 7 + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711000000000AAAAA25", + "label": "Bewegungsmelder für 55er Rahmen – innen", + "lastStatusUpdate": 1546776387401, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 338, + "modelType": "HmIP-SMI55", + "oem": "eQ-3", + "permanentlyReachable": false, + "serializedGlobalTradeItemNumber": "3014F711000000000AAAAA25", + "type": "MOTION_DETECTOR_PUSH_BUTTON", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000038": { + "availableFirmwareVersion": "1.0.18", + "firmwareVersion": "1.0.18", + "firmwareVersionInteger": 65554, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000038", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -55, + "rssiPeerValue": null, + "unreach": false + }, + "1": { + "actualTemperature": 4.3, + "deviceId": "3014F7110000000000000038", + "functionalChannelType": "WEATHER_SENSOR_PLUS_CHANNEL", + "groupIndex": 1, + "groups": [ + ], + "humidity": 97, + "vaporAmount": 6.177718198711658, + "illumination": 26.4, + "illuminationThresholdSunshine": 3500.0, + "index": 1, + "label": "", + "raining": false, + "storm": false, + "sunshine": false, + "todayRainCounter": 3.8999999999999773, + "todaySunshineDuration": 0, + "totalRainCounter": 544.0999999999999, + "totalSunshineDuration": 132057, + "windSpeed": 15.0, + "windValueType": "CURRENT_VALUE", + "yesterdayRainCounter": 25.600000000000023, + "yesterdaySunshineDuration": 0 + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000038", + "label": "Weather Sensor – plus", + "lastStatusUpdate": 1546789939739, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 351, + "modelType": "HmIP-SWO-PL", + "oem": "eQ-3", + "permanentlyReachable": false, + "serializedGlobalTradeItemNumber": "3014F7110000000000000038", + "type": "WEATHER_SENSOR_PLUS", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000BBBBB1": { + "availableFirmwareVersion": "1.6.2", + "firmwareVersion": "1.6.2", + "firmwareVersionInteger": 67074, + "functionalChannels": { + "0": { + "configPending": false, + "coolingEmergencyValue": 0.0, + "deviceId": "3014F7110000000000BBBBB1", + "dutyCycle": false, + "frostProtectionTemperature": 8.0, + "functionalChannelType": "DEVICE_GLOBAL_PUMP_CONTROL", + "globalPumpControl": true, + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000007" + ], + "heatingEmergencyValue": 0.25, + "heatingLoadType": "LOAD_BALANCING", + "heatingValveType": "NORMALLY_CLOSE", + "index": 0, + "label": "", + "lowBat": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -62, + "rssiPeerValue": null, + "unreach": false, + "valveProtectionDuration": 5, + "valveProtectionSwitchingInterval": 14 + }, + "1": { + "deviceId": "3014F7110000000000BBBBB1", + "functionalChannelType": "FLOOR_TERMINAL_BLOCK_LOCAL_PUMP_CHANNEL", + "groupIndex": 1, + "groups": [], + "index": 1, + "label": "", + "pumpFollowUpTime": 2, + "pumpLeadTime": 2, + "pumpProtectionDuration": 1, + "pumpProtectionSwitchingInterval": 14 + }, + "2": { + "deviceId": "3014F7110000000000BBBBB1", + "functionalChannelType": "FLOOR_TERMINAL_BLOCK_CHANNEL", + "groupIndex": 2, + "groups": [ + "00000000-0000-0000-0000-000000000008" + ], + "index": 2, + "label": "" + }, + "3": { + "deviceId": "3014F7110000000000BBBBB1", + "functionalChannelType": "FLOOR_TERMINAL_BLOCK_CHANNEL", + "groupIndex": 3, + "groups": [ + "00000000-0000-0000-0000-000000000009" + ], + "index": 3, + "label": "" + }, + "4": { + "deviceId": "3014F7110000000000BBBBB1", + "functionalChannelType": "FLOOR_TERMINAL_BLOCK_CHANNEL", + "groupIndex": 4, + "groups": [ + "00000000-0000-0000-0000-000000000010" + ], + "index": 4, + "label": "" + }, + "5": { + "deviceId": "3014F7110000000000BBBBB1", + "functionalChannelType": "FLOOR_TERMINAL_BLOCK_CHANNEL", + "groupIndex": 5, + "groups": [ + "00000000-0000-0000-0000-000000000011" + ], + "index": 5, + "label": "" + }, + "6": { + "deviceId": "3014F7110000000000BBBBB1", + "functionalChannelType": "FLOOR_TERMINAL_BLOCK_CHANNEL", + "groupIndex": 6, + "groups": [], + "index": 6, + "label": "" + }, + "7": { + "deviceId": "3014F7110000000000BBBBB1", + "functionalChannelType": "HEAT_DEMAND_CHANNEL", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000012", + "00000000-0000-0000-0000-000000000013" + ], + "index": 7, + "label": "" + }, + "8": { + "deviceId": "3014F7110000000000BBBBB1", + "functionalChannelType": "DEHUMIDIFIER_DEMAND_CHANNEL", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000014" + ], + "index": 8, + "label": "" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000BBBBB1", + "label": "Fußbodenheizungsaktor", + "lastStatusUpdate": 1545746610807, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 277, + "modelType": "HmIP-FAL230-C6", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000BBBBB1", + "type": "FLOOR_TERMINAL_BLOCK_6", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000BBBBB8": { + "availableFirmwareVersion": "1.2.16", + "firmwareVersion": "1.2.16", + "firmwareVersionInteger": 66064, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000BBBBB8", + "dutyCycle": false, + "functionalChannelType": "DEVICE_SABOTAGE", + "groupIndex": 0, + "groups": [ + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -59, + "rssiPeerValue": null, + "sabotage": false, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000BBBBB8", + "functionalChannelType": "ALARM_SIREN_CHANNEL", + "groupIndex": 1, + "groups": [ + ], + "index": 1, + "label": "" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000BBBBB8", + "label": "Alarmsirene", + "lastStatusUpdate": 1544480290322, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 298, + "modelType": "HmIP-ASIR", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000BBBBB8", + "type": "ALARM_SIREN_INDOOR", + "updateState": "UP_TO_DATE" + }, + "3014F711000000000000BB11": { + "availableFirmwareVersion": "1.4.8", + "firmwareVersion": "1.4.8", + "firmwareVersionInteger": 66568, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F711000000000000BB11", + "dutyCycle": false, + "functionalChannelType": "DEVICE_SABOTAGE", + "groupIndex": 0, + "groups": [], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -56, + "rssiPeerValue": -52, + "sabotage": false, + "unreach": false + }, + "1": { + "currentIllumination": null, + "deviceId": "3014F711000000000000BB11", + "functionalChannelType": "MOTION_DETECTION_CHANNEL", + "groupIndex": 1, + "groups": [], + "illumination": 0.1, + "index": 1, + "label": "", + "motionBufferActive": false, + "motionDetected": true, + "motionDetectionSendInterval": "SECONDS_480", + "numberOfBrightnessMeasurements": 7 + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711000000000000BB11", + "label": "Wohnzimmer", + "lastStatusUpdate": 1544480290322, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 291, + "modelType": "HmIP-SMI", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000011", + "type": "MOTION_DETECTOR_INDOOR", + "updateState": "UP_TO_DATE" + }, + "3014F71100000000000BBB17": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.0.2", + "firmwareVersionInteger": 65538, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F71100000000000BBB17", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -70, + "rssiPeerValue": -67, + "unreach": false + }, + "1": { + "currentIllumination": null, + "deviceId": "3014F71100000000000BBB17", + "functionalChannelType": "MOTION_DETECTION_CHANNEL", + "groupIndex": 1, + "groups": [ + ], + "illumination": 233.4, + "index": 1, + "label": "", + "motionBufferActive": true, + "motionDetected": true, + "motionDetectionSendInterval": "SECONDS_240", + "numberOfBrightnessMeasurements": 7 + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F71100000000000BBB17", + "label": "Außen Küche", + "lastStatusUpdate": 1546776559553, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 302, + "modelType": "HmIP-SMO-A", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F71100000000000BBB17", + "type": "MOTION_DETECTOR_OUTDOOR", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000050": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.0.2", + "firmwareVersionInteger": "65538", + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000050", + "dutyCycle": false, + "functionalChannelType": "DEVICE_INCORRECT_POSITIONED", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000020" + ], + "incorrectPositioned": true, + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -65, + "rssiPeerValue": null, + "unreach": false + }, + "1": { + "acousticAlarmSignal": "FREQUENCY_RISING", + "acousticAlarmTiming": "ONCE_PER_MINUTE", + "acousticWaterAlarmTrigger": "WATER_DETECTION", + "deviceId": "3014F7110000000000000050", + "functionalChannelType": "WATER_SENSOR_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000023" + ], + "inAppWaterAlarmTrigger": "WATER_MOISTURE_DETECTION", + "index": 1, + "label": "", + "moistureDetected": false, + "sirenWaterAlarmTrigger": "WATER_MOISTURE_DETECTION", + "waterlevelDetected": false + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000050", + "label": "Wassersensor", + "lastStatusUpdate": 1530802738493, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 353, + "modelType": "HmIP-SWD", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000050", + "type": "WATER_SENSOR", + "updateState": "UP_TO_DATE" + + }, + "3014F7110000000000000000": { + "availableFirmwareVersion": "1.16.8", + "firmwareVersion": "1.16.8", + "firmwareVersionInteger": 69640, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000000", + "dutyCycle": false, + "functionalChannelType": "DEVICE_SABOTAGE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000004", + "00000000-0000-0000-0000-000000000005" + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -85, + "rssiPeerValue": null, + "sabotage": false, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000000", + "eventDelay": 0, + "functionalChannelType": "SHUTTER_CONTACT_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000006", + "00000000-0000-0000-0000-000000000007", + "00000000-0000-0000-0000-000000000005" + ], + "index": 1, + "label": "", + "windowState": "OPEN" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000000", + "label": "Balkontüre", + "lastStatusUpdate": 1524516526498, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 258, + "modelType": "HMIP-SWDO", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000000", + "type": "SHUTTER_CONTACT", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000005551": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.2.12", + "firmwareVersionInteger": 66060, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000005551", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -73, + "rssiPeerValue": null, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000005551", + "eventDelay": 0, + "functionalChannelType": "SHUTTER_CONTACT_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000010", + "00000000-0000-0000-0000-000000000007" + ], + "index": 1, + "label": "", + "windowState": "CLOSED" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000005551", + "label": "Eingangst\u00fcrkontakt", + "lastStatusUpdate": 1524515854304, + "liveUpdateState": "UP_TO_DATE", + "manufacturerCode": 1, + "modelId": 340, + "modelType": "HmIP-SWDM", + "oem": "eQ-3", + "permanentlyReachable": false, + "serializedGlobalTradeItemNumber": "3014F7110000000000005551", + "type": "SHUTTER_CONTACT_MAGNETIC", + "updateState": "BACKGROUND_UPDATE_NOT_SUPPORTED" + }, + "3014F7110000000000000001": { + "availableFirmwareVersion": "1.16.8", + "firmwareVersion": "1.16.8", + "firmwareVersionInteger": 69640, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000001", + "dutyCycle": false, + "functionalChannelType": "DEVICE_SABOTAGE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000008", + "00000000-0000-0000-0000-000000000005" + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -64, + "rssiPeerValue": null, + "sabotage": false, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000001", + "eventDelay": 0, + "functionalChannelType": "SHUTTER_CONTACT_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000009", + "00000000-0000-0000-0000-000000000010", + "00000000-0000-0000-0000-000000000005" + ], + "index": 1, + "label": "", + "windowState": "CLOSED" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000001", + "label": "Fenster", + "lastStatusUpdate": 1524515854304, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 258, + "modelType": "HMIP-SWDO", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000001", + "type": "SHUTTER_CONTACT", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000002": { + "availableFirmwareVersion": "1.16.8", + "firmwareVersion": "1.16.8", + "firmwareVersionInteger": 69640, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000002", + "dutyCycle": false, + "functionalChannelType": "DEVICE_SABOTAGE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000004", + "00000000-0000-0000-0000-000000000005" + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -95, + "rssiPeerValue": null, + "sabotage": false, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000002", + "eventDelay": 0, + "functionalChannelType": "SHUTTER_CONTACT_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000006", + "00000000-0000-0000-0000-000000000007", + "00000000-0000-0000-0000-000000000005" + ], + "index": 1, + "label": "", + "windowState": "CLOSED" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000002", + "label": "Balkonfenster", + "lastStatusUpdate": 1524516088763, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 258, + "modelType": "HMIP-SWDO", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000002", + "type": "SHUTTER_CONTACT", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000003": { + "availableFirmwareVersion": "1.16.8", + "firmwareVersion": "1.16.8", + "firmwareVersionInteger": 69640, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000003", + "dutyCycle": false, + "functionalChannelType": "DEVICE_SABOTAGE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000004", + "00000000-0000-0000-0000-000000000005" + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -78, + "rssiPeerValue": null, + "sabotage": false, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000003", + "eventDelay": 0, + "functionalChannelType": "SHUTTER_CONTACT_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000006", + "00000000-0000-0000-0000-000000000007", + "00000000-0000-0000-0000-000000000005" + ], + "index": 1, + "label": "", + "windowState": "CLOSED" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000003", + "label": "Küche", + "lastStatusUpdate": 1524514836466, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 258, + "modelType": "HMIP-SWDO", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000003", + "type": "SHUTTER_CONTACT", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000004": { + "availableFirmwareVersion": "1.16.8", + "firmwareVersion": "1.16.8", + "firmwareVersionInteger": 69640, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000004", + "dutyCycle": false, + "functionalChannelType": "DEVICE_SABOTAGE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000011", + "00000000-0000-0000-0000-000000000005" + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -56, + "rssiPeerValue": null, + "sabotage": false, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000004", + "eventDelay": 0, + "functionalChannelType": "SHUTTER_CONTACT_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000012", + "00000000-0000-0000-0000-000000000013", + "00000000-0000-0000-0000-000000000005" + ], + "index": 1, + "label": "", + "windowState": "OPEN" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000004", + "label": "Fenster", + "lastStatusUpdate": 1524512404032, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 258, + "modelType": "HMIP-SWDO", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000004", + "type": "SHUTTER_CONTACT", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000005": { + "availableFirmwareVersion": "1.16.8", + "firmwareVersion": "1.16.8", + "firmwareVersionInteger": 69640, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000005", + "dutyCycle": false, + "functionalChannelType": "DEVICE_SABOTAGE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000004", + "00000000-0000-0000-0000-000000000005" + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -80, + "rssiPeerValue": null, + "sabotage": false, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000005", + "eventDelay": 0, + "functionalChannelType": "SHUTTER_CONTACT_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000006", + "00000000-0000-0000-0000-000000000007", + "00000000-0000-0000-0000-000000000005" + ], + "index": 1, + "label": "", + "windowState": "OPEN" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000005", + "label": "Wohnzimmer", + "lastStatusUpdate": 0, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 258, + "modelType": "HMIP-SWDO", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000005", + "type": "SHUTTER_CONTACT", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000006": { + "availableFirmwareVersion": "1.16.8", + "firmwareVersion": "1.16.8", + "firmwareVersionInteger": 69640, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000006", + "dutyCycle": false, + "functionalChannelType": "DEVICE_SABOTAGE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000014", + "00000000-0000-0000-0000-000000000005" + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -76, + "rssiPeerValue": null, + "sabotage": false, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000006", + "eventDelay": 0, + "functionalChannelType": "SHUTTER_CONTACT_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000015", + "00000000-0000-0000-0000-000000000005" + ], + "index": 1, + "label": "", + "windowState": "CLOSED" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000006", + "label": "Wohnungstüre", + "lastStatusUpdate": 1524516489316, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 258, + "modelType": "HMIP-SWDO", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000006", + "type": "SHUTTER_CONTACT", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000007": { + "availableFirmwareVersion": "1.16.8", + "firmwareVersion": "1.16.8", + "firmwareVersionInteger": 69640, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000007", + "dutyCycle": false, + "functionalChannelType": "DEVICE_SABOTAGE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000014", + "00000000-0000-0000-0000-000000000016" + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -56, + "rssiPeerValue": null, + "sabotage": false, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000007", + "eventDelay": 0, + "functionalChannelType": "SHUTTER_CONTACT_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000016", + "00000000-0000-0000-0000-000000000015" + ], + "index": 1, + "label": "", + "windowState": "CLOSED" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000007", + "label": "Vorzimmer", + "lastStatusUpdate": 1524515489257, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 258, + "modelType": "HMIP-SWDO", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000007", + "type": "SHUTTER_CONTACT", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000108": { + "availableFirmwareVersion": "1.12.6", + "firmwareVersion": "1.12.6", + "firmwareVersionInteger": 68614, + "functionalChannels": { + "0": { + "coProFaulty": false, + "coProRestartNeeded": false, + "coProUpdateFailure": false, + "configPending": false, + "deviceId": "3014F7110000000000000108", + "deviceOverheated": false, + "deviceOverloaded": false, + "deviceUndervoltage": false, + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000009" + ], + "index": 0, + "label": "", + "lowBat": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -68, + "rssiPeerValue": -63, + "supportedOptionalFeatures": { + "IFeatureDeviceCoProError": false, + "IFeatureDeviceCoProRestart": false, + "IFeatureDeviceCoProUpdate": false, + "IFeatureDeviceOverheated": true, + "IFeatureDeviceOverloaded": false, + "IFeatureDeviceTemperatureOutOfRange": false, + "IFeatureDeviceUndervoltage": false + }, + "temperatureOutOfRange": false, + "unreach": false + }, + "1": { + "currentPowerConsumption": 0.0, + "deviceId": "3014F7110000000000000108", + "energyCounter": 6.333200000000001, + "functionalChannelType": "SWITCH_MEASURING_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000023" + ], + "index": 1, + "label": "", + "on": false, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000108", + "label": "Flur oben", + "lastStatusUpdate": 1570365990392, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 288, + "modelType": "HmIP-BSM", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000108", + "type": "BRAND_SWITCH_MEASURING", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000109": { + "availableFirmwareVersion": "1.6.2", + "firmwareVersion": "1.6.2", + "firmwareVersionInteger": 67074, + "functionalChannels": { + "0": { + "coProFaulty": false, + "coProRestartNeeded": false, + "coProUpdateFailure": false, + "configPending": false, + "deviceId": "3014F7110000000000000109", + "deviceOverheated": null, + "deviceOverloaded": false, + "deviceUndervoltage": false, + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000029" + ], + "index": 0, + "label": "", + "lowBat": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -80, + "rssiPeerValue": -73, + "supportedOptionalFeatures": { + "IFeatureDeviceCoProError": false, + "IFeatureDeviceCoProRestart": false, + "IFeatureDeviceCoProUpdate": false, + "IFeatureDeviceOverheated": true, + "IFeatureDeviceOverloaded": false, + "IFeatureDeviceTemperatureOutOfRange": false, + "IFeatureDeviceUndervoltage": false + }, + "temperatureOutOfRange": false, + "unreach": false + }, + "1": { + "currentPowerConsumption": 0.0, + "deviceId": "3014F7110000000000000109", + "energyCounter": 0.0011, + "functionalChannelType": "SWITCH_MEASURING_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000030" + ], + "index": 1, + "label": "", + "on": false, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000011", + "label": "Ausschalter Terrasse Bewegungsmelder", + "lastStatusUpdate": 1570366291250, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 289, + "modelType": "HmIP-FSM", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000109", + "type": "FULL_FLUSH_SWITCH_MEASURING", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000008": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "2.6.2", + "firmwareVersionInteger": 132610, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000008", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000017" + ], + "index": 0, + "label": "", + "lowBat": null, + "routerModuleEnabled": true, + "routerModuleSupported": true, + "rssiDeviceValue": -48, + "rssiPeerValue": -49, + "unreach": false + }, + "1": { + "currentPowerConsumption": 195.3, + "deviceId": "3014F7110000000000000008", + "energyCounter": 35.536, + "functionalChannelType": "SWITCH_MEASURING_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000018" + ], + "index": 1, + "label": "", + "on": true, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000008", + "label": "Pc", + "lastStatusUpdate": 1524516554056, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 262, + "modelType": "HMIP-PSM", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000008", + "type": "PLUGABLE_SWITCH_MEASURING", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000009": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "2.6.2", + "firmwareVersionInteger": 132610, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000009", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000017" + ], + "index": 0, + "label": "", + "lowBat": null, + "routerModuleEnabled": true, + "routerModuleSupported": true, + "rssiDeviceValue": -60, + "rssiPeerValue": -66, + "unreach": false + }, + "1": { + "currentPowerConsumption": 0.0, + "deviceId": "3014F7110000000000000009", + "energyCounter": 0.4754, + "functionalChannelType": "SWITCH_MEASURING_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000018" + ], + "index": 1, + "label": "", + "on": false, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000009", + "label": "Brunnen", + "lastStatusUpdate": 1524515786303, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 262, + "modelType": "HMIP-PSM", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000009", + "type": "PLUGABLE_SWITCH_MEASURING", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000010": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "2.6.2", + "firmwareVersionInteger": 132610, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000010", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000017" + ], + "index": 0, + "label": "", + "lowBat": null, + "routerModuleEnabled": true, + "routerModuleSupported": true, + "rssiDeviceValue": -47, + "rssiPeerValue": -49, + "unreach": false + }, + "1": { + "currentPowerConsumption": 2.04, + "deviceId": "3014F7110000000000000010", + "energyCounter": 1.5343, + "functionalChannelType": "SWITCH_MEASURING_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000018" + ], + "index": 1, + "label": "", + "on": true, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000010", + "label": "Büro", + "lastStatusUpdate": 1524513613922, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 262, + "modelType": "HMIP-PSM", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000010", + "type": "PLUGABLE_SWITCH_MEASURING", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000110": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "2.6.2", + "firmwareVersionInteger": 132610, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000110", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000017" + ], + "index": 0, + "label": "", + "lowBat": null, + "routerModuleEnabled": true, + "routerModuleSupported": true, + "rssiDeviceValue": -47, + "rssiPeerValue": -49, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000110", + "functionalChannelType": "SWITCH_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000018" + ], + "index": 1, + "label": "", + "on": true, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000110", + "label": "Schrank", + "lastStatusUpdate": 1524513613922, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 262, + "modelType": "HMIP-PS", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000110", + "type": "PLUGABLE_SWITCH", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000011": { + "automaticValveAdaptionNeeded": false, + "availableFirmwareVersion": "2.0.2", + "firmwareVersion": "2.0.2", + "firmwareVersionInteger": 131074, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000011", + "dutyCycle": false, + "functionalChannelType": "DEVICE_OPERATIONLOCK", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000011" + ], + "index": 0, + "label": "", + "lowBat": false, + "operationLockActive": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -54, + "rssiPeerValue": -51, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000011", + "functionalChannelType": "HEATING_THERMOSTAT_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000012" + ], + "index": 1, + "label": "", + "valveActualTemperature": 20.0, + "setPointTemperature": 5.0, + "temperatureOffset": 0.0, + "valvePosition": 0.0, + "valveState": "ADAPTION_DONE" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000011", + "label": "Heizung", + "lastStatusUpdate": 1524516360178, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 269, + "modelType": "HMIP-eTRV", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000011", + "type": "HEATING_THERMOSTAT", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000012": { + "automaticValveAdaptionNeeded": false, + "availableFirmwareVersion": "2.0.2", + "firmwareVersion": "2.0.2", + "firmwareVersionInteger": 131074, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000012", + "dutyCycle": false, + "functionalChannelType": "DEVICE_OPERATIONLOCK", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000008" + ], + "index": 0, + "label": "", + "lowBat": false, + "operationLockActive": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -46, + "rssiPeerValue": -54, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000012", + "functionalChannelType": "HEATING_THERMOSTAT_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000010" + ], + "index": 1, + "label": "", + "valveActualTemperature": 20.0, + "setPointTemperature": 19.0, + "temperatureOffset": 0.0, + "valvePosition": 0.0, + "valveState": "ADAPTION_DONE" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000012", + "label": "Heizkörperthermostat", + "lastStatusUpdate": 1524514105832, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 269, + "modelType": "HMIP-eTRV", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000012", + "type": "HEATING_THERMOSTAT", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000013": { + "automaticValveAdaptionNeeded": false, + "availableFirmwareVersion": "2.0.2", + "firmwareVersion": "2.0.2", + "firmwareVersionInteger": 131074, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000013", + "dutyCycle": false, + "functionalChannelType": "DEVICE_OPERATIONLOCK", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000014" + ], + "index": 0, + "label": "", + "lowBat": false, + "operationLockActive": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -58, + "rssiPeerValue": -58, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000013", + "functionalChannelType": "HEATING_THERMOSTAT_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000019" + ], + "index": 1, + "label": "", + "valveActualTemperature": 20.0, + "setPointTemperature": 5.0, + "temperatureOffset": 0.0, + "valvePosition": 0.0, + "valveState": "ADAPTION_DONE" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000013", + "label": "Heizkörperthermostat", + "lastStatusUpdate": 1524514007132, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 269, + "modelType": "HMIP-eTRV", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000013", + "type": "HEATING_THERMOSTAT", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000014": { + "automaticValveAdaptionNeeded": false, + "availableFirmwareVersion": "2.0.2", + "firmwareVersion": "2.0.2", + "firmwareVersionInteger": 131074, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000014", + "dutyCycle": false, + "functionalChannelType": "DEVICE_OPERATIONLOCK", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000004" + ], + "index": 0, + "label": "", + "lowBat": false, + "operationLockActive": true, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -60, + "rssiPeerValue": -58, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000014", + "functionalChannelType": "HEATING_THERMOSTAT_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000007" + ], + "index": 1, + "label": "", + "valveActualTemperature": 20.0, + "setPointTemperature": 5.0, + "temperatureOffset": 0.0, + "valvePosition": 0.0, + "valveState": "ADAPTION_DONE" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000014", + "label": "Küche-Heizung", + "lastStatusUpdate": 1524513898337, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 269, + "modelType": "HMIP-eTRV", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000014", + "type": "HEATING_THERMOSTAT", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000015": { + "automaticValveAdaptionNeeded": false, + "availableFirmwareVersion": "2.0.2", + "firmwareVersion": "2.0.2", + "firmwareVersionInteger": 131074, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000015", + "dutyCycle": false, + "functionalChannelType": "DEVICE_OPERATIONLOCK", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000004" + ], + "index": 0, + "label": "", + "lowBat": false, + "operationLockActive": true, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -65, + "rssiPeerValue": -66, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000015", + "functionalChannelType": "HEATING_THERMOSTAT_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000007" + ], + "index": 1, + "label": "", + "valveActualTemperature": 20.0, + "setPointTemperature": 5.0, + "temperatureOffset": 0.0, + "valvePosition": 0.0, + "valveState": "ADAPTION_DONE" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000015", + "label": "Wohnzimmer-Heizung", + "lastStatusUpdate": 1524513950325, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 269, + "modelType": "HMIP-eTRV", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000015", + "type": "HEATING_THERMOSTAT", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000016": { + "automaticValveAdaptionNeeded": false, + "availableFirmwareVersion": "2.0.2", + "firmwareVersion": "2.0.2", + "firmwareVersionInteger": 131074, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000016", + "dutyCycle": false, + "functionalChannelType": "DEVICE_OPERATIONLOCK", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000020" + ], + "index": 0, + "label": "", + "lowBat": false, + "operationLockActive": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -50, + "rssiPeerValue": -51, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000016", + "functionalChannelType": "HEATING_THERMOSTAT_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000021" + ], + "index": 1, + "label": "", + "valveActualTemperature": 20.0, + "setPointTemperature": 5.0, + "temperatureOffset": 0.0, + "valvePosition": 0.0, + "valveState": "ADAPTION_DONE" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000016", + "label": "Heizkörperthermostat", + "lastStatusUpdate": 1524514626157, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 269, + "modelType": "HMIP-eTRV", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000016", + "type": "HEATING_THERMOSTAT", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000017": { + "automaticValveAdaptionNeeded": false, + "availableFirmwareVersion": "2.0.2", + "firmwareVersion": "2.0.2", + "firmwareVersionInteger": 131074, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000017", + "dutyCycle": false, + "functionalChannelType": "DEVICE_OPERATIONLOCK", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000004" + ], + "index": 0, + "label": "", + "lowBat": false, + "operationLockActive": true, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -67, + "rssiPeerValue": -62, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000017", + "functionalChannelType": "HEATING_THERMOSTAT_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000007" + ], + "index": 1, + "label": "", + "valveActualTemperature": 20.0, + "setPointTemperature": 5.0, + "temperatureOffset": 0.0, + "valvePosition": 0.0, + "valveState": "ADAPTION_DONE" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000017", + "label": "Balkon-Heizung", + "lastStatusUpdate": 1524511331830, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 269, + "modelType": "HMIP-eTRV", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000017", + "type": "HEATING_THERMOSTAT", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000018": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.0.11", + "firmwareVersionInteger": 65547, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000018", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000004", + "00000000-0000-0000-0000-000000000016" + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -67, + "rssiPeerValue": null, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000018", + "functionalChannelType": "SMOKE_DETECTOR_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000022", + "00000000-0000-0000-0000-000000000006" + ], + "index": 1, + "label": "", + "smokeDetectorAlarmType": "IDLE_OFF" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000018", + "label": "Rauchwarnmelder", + "lastStatusUpdate": 1524461072721, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 296, + "modelType": "HmIP-SWSD", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000018", + "type": "SMOKE_DETECTOR", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000019": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.0.11", + "firmwareVersionInteger": 65547, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000019", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000008", + "00000000-0000-0000-0000-000000000016" + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -50, + "rssiPeerValue": null, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000019", + "functionalChannelType": "SMOKE_DETECTOR_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000022", + "00000000-0000-0000-0000-000000000009" + ], + "index": 1, + "label": "", + "smokeDetectorAlarmType": "IDLE_OFF" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000019", + "label": "Rauchwarnmelder", + "lastStatusUpdate": 1524480981494, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 296, + "modelType": "HmIP-SWSD", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000019", + "type": "SMOKE_DETECTOR", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000020": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.0.11", + "firmwareVersionInteger": 65547, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000020", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000011", + "00000000-0000-0000-0000-000000000016" + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -54, + "rssiPeerValue": null, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000020", + "functionalChannelType": "SMOKE_DETECTOR_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000013", + "00000000-0000-0000-0000-000000000022" + ], + "index": 1, + "label": "", + "smokeDetectorAlarmType": "IDLE_OFF" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000020", + "label": "Rauchwarnmelder", + "lastStatusUpdate": 1524456324824, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 296, + "modelType": "HmIP-SWSD", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000020", + "type": "SMOKE_DETECTOR", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000021": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.0.11", + "firmwareVersionInteger": 65547, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000021", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000014", + "00000000-0000-0000-0000-000000000016" + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -80, + "rssiPeerValue": null, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000021", + "functionalChannelType": "SMOKE_DETECTOR_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000022", + "00000000-0000-0000-0000-000000000015" + ], + "index": 1, + "label": "", + "smokeDetectorAlarmType": "IDLE_OFF" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000021", + "label": "Rauchwarnmelder", + "lastStatusUpdate": 1524443129876, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 296, + "modelType": "HmIP-SWSD", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000021", + "type": "SMOKE_DETECTOR", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000022": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.8.0", + "firmwareVersionInteger": 67584, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000022", + "dutyCycle": false, + "functionalChannelType": "DEVICE_OPERATIONLOCK", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000011" + ], + "index": 0, + "label": "", + "lowBat": false, + "operationLockActive": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -76, + "rssiPeerValue": -63, + "unreach": false + }, + "1": { + "actualTemperature": 24.7, + "deviceId": "3014F7110000000000000022", + "display": "ACTUAL_HUMIDITY", + "functionalChannelType": "WALL_MOUNTED_THERMOSTAT_PRO_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000012" + ], + "humidity": 43, + "vaporAmount": 6.177718198711658, + "index": 1, + "label": "", + "valveActualTemperature": 20.0, + "setPointTemperature": 5.0, + "temperatureOffset": 0.0 + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000022", + "label": "Wandthermostat", + "lastStatusUpdate": 1524516534382, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 297, + "modelType": "HmIP-WTH-2", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000022", + "type": "WALL_MOUNTED_THERMOSTAT_PRO", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000023": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.8.0", + "firmwareVersionInteger": 67584, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000023", + "dutyCycle": false, + "functionalChannelType": "DEVICE_OPERATIONLOCK", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000008" + ], + "index": 0, + "label": "", + "lowBat": false, + "operationLockActive": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -61, + "rssiPeerValue": -58, + "unreach": false + }, + "1": { + "actualTemperature": 24.5, + "deviceId": "3014F7110000000000000023", + "display": "ACTUAL_HUMIDITY", + "functionalChannelType": "WALL_MOUNTED_THERMOSTAT_PRO_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000010" + ], + "humidity": 46, + "vaporAmount": 6.177718198711658, + "index": 1, + "label": "", + "valveActualTemperature": 20.0, + "setPointTemperature": 19.0, + "temperatureOffset": 0.0 + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000023", + "label": "Wandthermostat", + "lastStatusUpdate": 1524516454116, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 297, + "modelType": "HmIP-WTH-2", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000023", + "type": "WALL_MOUNTED_THERMOSTAT_PRO", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000024": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.8.0", + "firmwareVersionInteger": 67584, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000024", + "dutyCycle": false, + "functionalChannelType": "DEVICE_OPERATIONLOCK", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000004" + ], + "index": 0, + "label": "", + "lowBat": false, + "operationLockActive": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -75, + "rssiPeerValue": -85, + "unreach": false + }, + "1": { + "actualTemperature": 23.6, + "deviceId": "3014F7110000000000000024", + "display": "ACTUAL", + "functionalChannelType": "WALL_MOUNTED_THERMOSTAT_PRO_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000007" + ], + "humidity": 45, + "vaporAmount": 6.177718198711658, + "index": 1, + "label": "", + "valveActualTemperature": 20.0, + "setPointTemperature": 5.0, + "temperatureOffset": 0.0 + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000024", + "label": "Wandthermostat", + "lastStatusUpdate": 1524516436601, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 297, + "modelType": "HmIP-WTH-2", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000024", + "type": "WALL_MOUNTED_THERMOSTAT_PRO", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000025": { + "availableFirmwareVersion": "1.8.0", + "firmwareVersion": "1.8.0", + "firmwareVersionInteger": 67584, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000025", + "dutyCycle": false, + "functionalChannelType": "DEVICE_OPERATIONLOCK", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000020" + ], + "index": 0, + "label": "", + "lowBat": false, + "operationLockActive": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -46, + "rssiPeerValue": -47, + "unreach": false + }, + "1": { + "actualTemperature": 23.8, + "deviceId": "3014F7110000000000000025", + "display": "ACTUAL_HUMIDITY", + "functionalChannelType": "WALL_MOUNTED_THERMOSTAT_PRO_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000021" + ], + "humidity": 47, + "vaporAmount": 6.177718198711658, + "index": 1, + "label": "", + "valveActualTemperature": 20.0, + "setPointTemperature": 5.0, + "temperatureOffset": 0.0 + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000025", + "label": "Wandthermostat", + "lastStatusUpdate": 1524516556479, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 297, + "modelType": "HmIP-WTH-2", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000025", + "type": "WALL_MOUNTED_THERMOSTAT_PRO", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000029": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.0.14", + "firmwareVersionInteger": 65550, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000029", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000019" + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -46, + "rssiPeerValue": null, + "unreach": false + }, + "1": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "deviceId": "3014F7110000000000000029", + "functionalChannelType": "MULTI_MODE_INPUT_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000020" + ], + "index": 1, + "label": "", + "multiModeInputMode": "KEY_BEHAVIOR", + "windowState": "CLOSED" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000029", + "label": "Kontakt-Schnittstelle Unterputz – 1-fach", + "lastStatusUpdate": 1547923306429, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 382, + "modelType": "HmIP-FCI1", + "oem": "eQ-3", + "permanentlyReachable": false, + "serializedGlobalTradeItemNumber": "3014F7110000000000000029", + "type": "FULL_FLUSH_CONTACT_INTERFACE", + "updateState": "UP_TO_DATE" + }, + "3014F711AAAA000000000001": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.0.10", + "firmwareVersionInteger": 65546, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F711AAAA000000000001", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000008" + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -68, + "rssiPeerValue": null, + "unreach": false + }, + "1": { + "actualTemperature": 15.4, + "deviceId": "3014F711AAAA000000000001", + "functionalChannelType": "WEATHER_SENSOR_PRO_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-AAAA-0000-0000-000000000001" + ], + "humidity": 65, + "vaporAmount": 6.177718198711658, + "illumination": 4153.0, + "illuminationThresholdSunshine": 10.0, + "index": 1, + "label": "", + "raining": false, + "storm": false, + "sunshine": true, + "todayRainCounter": 6.5, + "todaySunshineDuration": 100, + "totalRainCounter": 6.5, + "totalSunshineDuration": 100, + "weathervaneAlignmentNeeded": false, + "windDirection": 295.0, + "windDirectionVariation": 56.25, + "windSpeed": 2.6, + "windValueType": "AVERAGE_VALUE", + "yesterdayRainCounter": 0.0, + "yesterdaySunshineDuration": 0 + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711AAAA000000000001", + "label": "Wettersensor - pro", + "lastStatusUpdate": 1524513950325, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 352, + "modelType": "HmIP-SWO-PR", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F711AAAA000000000001", + "type": "WEATHER_SENSOR_PRO", + "updateState": "UP_TO_DATE" + }, + "3014F711AAAA000000000002": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.0.6", + "firmwareVersionInteger": 65542, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F711AAAA000000000002", + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "dutyCycle": false, + "groups": [ + "00000000-0000-0000-0000-000000000008" + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -55, + "rssiPeerValue": null, + "unreach": false + }, + "1": { + "actualTemperature": 15.1, + "deviceId": "3014F711AAAA000000000002", + "functionalChannelType": "CLIMATE_SENSOR_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-AAAA-0000-0000-000000000001" + ], + "humidity": 70, + "vaporAmount": 6.177718198711658, + "index": 1, + "label": "" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711AAAA000000000002", + "label": "Temperatur- und Luftfeuchtigkeitssensor - außen", + "lastStatusUpdate": 1524513950325, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 314, + "modelType": "HmIP-STHO", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F711AAAA000000000002", + "type": "TEMPERATURE_HUMIDITY_SENSOR_OUTDOOR", + "updateState": "UP_TO_DATE" + }, + "3014F711AAAA000000000003": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.0.10", + "firmwareVersionInteger": 65546, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F711AAAA000000000003", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ "00000000-0000-0000-0000-000000000008" ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -77, + "rssiPeerValue": null, + "unreach": false + }, + "1": { + "actualTemperature": 15.2, + "deviceId": "3014F711AAAA000000000003", + "functionalChannelType": "WEATHER_SENSOR_CHANNEL", + "groupIndex": 1, + "groups": [ "00000000-AAAA-0000-0000-000000000001" ], + "humidity": 42, + "vaporAmount": 6.177718198711658, + "illumination": 4890.0, + "illuminationThresholdSunshine": 3500.0, + "index": 1, + "label": "", + "storm": false, + "sunshine": true, + "todaySunshineDuration": 51, + "totalSunshineDuration": 54, + "windSpeed": 6.6, + "windValueType": "MAX_VALUE", + "yesterdaySunshineDuration": 3 + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711AAAA000000000003", + "label": "Wettersensor", + "lastStatusUpdate": 1524513950325, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 350, + "modelType": "HmIP-SWO-B", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F711AAAA000000000003", + "type": "WEATHER_SENSOR", + "updateState": "UP_TO_DATE" + }, + "3014F711AAAA000000000004": { + "availableFirmwareVersion": "1.2.10", + "firmwareVersion": "1.2.10", + "firmwareVersionInteger": 66058, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F711AAAA000000000004", + "dutyCycle": false, + "functionalChannelType": "DEVICE_SABOTAGE", + "groupIndex": 0, + "groups": [ "00000000-0000-0000-0000-000000000008" ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -54, + "rssiPeerValue": null, + "sabotage": false, + "unreach": false + }, + "1": { + "deviceId": "3014F711AAAA000000000004", + "eventDelay": 0, + "functionalChannelType": "ROTARY_HANDLE_CHANNEL", + "groupIndex": 1, + "groups": [ "00000000-0000-0000-0000-000000000009" ], + "index": 1, + "label": "", + "windowState": "TILTED" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711AAAA000000000004", + "label": "Fenstergriffsensor", + "lastStatusUpdate": 1524816385462, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 286, + "modelType": "HmIP-SRH", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F711AAAA000000000004", + "type": "ROTARY_HANDLE_SENSOR", + "updateState": "UP_TO_DATE" + }, + "3014F711AAAA000000000005": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.4.8", + "firmwareVersionInteger": 66568, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F711AAAA000000000005", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000008" + ], + "index": 0, + "label": "", + "lowBat": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -44, + "rssiPeerValue": -42, + "unreach": false + }, + "1": { + "deviceId": "3014F711AAAA000000000005", + "dimLevel": 0.0, + "functionalChannelType": "DIMMER_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000008" + ], + "index": 1, + "label": "", + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711AAAA000000000005", + "label": "Schlafzimmerlicht", + "lastStatusUpdate": 1524816385462, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 290, + "modelType": "HmIP-BDT", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F711AAAA000000000005", + "type": "BRAND_DIMMER", + "updateState": "UP_TO_DATE" + }, + "3014F711BBBBBBBBBBBBB017": { + "availableFirmwareVersion": "1.0.19", + "firmwareVersion": "1.0.19", + "firmwareVersionInteger": 65555, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F711BBBBBBBBBBBBB017", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -61, + "rssiPeerValue": null, + "unreach": false + }, + "1": { + "deviceId": "3014F711BBBBBBBBBBBBB017", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 1, + "groups": [ + ], + "index": 1, + "label": "" + }, + "2": { + "deviceId": "3014F711BBBBBBBBBBBBB017", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 1, + "groups": [ + ], + "index": 2, + "label": "" + }, + "3": { + "deviceId": "3014F711BBBBBBBBBBBBB017", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 2, + "groups": [ + ], + "index": 3, + "label": "" + }, + "4": { + "deviceId": "3014F711BBBBBBBBBBBBB017", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 2, + "groups": [ + ], + "index": 4, + "label": "" + }, + "5": { + "deviceId": "3014F711BBBBBBBBBBBBB017", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 3, + "groups": [ + ], + "index": 5, + "label": "" + }, + "6": { + "deviceId": "3014F711BBBBBBBBBBBBB017", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 3, + "groups": [ + ], + "index": 6, + "label": "" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711BBBBBBBBBBBBB017", + "label": "Wandtaster - 6-fach", + "lastStatusUpdate": 1544475961687, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 300, + "modelType": "HmIP-WRC6", + "oem": "eQ-3", + "permanentlyReachable": false, + "serializedGlobalTradeItemNumber": "3014F711BBBBBBBBBBBBB017", + "type": "PUSH_BUTTON_6", + "updateState": "UP_TO_DATE" + }, + "3014F711BBBBBBBBBBBBB016": { + "availableFirmwareVersion": "1.0.19", + "firmwareVersion": "1.0.19", + "firmwareVersionInteger": 65555, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F711BBBBBBBBBBBBB016", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -42, + "rssiPeerValue": null, + "unreach": false + }, + "1": { + "deviceId": "3014F711BBBBBBBBBBBBB016", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 1, + "groups": [ + ], + "index": 1, + "label": "" + }, + "2": { + "deviceId": "3014F711BBBBBBBBBBBBB016", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 1, + "groups": [ + ], + "index": 2, + "label": "" + }, + "3": { + "deviceId": "3014F711BBBBBBBBBBBBB016", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 2, + "groups": [ + ], + "index": 3, + "label": "" + }, + "4": { + "deviceId": "3014F711BBBBBBBBBBBBB016", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 2, + "groups": [ + ], + "index": 4, + "label": "" + }, + "5": { + "deviceId": "3014F711BBBBBBBBBBBBB016", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 3, + "groups": [ + ], + "index": 5, + "label": "" + }, + "6": { + "deviceId": "3014F711BBBBBBBBBBBBB016", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 3, + "groups": [ + ], + "index": 6, + "label": "" + }, + "7": { + "deviceId": "3014F711BBBBBBBBBBBBB016", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 4, + "groups": [ + ], + "index": 7, + "label": "" + }, + "8": { + "deviceId": "3014F711BBBBBBBBBBBBB016", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 4, + "groups": [ + ], + "index": 8, + "label": "" + } + + + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711BBBBBBBBBBBBB016", + "label": "Fernbedienung - 8 Tasten", + "lastStatusUpdate": 1544479483638, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 299, + "modelType": "HmIP-RC8", + "oem": "eQ-3", + "permanentlyReachable": false, + "serializedGlobalTradeItemNumber": "3014F711BBBBBBBBBBBBB016", + "type": "REMOTE_CONTROL_8", + "updateState": "UP_TO_DATE" + }, + "3014F711AAAAAAAAAAAAAA51": { + "availableFirmwareVersion": "1.4.0", + "firmwareVersion": "1.4.0", + "firmwareVersionInteger": 66560, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F711AAAAAAAAAAAAAA51", + "dutyCycle": false, + "functionalChannelType": "DEVICE_SABOTAGE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000021", + "00000000-0000-0000-0000-000000000060" + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -62, + "rssiPeerValue": -61, + "sabotage": false, + "unreach": false + }, + "1": { + "currentIllumination": null, + "deviceId": "3014F711AAAAAAAAAAAAAA51", + "functionalChannelType": "PRESENCE_DETECTION_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000022", + "00000000-0000-0000-0000-000000000060" + ], + "illumination": 1.8, + "index": 1, + "label": "", + "motionBufferActive": false, + "motionDetectionSendInterval": "SECONDS_240", + "numberOfBrightnessMeasurements": 7, + "presenceDetected": false + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711AAAAAAAAAAAAAA51", + "label": "SPI_1", + "lastStatusUpdate": 1542758692234, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 303, + "modelType": "HmIP-SPI", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F711AAAAAAAAAAAAAA51", + "type": "PRESENCE_DETECTOR_INDOOR", + "updateState": "UP_TO_DATE" + }, + "3014F711ACBCDABCADCA66": { + "availableFirmwareVersion": "1.6.2", + "firmwareVersion": "1.6.2", + "firmwareVersionInteger": 67074, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F711ACBCDABCADCA66", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000024" + ], + "index": 0, + "label": "", + "lowBat": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -78, + "rssiPeerValue": -77, + "unreach": false + }, + "1": { + "bottomToTopReferenceTime": 30.080000000000002, + "changeOverDelay": 0.5, + "delayCompensationValue": 12.7, + "deviceId": "3014F711ACBCDABCADCA66", + "endpositionAutoDetectionEnabled": true, + "functionalChannelType": "SHUTTER_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000069", + "00000000-0000-0000-0000-000000000070" + ], + "index": 1, + "label": "", + "previousShutterLevel": null, + "processing": false, + "profileMode": "AUTOMATIC", + "selfCalibrationInProgress": null, + "shutterLevel": 1.0, + "supportingDelayCompensation": true, + "supportingEndpositionAutoDetection": true, + "supportingSelfCalibration": true, + "topToBottomReferenceTime": 24.68, + "userDesiredProfileMode": "AUTOMATIC" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711ACBCDABCADCA66", + "label": "BROLL_1", + "lastStatusUpdate": 1542756558785, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 323, + "modelType": "HmIP-BROLL", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F711ACBCDABCADCA66", + "type": "BRAND_SHUTTER", + "updateState": "UP_TO_DATE" + }, + "3014F711BBBBBBBBBBBBB18": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.8.12", + "firmwareVersionInteger": 67596, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F711BBBBBBBBBBBBB18", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000041" + ], + "index": 0, + "label": "", + "lowBat": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -35, + "rssiPeerValue": -36, + "unreach": false + }, + "1": { + "deviceId": "3014F711BBBBBBBBBBBBB18", + "functionalChannelType": "SWITCH_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000042" + ], + "index": 1, + "label": "", + "on": false, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + }, + "2": { + "deviceId": "3014F711BBBBBBBBBBBBB18", + "functionalChannelType": "SWITCH_CHANNEL", + "groupIndex": 2, + "groups": [ + "00000000-0000-0000-0000-000000000042", + "00000000-0000-0000-0000-000000000040" + ], + "index": 2, + "label": "", + "on": false, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + }, + "3": { + "deviceId": "3014F711BBBBBBBBBBBBB18", + "functionalChannelType": "SWITCH_CHANNEL", + "groupIndex": 3, + "groups": [ + "00000000-0000-0000-0000-000000000042" + ], + "index": 3, + "label": "", + "on": false, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + }, + "4": { + "deviceId": "3014F711BBBBBBBBBBBBB18", + "functionalChannelType": "SWITCH_CHANNEL", + "groupIndex": 4, + "groups": [ + "00000000-0000-0000-0000-000000000042" + ], + "index": 4, + "label": "", + "on": false, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + }, + "5": { + "deviceId": "3014F711BBBBBBBBBBBBB18", + "functionalChannelType": "SWITCH_CHANNEL", + "groupIndex": 5, + "groups": [ + "00000000-0000-0000-0000-000000000042" + ], + "index": 5, + "label": "", + "on": false, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + }, + "6": { + "deviceId": "3014F711BBBBBBBBBBBBB18", + "functionalChannelType": "SWITCH_CHANNEL", + "groupIndex": 6, + "groups": [ + "00000000-0000-0000-0000-000000000042" + ], + "index": 6, + "label": "", + "on": false, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + }, + "7": { + "deviceId": "3014F711BBBBBBBBBBBBB18", + "functionalChannelType": "SWITCH_CHANNEL", + "groupIndex": 7, + "groups": [ + "00000000-0000-0000-0000-000000000042" + ], + "index": 7, + "label": "", + "on": false, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + }, + "8": { + "deviceId": "3014F711BBBBBBBBBBBBB18", + "functionalChannelType": "SWITCH_CHANNEL", + "groupIndex": 8, + "groups": [ + "00000000-0000-0000-0000-000000000042" + ], + "index": 8, + "label": "", + "on": true, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711BBBBBBBBBBBBB18", + "label": "ioBroker", + "lastStatusUpdate": 1543746604446, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 307, + "modelType": "HmIP-MOD-OC8", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F711BBBBBBBBBBBBB18", + "type": "OPEN_COLLECTOR_8_MODULE", + "updateState": "UP_TO_DATE" + } + }, + "groups": { + "00000000-0000-0000-0000-000000000020": { + "channels": [ + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000025" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000016" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000050" + } + ], + "configPending": false, + "dutyCycle": false, + "groups": [ + "00000000-0000-0000-0000-000000000021" + ], + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000020", + "incorrectPositioned": null, + "label": "Badezimmer", + "lastStatusUpdate": 1524516556479, + "lowBat": false, + "metaGroupId": null, + "sabotage": null, + "type": "META", + "unreach": false + }, + "00000000-0000-0000-0000-000000000012": { + "activeProfile": "PROFILE_1", + "actualTemperature": 24.7, + "boostDuration": 15, + "boostMode": false, + "channels": [ + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000004" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000022" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000011" + } + ], + "controlMode": "AUTOMATIC", + "controllable": true, + "cooling": false, + "coolingAllowed": false, + "coolingIgnored": false, + "dutyCycle": false, + "ecoAllowed": true, + "ecoIgnored": false, + "externalClockCoolingTemperature": 23.0, + "externalClockEnabled": false, + "externalClockHeatingTemperature": 19.0, + "floorHeatingMode": "FLOOR_HEATING_STANDARD", + "homeId": "00000000-0000-0000-0000-000000000001", + "humidity": 43, + "humidityLimitEnabled": true, + "humidityLimitValue": 60, + "id": "00000000-0000-0000-0000-000000000012", + "label": "Schlafzimmer", + "lastSetPointReachedTimestamp": 1557767559939, + "lastSetPointUpdatedTimestamp": 1557767559939, + "lastStatusUpdate": 1524516534382, + "lowBat": false, + "maxTemperature": 30.0, + "metaGroupId": "00000000-0000-0000-0000-000000000011", + "minTemperature": 5.0, + "partyMode": false, + "profiles": { + "PROFILE_1": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000012", + "index": "PROFILE_1", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000023", + "visible": true + }, + "PROFILE_2": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000012", + "index": "PROFILE_2", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000024", + "visible": true + }, + "PROFILE_3": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000012", + "index": "PROFILE_3", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000025", + "visible": false + }, + "PROFILE_4": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000012", + "index": "PROFILE_4", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000026", + "visible": true + }, + "PROFILE_5": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000012", + "index": "PROFILE_5", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000027", + "visible": true + }, + "PROFILE_6": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000012", + "index": "PROFILE_6", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000028", + "visible": false + } + }, + "setPointTemperature": 5.0, + "type": "HEATING", + "unreach": false, + "valvePosition": 0.0, + "valveSilentModeEnabled": false, + "valveSilentModeSupported": false, + "heatingFailureSupported": true, + "windowOpenTemperature": 5.0, + "windowState": "OPEN" + }, + "00000000-0000-0000-0000-000000000016": { + "active": false, + "channels": [ + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000021" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000020" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000007" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000007" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000019" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000018" + } + ], + "configPending": false, + "dutyCycle": false, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000016", + "ignorableDevices": [], + "label": "INTERNAL", + "lastStatusUpdate": 1524515489257, + "lowBat": false, + "metaGroupId": null, + "motionDetected": null, + "presenceDetected": null, + "sabotage": false, + "silent": true, + "type": "SECURITY_ZONE", + "unreach": false, + "windowState": "CLOSED", + "zoneAssignmentIndex": "ALARM_MODE_ZONE_3" + }, + "00000000-0000-0000-0000-000000000017": { + "channels": [ + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000008" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000009" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000010" + } + ], + "configPending": false, + "dutyCycle": false, + "groups": [ + "00000000-0000-0000-0000-000000000018" + ], + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000017", + "incorrectPositioned": null, + "label": "Strom", + "lastStatusUpdate": 1524516554056, + "lowBat": null, + "metaGroupId": null, + "sabotage": null, + "type": "META", + "unreach": false + }, + "00000000-0000-0000-0000-000000000029": { + "channels": [], + "dutyCycle": null, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000029", + "label": "HEATING_TEMPERATURE_LIMITER", + "lastStatusUpdate": 0, + "lowBat": null, + "metaGroupId": null, + "type": "HEATING_TEMPERATURE_LIMITER", + "unreach": null + }, + "00000000-0000-0000-0000-000000000030": { + "boilerFollowUpTime": 0, + "boilerLeadTime": 0, + "channels": [], + "dutyCycle": null, + "heatDemand": null, + "heatDemandRuleEnabled": false, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000030", + "label": "HEATING_COOLING_DEMAND_BOILER", + "lastStatusUpdate": 0, + "lowBat": null, + "metaGroupId": null, + "on": null, + "triggered": false, + "type": "HEATING_COOLING_DEMAND_BOILER", + "unreach": null + }, + "00000000-0000-0000-0000-000000000010": { + "activeProfile": "PROFILE_1", + "actualTemperature": 24.5, + "boostDuration": 15, + "boostMode": false, + "channels": [ + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000001" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000023" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000012" + } + ], + "controlMode": "AUTOMATIC", + "controllable": true, + "cooling": false, + "coolingAllowed": false, + "coolingIgnored": false, + "dutyCycle": false, + "ecoAllowed": true, + "ecoIgnored": false, + "externalClockCoolingTemperature": 23.0, + "externalClockEnabled": false, + "externalClockHeatingTemperature": 19.0, + "floorHeatingMode": "FLOOR_HEATING_STANDARD", + "homeId": "00000000-0000-0000-0000-000000000001", + "humidity": 46, + "humidityLimitEnabled": true, + "humidityLimitValue": 60, + "id": "00000000-0000-0000-0000-000000000010", + "label": "Büro", + "lastSetPointReachedTimestamp": 1557767559939, + "lastSetPointUpdatedTimestamp": 1557767559939, + "lastStatusUpdate": 1524516454116, + "lowBat": false, + "maxTemperature": 30.0, + "metaGroupId": "00000000-0000-0000-0000-000000000008", + "minTemperature": 5.0, + "partyMode": false, + "profiles": { + "PROFILE_1": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000010", + "index": "PROFILE_1", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000031", + "visible": true + }, + "PROFILE_2": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000010", + "index": "PROFILE_2", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000032", + "visible": true + }, + "PROFILE_3": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000010", + "index": "PROFILE_3", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000033", + "visible": false + }, + "PROFILE_4": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000010", + "index": "PROFILE_4", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000034", + "visible": true + }, + "PROFILE_5": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000010", + "index": "PROFILE_5", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000035", + "visible": true + }, + "PROFILE_6": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000010", + "index": "PROFILE_6", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000036", + "visible": false + } + }, + "setPointTemperature": 19.0, + "type": "HEATING", + "unreach": false, + "valvePosition": 0.0, + "valveSilentModeEnabled": false, + "valveSilentModeSupported": false, + "heatingFailureSupported": true, + "windowOpenTemperature": 5.0, + "windowState": "CLOSED" + }, + "00000000-0000-0000-0000-000000000018": { + "channels": [ + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000010" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000009" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000008" + } + ], + "dimLevel": null, + "dutyCycle": false, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000018", + "label": "Strom", + "lastStatusUpdate": 1524516554056, + "lowBat": null, + "metaGroupId": "00000000-0000-0000-0000-000000000017", + "on": true, + "processing": null, + "shutterLevel": null, + "slatsLevel": null, + "type": "SWITCHING", + "unreach": false + }, + "00000000-0000-0000-0000-000000000009": { + "channels": [ + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000001" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000019" + } + ], + "dutyCycle": false, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000009", + "label": "Büro", + "lastStatusUpdate": 1524515854304, + "lowBat": false, + "metaGroupId": "00000000-0000-0000-0000-000000000008", + "motionDetected": null, + "presenceDetected": null, + "moistureDetected": null, + "waterlevelDetected": null, + "powerMainsFailure": null, + "sabotage": false, + "smokeDetectorAlarmType": "IDLE_OFF", + "type": "SECURITY", + "unreach": false, + "windowState": "CLOSED" + }, + "00000000-0000-0000-0000-000000000013": { + "channels": [ + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000004" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000020" + } + ], + "dutyCycle": false, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000013", + "label": "Schlafzimmer", + "lastStatusUpdate": 1524512404032, + "lowBat": false, + "metaGroupId": "00000000-0000-0000-0000-000000000011", + "motionDetected": null, + "presenceDetected": null, + "moistureDetected": null, + "waterlevelDetected": null, + "powerMainsFailure": null, + "sabotage": false, + "smokeDetectorAlarmType": "IDLE_OFF", + "type": "SECURITY", + "unreach": false, + "windowState": "OPEN" + }, + "00000000-0000-0000-0000-000000000005": { + "active": false, + "channels": [ + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000001" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000002" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000001" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000002" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000003" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000006" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000006" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000003" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000005" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000000" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000004" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000005" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000000" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000004" + } + ], + "configPending": false, + "dutyCycle": false, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000005", + "ignorableDevices": [], + "label": "EXTERNAL", + "lastStatusUpdate": 1524516526498, + "lowBat": false, + "metaGroupId": null, + "motionDetected": null, + "presenceDetected": null, + "sabotage": false, + "silent": true, + "type": "SECURITY_ZONE", + "unreach": false, + "windowState": "OPEN", + "zoneAssignmentIndex": "ALARM_MODE_ZONE_2" + }, + "00000000-0000-0000-0000-000000000022": { + "acousticFeedbackEnabled": true, + "channels": [ + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000020" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000018" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000021" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000019" + } + ], + "dimLevel": null, + "dutyCycle": false, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000022", + "label": "SIREN", + "lastStatusUpdate": 1524480981494, + "lowBat": false, + "metaGroupId": null, + "on": false, + "onTime": 180.0, + "signalAcoustic": "FREQUENCY_RISING", + "signalOptical": "DOUBLE_FLASHING_REPEATING", + "smokeDetectorAlarmType": "IDLE_OFF", + "type": "ALARM_SWITCHING", + "unreach": false + }, + "00000000-0000-0000-0000-000000000037": { + "channels": [], + "dimLevel": null, + "dutyCycle": null, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000037", + "label": "COMING_HOME", + "lastStatusUpdate": 0, + "lowBat": null, + "metaGroupId": null, + "on": null, + "type": "LINKED_SWITCHING", + "unreach": null + }, + "00000000-0000-0000-0000-000000000021": { + "activeProfile": "PROFILE_1", + "actualTemperature": 23.8, + "boostDuration": 15, + "boostMode": false, + "channels": [ + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000025" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000016" + } + ], + "controlMode": "AUTOMATIC", + "controllable": true, + "cooling": false, + "coolingAllowed": false, + "coolingIgnored": false, + "dutyCycle": false, + "ecoAllowed": true, + "ecoIgnored": false, + "externalClockCoolingTemperature": 23.0, + "externalClockEnabled": false, + "externalClockHeatingTemperature": 19.0, + "floorHeatingMode": "FLOOR_HEATING_STANDARD", + "homeId": "00000000-0000-0000-0000-000000000001", + "humidity": 47, + "humidityLimitEnabled": true, + "humidityLimitValue": 60, + "id": "00000000-0000-0000-0000-000000000021", + "label": "Badezimmer", + "lastSetPointReachedTimestamp": 1557767559939, + "lastSetPointUpdatedTimestamp": 1557767559939, + "lastStatusUpdate": 1524516556479, + "lowBat": false, + "maxTemperature": 30.0, + "metaGroupId": "00000000-0000-0000-0000-000000000020", + "minTemperature": 5.0, + "partyMode": false, + "profiles": { + "PROFILE_1": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000021", + "index": "PROFILE_1", + "name": "STD", + "profileId": "00000000-0000-0000-0000-000000000038", + "visible": true + }, + "PROFILE_2": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000021", + "index": "PROFILE_2", + "name": "Winter", + "profileId": "00000000-0000-0000-0000-000000000039", + "visible": true + }, + "PROFILE_3": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000021", + "index": "PROFILE_3", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000040", + "visible": false + }, + "PROFILE_4": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000021", + "index": "PROFILE_4", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000041", + "visible": true + }, + "PROFILE_5": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000021", + "index": "PROFILE_5", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000042", + "visible": false + }, + "PROFILE_6": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000021", + "index": "PROFILE_6", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000043", + "visible": false + } + }, + "setPointTemperature": 5.0, + "type": "HEATING", + "unreach": false, + "valvePosition": 0.0, + "valveSilentModeEnabled": false, + "valveSilentModeSupported": false, + "heatingFailureSupported": true, + "windowOpenTemperature": 5.0, + "windowState": null + }, + "00000000-0000-0000-0000-000000000006": { + "channels": [ + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000005" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000002" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000000" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000018" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000003" + } + ], + "dutyCycle": false, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000006", + "label": "Wohnzimmer", + "lastStatusUpdate": 1524516526498, + "lowBat": false, + "metaGroupId": "00000000-0000-0000-0000-000000000004", + "motionDetected": null, + "presenceDetected": null, + "moistureDetected": null, + "waterlevelDetected": null, + "powerMainsFailure": null, + "sabotage": false, + "smokeDetectorAlarmType": "IDLE_OFF", + "type": "SECURITY", + "unreach": false, + "windowState": "OPEN" + }, + "00000000-0000-0000-0000-000000000044": { + "channels": [], + "dutyCycle": null, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000044", + "label": "INBOX", + "lastStatusUpdate": 0, + "lowBat": null, + "metaGroupId": null, + "type": "INBOX", + "unreach": null + }, + "00000000-0000-0000-0000-000000000045": { + "channels": [], + "dutyCycle": null, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000045", + "label": "HEATING_HUMIDITY_LIMITER", + "lastStatusUpdate": 0, + "lowBat": null, + "metaGroupId": null, + "type": "HEATING_HUMIDITY_LIMITER", + "unreach": null + }, + "00000000-0000-0000-0000-000000000008": { + "channels": [ + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000001" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000012" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000023" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000019" + } + ], + "configPending": false, + "dutyCycle": false, + "groups": [ + "00000000-0000-0000-0000-000000000010", + "00000000-0000-0000-0000-000000000009" + ], + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000008", + "incorrectPositioned": null, + "label": "Büro", + "lastStatusUpdate": 1524516454116, + "lowBat": false, + "metaGroupId": null, + "sabotage": false, + "type": "META", + "unreach": false + }, + "00000000-0000-0000-0000-000000000011": { + "channels": [ + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000022" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000004" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000020" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000011" + } + ], + "configPending": false, + "dutyCycle": false, + "groups": [ + "00000000-0000-0000-0000-000000000012", + "00000000-0000-0000-0000-000000000013" + ], + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000011", + "incorrectPositioned": null, + "label": "Schlafzimmer", + "lastStatusUpdate": 1524516534382, + "lowBat": false, + "metaGroupId": null, + "sabotage": false, + "type": "META", + "unreach": false + }, + "00000000-0000-0000-0000-000000000046": { + "channels": [], + "dutyCycle": null, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000046", + "label": "HEATING_CHANGEOVER", + "lastStatusUpdate": 0, + "lowBat": null, + "metaGroupId": null, + "on": null, + "type": "HEATING_CHANGEOVER", + "unreach": null + }, + "00000000-0000-0000-0000-000000000014": { + "channels": [ + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000021" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000007" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000006" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000013" + } + ], + "configPending": false, + "dutyCycle": false, + "groups": [ + "00000000-0000-0000-0000-000000000015", + "00000000-0000-0000-0000-000000000019" + ], + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000014", + "incorrectPositioned": null, + "label": "Vorzimmer", + "lastStatusUpdate": 1524516489316, + "lowBat": false, + "metaGroupId": null, + "sabotage": false, + "type": "META", + "unreach": false + }, + "00000000-0000-0000-0000-000000000007": { + "activeProfile": "PROFILE_1", + "actualTemperature": 23.6, + "boostDuration": 15, + "boostMode": false, + "channels": [ + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000005" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000002" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000000" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000024" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000017" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000015" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000014" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000003" + } + ], + "controlMode": "AUTOMATIC", + "controllable": true, + "cooling": false, + "coolingAllowed": false, + "coolingIgnored": false, + "dutyCycle": false, + "ecoAllowed": true, + "ecoIgnored": false, + "externalClockCoolingTemperature": 23.0, + "externalClockEnabled": false, + "externalClockHeatingTemperature": 19.0, + "floorHeatingMode": "FLOOR_HEATING_STANDARD", + "homeId": "00000000-0000-0000-0000-000000000001", + "humidity": 45, + "humidityLimitEnabled": true, + "humidityLimitValue": 60, + "id": "00000000-0000-0000-0000-000000000007", + "label": "Wohnzimmer", + "lastSetPointReachedTimestamp": 1557767559939, + "lastSetPointUpdatedTimestamp": 1557767559939, + "lastStatusUpdate": 1524516526498, + "lowBat": false, + "maxTemperature": 30.0, + "metaGroupId": "00000000-0000-0000-0000-000000000004", + "minTemperature": 5.0, + "partyMode": false, + "profiles": { + "PROFILE_1": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000007", + "index": "PROFILE_1", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000047", + "visible": true + }, + "PROFILE_2": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000007", + "index": "PROFILE_2", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000048", + "visible": true + }, + "PROFILE_3": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000007", + "index": "PROFILE_3", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000049", + "visible": false + }, + "PROFILE_4": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000007", + "index": "PROFILE_4", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000050", + "visible": true + }, + "PROFILE_5": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000007", + "index": "PROFILE_5", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000051", + "visible": true + }, + "PROFILE_6": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000007", + "index": "PROFILE_6", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000052", + "visible": false + } + }, + "setPointTemperature": 5.0, + "type": "HEATING", + "unreach": false, + "valvePosition": 0.0, + "valveSilentModeEnabled": false, + "valveSilentModeSupported": false, + "heatingFailureSupported": true, + "windowOpenTemperature": 5.0, + "windowState": "OPEN" + }, + "00000000-0000-0000-0000-000000000053": { + "channels": [], + "dimLevel": null, + "dutyCycle": null, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000053", + "label": "PANIC", + "lastStatusUpdate": 0, + "lowBat": null, + "metaGroupId": null, + "on": null, + "type": "LINKED_SWITCHING", + "unreach": null + }, + "00000000-0000-0000-0000-000000000054": { + "channels": [], + "dutyCycle": null, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000054", + "label": "HEATING_EXTERNAL_CLOCK", + "lastStatusUpdate": 0, + "lowBat": null, + "metaGroupId": null, + "type": "HEATING_EXTERNAL_CLOCK", + "unreach": null + }, + "00000000-0000-0000-0000-000000000055": { + "channels": [], + "dutyCycle": null, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000055", + "label": "HEATING_DEHUMIDIFIER", + "lastStatusUpdate": 0, + "lowBat": null, + "metaGroupId": null, + "on": null, + "type": "HEATING_DEHUMIDIFIER", + "unreach": null + }, + "00000000-0000-0000-0000-000000000056": { + "acousticFeedbackEnabled": true, + "channels": [], + "dimLevel": null, + "dutyCycle": null, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000056", + "label": "ALARM", + "lastStatusUpdate": 0, + "lowBat": null, + "metaGroupId": null, + "on": null, + "onTime": 7200.0, + "signalAcoustic": "FREQUENCY_RISING", + "signalOptical": "DOUBLE_FLASHING_REPEATING", + "smokeDetectorAlarmType": null, + "type": "ALARM_SWITCHING", + "unreach": null + }, + "00000000-0000-0000-0000-000000000057": { + "channels": [], + "dutyCycle": null, + "heatDemand": null, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000057", + "label": "HEATING_COOLING_DEMAND_PUMP", + "lastStatusUpdate": 0, + "lowBat": null, + "metaGroupId": null, + "on": null, + "pumpFollowUpTime": 2, + "pumpLeadTime": 2, + "pumpProtectionDuration": 1, + "pumpProtectionSwitchingInterval": 14, + "type": "HEATING_COOLING_DEMAND_PUMP", + "unreach": null + }, + "00000000-0000-0000-0000-000000000015": { + "channels": [ + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000007" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000006" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000021" + } + ], + "dutyCycle": false, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000015", + "label": "Vorzimmer", + "lastStatusUpdate": 1524516489316, + "lowBat": false, + "metaGroupId": "00000000-0000-0000-0000-000000000014", + "motionDetected": null, + "presenceDetected": null, + "moistureDetected": null, + "waterlevelDetected": null, + "powerMainsFailure": null, + "sabotage": false, + "smokeDetectorAlarmType": "IDLE_OFF", + "type": "SECURITY", + "unreach": false, + "windowState": "CLOSED" + }, + "00000000-0000-0000-0000-000000000004": { + "channels": [ + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000024" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000005" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000002" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000000" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000014" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000003" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000017" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000015" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000018" + } + ], + "configPending": false, + "dutyCycle": false, + "groups": [ + "00000000-0000-0000-0000-000000000006", + "00000000-0000-0000-0000-000000000007" + ], + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000004", + "incorrectPositioned": null, + "label": "Wohnzimmer", + "lastStatusUpdate": 1524516526498, + "lowBat": false, + "metaGroupId": null, + "sabotage": false, + "type": "META", + "unreach": false + }, + "00000000-0000-0000-0000-000000000019": { + "activeProfile": "PROFILE_1", + "actualTemperature": null, + "boostDuration": 15, + "boostMode": false, + "channels": [ + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000013" + } + ], + "controlMode": "AUTOMATIC", + "controllable": true, + "cooling": null, + "coolingAllowed": false, + "coolingIgnored": false, + "dutyCycle": false, + "ecoAllowed": true, + "ecoIgnored": false, + "externalClockCoolingTemperature": 23.0, + "externalClockEnabled": false, + "externalClockHeatingTemperature": 19.0, + "floorHeatingMode": "FLOOR_HEATING_STANDARD", + "homeId": "00000000-0000-0000-0000-000000000001", + "humidity": null, + "humidityLimitEnabled": true, + "humidityLimitValue": 60, + "id": "00000000-0000-0000-0000-000000000019", + "label": "Vorzimmer", + "lastSetPointReachedTimestamp": 1557767559939, + "lastSetPointUpdatedTimestamp": 1557767559939, + "lastStatusUpdate": 1524514007132, + "lowBat": false, + "maxTemperature": 30.0, + "metaGroupId": "00000000-0000-0000-0000-000000000014", + "minTemperature": 5.0, + "partyMode": false, + "profiles": { + "PROFILE_1": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000019", + "index": "PROFILE_1", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000058", + "visible": true + }, + "PROFILE_2": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000019", + "index": "PROFILE_2", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000059", + "visible": false + }, + "PROFILE_3": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000019", + "index": "PROFILE_3", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000060", + "visible": false + }, + "PROFILE_4": { + "enabled": false, + "groupId": "00000000-0000-0000-0000-000000000019", + "index": "PROFILE_4", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000061", + "visible": true + }, + "PROFILE_5": { + "enabled": false, + "groupId": "00000000-0000-0000-0000-000000000019", + "index": "PROFILE_5", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000062", + "visible": false + }, + "PROFILE_6": { + "enabled": false, + "groupId": "00000000-0000-0000-0000-000000000019", + "index": "PROFILE_6", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000063", + "visible": false + } + }, + "setPointTemperature": 5.0, + "type": "HEATING", + "unreach": false, + "valvePosition": 0.0, + "valveSilentModeEnabled": false, + "valveSilentModeSupported": false, + "heatingFailureSupported": true, + "windowOpenTemperature": 5.0, + "windowState": null + }, + "00000000-AAAA-0000-0000-000000000001": { + "actualTemperature": 15.4, + "channels": [ + { + "channelIndex": 1, + "deviceId": "3014F711AAAA000000000003" + }, + { + "channelIndex": 1, + "deviceId": "3014F711AAAA000000000002" + }, + { + "channelIndex": 1, + "deviceId": "3014F711AAAA000000000001" + } + ], + "homeId": "00000000-0000-0000-0000-000000000001", + "humidity": 65, + "id": "00000000-AAAA-0000-0000-000000000001", + "illumination": 4703.0, + "label": "Terrasse", + "lastStatusUpdate": 1520770214834, + "lowBat": false, + "metaGroupId": "76df95a5-afa5-45ee-b817-f724ffaf04a1", + "raining": false, + "type": "ENVIRONMENT", + "unreach": false, + "windSpeed": 29.1 + }, + "00000000-BBBB-0000-0000-000000000052": { + "channels": [], + "checkInterval": 600, + "dutyCycle": null, + "enabled": true, + "heatingFailureValidationResult": "NO_HEATING_FAILURE", + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-BBBB-0000-0000-000000000052", + "label": "HEATING_FAILURE_ALERT_RULE_GROUP", + "lastExecutionTimestamp": 1550773800084, + "lastStatusUpdate": 0, + "lowBat": null, + "metaGroupId": null, + "triggered": false, + "type": "HEATING_FAILURE_ALERT_RULE_GROUP", + "unreach": null, + "validationTimeout": 86400000 + }, + "00000000-AAAA-0000-0000-000000000068": { + "acousticFeedbackEnabled": true, + "channels": [], + "dimLevel": null, + "dutyCycle": null, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-AAAA-0000-0000-000000000068", + "label": "BACKUP_ALARM_SIREN", + "lastStatusUpdate": 0, + "lowBat": null, + "metaGroupId": null, + "on": null, + "onTime": 180.0, + "signalAcoustic": "FREQUENCY_RISING", + "signalOptical": "DISABLE_OPTICAL_SIGNAL", + "smokeDetectorAlarmType": null, + "type": "SECURITY_BACKUP_ALARM_SWITCHING", + "unreach": null + }, + "00000000-0000-0000-AAAA-000000000029": { + "channels": [ + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000023" + } + ], + "dutyCycle": false, + "enabled": true, + "homeId": "00000000-0000-0000-0000-000000000001", + "humidityLowerThreshold": 40, + "humidityUpperThreshold": 60, + "humidityValidationResult": "LESSER_LOWER_THRESHOLD", + "id": "00000000-0000-0000-AAAA-000000000029", + "label": "B\u00fcro", + "lastExecutionTimestamp": 1551387905665, + "lastStatusUpdate": 1551388104260, + "lowBat": false, + "metaGroupId": "00000000-0000-0000-0000-000000000008", + "outdoorClimateSensor": null, + "triggered": false, + "type": "HUMIDITY_WARNING_RULE_GROUP", + "unreach": false, + "ventilationRecommended": true + }, + "00000000-0000-0000-0000-000000000049": { + "channels": [ + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000038" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000023" + } + ], + "dutyCycle": false, + "enabled": true, + "homeId": "00000000-0000-0000-0000-000000000001", + "humidityLowerThreshold": 30, + "humidityUpperThreshold": 60, + "humidityValidationResult": null, + "id": "00000000-0000-0000-0000-000000000049", + "label": "Schlafzimmer", + "lastExecutionTimestamp": 0, + "lastStatusUpdate": 1551003370150, + "lowBat": false, + "metaGroupId": "00000000-0000-0000-0000-000000000008", + "outdoorClimateSensor": { + "channelIndex": 1, + "deviceId": "3014F7110000000000000038" + }, + "triggered": false, + "type": "HUMIDITY_WARNING_RULE_GROUP", + "unreach": false, + "ventilationRecommended": false + } + }, + "home": { + "apExchangeClientId": null, + "apExchangeState": "NONE", + "availableAPVersion": null, + "carrierSense": null, + "clients": [ + "00000000-0000-0000-0000-000000000000" + ], + "connected": true, + "currentAPVersion": "1.2.4", + "deviceUpdateStrategy": "AUTOMATICALLY_IF_POSSIBLE", + "dutyCycle": 8.0, + "functionalHomes": { + "INDOOR_CLIMATE": { + "absenceEndTime": null, + "absenceType": "NOT_ABSENT", + "active": true, + "coolingEnabled": false, + "ecoDuration": "PERMANENT", + "ecoTemperature": 17.0, + "floorHeatingSpecificGroups": { + "HEATING_CHANGEOVER": "00000000-0000-0000-0000-000000000046", + "HEATING_COOLING_DEMAND_BOILER": "00000000-0000-0000-0000-000000000030", + "HEATING_COOLING_DEMAND_PUMP": "00000000-0000-0000-0000-000000000057", + "HEATING_DEHUMIDIFIER": "00000000-0000-0000-0000-000000000055", + "HEATING_EXTERNAL_CLOCK": "00000000-0000-0000-0000-000000000054", + "HEATING_HUMIDITY_LIMITER": "00000000-0000-0000-0000-000000000045", + "HEATING_TEMPERATURE_LIMITER": "00000000-0000-0000-0000-000000000029" + }, + "functionalGroups": [ + "00000000-0000-0000-0000-000000000012", + "00000000-0000-0000-0000-000000000007", + "00000000-0000-0000-0000-000000000019", + "00000000-0000-0000-0000-000000000010", + "00000000-0000-0000-0000-000000000021" + ], + "optimumStartStopEnabled": false, + "solution": "INDOOR_CLIMATE" + }, + "LIGHT_AND_SHADOW": { + "active": true, + "extendedLinkedShutterGroups": [], + "extendedLinkedSwitchingGroups": [], + "functionalGroups": [ + "00000000-0000-0000-0000-000000000018" + ], + "shutterProfileGroups": [], + "solution": "LIGHT_AND_SHADOW", + "switchingProfileGroups": [] + }, + "SECURITY_AND_ALARM": { + "activationInProgress": false, + "active": true, + "alarmActive": false, + "alarmEventDeviceId": "3014F7110000000000000007", + "alarmEventTimestamp": 1524504122047, + "alarmSecurityJournalEntryType": "SENSOR_EVENT", + "functionalGroups": [ + "00000000-0000-0000-0000-000000000013", + "00000000-0000-0000-0000-000000000006", + "00000000-0000-0000-0000-000000000015", + "00000000-0000-0000-0000-000000000009" + ], + "intrusionAlertThroughSmokeDetectors": false, + "securitySwitchingGroups": { + "ALARM": "00000000-0000-0000-0000-000000000056", + "BACKUP_ALARM_SIREN": "00000000-AAAA-0000-0000-000000000068", + "COMING_HOME": "00000000-0000-0000-0000-000000000037", + "PANIC": "00000000-0000-0000-0000-000000000053", + "SIREN": "00000000-0000-0000-0000-000000000022" + }, + "securityZoneActivationMode": "ACTIVATION_WITH_DEVICE_IGNORELIST", + "securityZones": { + "EXTERNAL": "00000000-0000-0000-0000-000000000005", + "INTERNAL": "00000000-0000-0000-0000-000000000016" + }, + "solution": "SECURITY_AND_ALARM", + "zoneActivationDelay": 0.0 + }, + "WEATHER_AND_ENVIRONMENT": { + "active": true, + "functionalGroups": [ + "00000000-AAAA-0000-0000-000000000001" + ], + "solution": "WEATHER_AND_ENVIRONMENT" + } + }, + "id": "00000000-0000-0000-0000-000000000001", + "inboxGroup": "00000000-0000-0000-0000-000000000044", + "lastReadyForUpdateTimestamp": 1522319489138, + "location": { + "city": "1010 Wien, Österreich", + "latitude": "48.208088", + "longitude": "16.358608" + }, + "metaGroups": [ + "00000000-0000-0000-0000-000000000011", + "00000000-0000-0000-0000-000000000008", + "00000000-0000-0000-0000-000000000014", + "00000000-0000-0000-0000-000000000004", + "00000000-0000-0000-0000-000000000017", + "00000000-0000-0000-0000-000000000020" + ], + "pinAssigned": false, + "powerMeterCurrency": "EUR", + "powerMeterUnitPrice": 0.0, + "ruleGroups": [ + "00000000-0000-0000-0000-000000000057", + "00000000-0000-0000-0000-000000000030" + ], + "ruleMetaDatas": { + "00000000-0000-0000-0000-000000000065": { + "active": true, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000065", + "label": "Alarmanlage", + "ruleErrorCategories": [], + "type": "SIMPLE" + } + }, + "timeZoneId": "Europe/Vienna", + "updateState": "UP_TO_DATE", + "voiceControlSettings": { + "allowedActiveSecurityZoneIds": [] + }, + "weather": { + "humidity": 54, + "maxTemperature": 16.6, + "minTemperature": 16.6, + "temperature": 16.6, + "vaporAmount": 5.465858858389302, + "weatherCondition": "LIGHT_CLOUDY", + "weatherDayTime": "NIGHT", + "windDirection": 294, + "windSpeed": 8.568 + } + } +} diff --git a/tests/fixtures/yandex_transport_reply.json b/tests/fixtures/yandex_transport_reply.json index c5e4857297a..3189d7a9d9b 100644 --- a/tests/fixtures/yandex_transport_reply.json +++ b/tests/fixtures/yandex_transport_reply.json @@ -1,2106 +1,1560 @@ { - "data": { - "geometries": [ - { - "type": "Point", - "coordinates": [ - 37.565280044, - 55.851959656 - ] - } - ], - "geometry": { - "type": "Point", - "coordinates": [ - 37.565280044, - 55.851959656 + "data": { + "geometries": [ + { + "type": "Point", + "coordinates": [ + 37.565280044, + 55.851959656 + ] + } + ], + "geometry": { + "type": "Point", + "coordinates": [ + 37.565280044, + 55.851959656 + ] + }, + "properties": { + "name": "7-й автобусный парк", + "description": "7-й автобусный парк", + "currentTime": 1570971868567, + "tzOffset": 10800, + "StopMetaData": { + "id": "stop__9639579", + "name": "7-й автобусный парк", + "type": "urban", + "region": { + "id": 213, + "type": 6, + "parent_id": 1, + "capital_id": 0, + "geo_parent_id": 0, + "city_id": 213, + "name": "moscow", + "native_name": "", + "iso_name": "RU MOW", + "is_main": true, + "en_name": "Moscow", + "short_en_name": "MSK", + "phone_code": "495 499", + "phone_code_old": "095", + "zip_code": "", + "population": 12506468, + "synonyms": "Moskau, Moskva", + "latitude": 55.753215, + "longitude": 37.622504, + "latitude_size": 0.878654, + "longitude_size": 1.164423, + "zoom": 10, + "tzname": "Europe/Moscow", + "official_languages": "ru", + "widespread_languages": "ru", + "suggest_list": [], + "is_eu": false, + "services_names": [ + "bs", + "yaca", + "weather", + "afisha", + "maps", + "tv", + "ad", + "etrain", + "subway", + "delivery", + "route" + ], + "ename": "moscow", + "bounds": [ + [ + 37.0402925, + 55.31141404514547 + ], + [ + 38.2047155, + 56.190068045145466 ] - }, - "properties": { - "name": "7-й автобусный парк", - "description": "7-й автобусный парк", - "currentTime": "Mon Sep 16 2019 21:40:40 GMT+0300 (Moscow Standard Time)", - "StopMetaData": { - "id": "stop__9639579", - "name": "7-й автобусный парк", - "type": "urban", - "region": { - "id": 213, - "type": 6, - "parent_id": 1, - "capital_id": 0, - "geo_parent_id": 0, - "city_id": 213, - "name": "moscow", - "native_name": "", - "iso_name": "RU MOW", - "is_main": true, - "en_name": "Moscow", - "short_en_name": "MSK", - "phone_code": "495 499", - "phone_code_old": "095", - "zip_code": "", - "population": 12506468, - "synonyms": "Moskau, Moskva", - "latitude": 55.753215, - "longitude": 37.622504, - "latitude_size": 0.878654, - "longitude_size": 1.164423, - "zoom": 10, - "tzname": "Europe/Moscow", - "official_languages": "ru", - "widespread_languages": "ru", - "suggest_list": [], - "is_eu": false, - "services_names": [ - "bs", - "yaca", - "weather", - "afisha", - "maps", - "tv", - "ad", - "etrain", - "subway", - "delivery", - "route" - ], - "ename": "moscow", - "bounds": [ - [ - 37.0402925, - 55.31141404514547 - ], - [ - 38.2047155, - 56.190068045145466 - ] - ], - "names": { - "ablative": "", - "accusative": "Москву", - "dative": "Москве", - "directional": "", - "genitive": "Москвы", - "instrumental": "Москвой", - "locative": "", - "nominative": "Москва", - "preposition": "в", - "prepositional": "Москве" - }, - "parent": { - "id": 1, - "type": 5, - "parent_id": 3, - "capital_id": 213, - "geo_parent_id": 0, - "city_id": 213, - "name": "moscow-and-moscow-oblast", - "native_name": "", - "iso_name": "RU-MOS", - "is_main": true, - "en_name": "Moscow and Moscow Oblast", - "short_en_name": "RU-MOS", - "phone_code": "495 496 498 499", - "phone_code_old": "", - "zip_code": "", - "population": 7503385, - "synonyms": "Московская область, Подмосковье, Podmoskovye", - "latitude": 55.815792, - "longitude": 37.380031, - "latitude_size": 2.705659, - "longitude_size": 5.060749, - "zoom": 8, - "tzname": "Europe/Moscow", - "official_languages": "ru", - "widespread_languages": "ru", - "suggest_list": [ - 213, - 10716, - 10747, - 10758, - 20728, - 10740, - 10738, - 20523, - 10735, - 10734, - 10743, - 21622 - ], - "is_eu": false, - "services_names": [ - "bs", - "yaca", - "ad" - ], - "ename": "moscow-and-moscow-oblast", - "bounds": [ - [ - 34.8496565, - 54.439456064325434 - ], - [ - 39.9104055, - 57.14511506432543 - ] - ], - "names": { - "ablative": "", - "accusative": "Москву и Московскую область", - "dative": "Москве и Московской области", - "directional": "", - "genitive": "Москвы и Московской области", - "instrumental": "Москвой и Московской областью", - "locative": "", - "nominative": "Москва и Московская область", - "preposition": "в", - "prepositional": "Москве и Московской области" - }, - "parent": { - "id": 225, - "type": 3, - "parent_id": 10001, - "capital_id": 213, - "geo_parent_id": 0, - "city_id": 213, - "name": "russia", - "native_name": "", - "iso_name": "RU", - "is_main": false, - "en_name": "Russia", - "short_en_name": "RU", - "phone_code": "7", - "phone_code_old": "", - "zip_code": "", - "population": 146880432, - "synonyms": "Russian Federation,Российская Федерация", - "latitude": 61.698653, - "longitude": 99.505405, - "latitude_size": 40.700127, - "longitude_size": 171.643239, - "zoom": 3, - "tzname": "", - "official_languages": "ru", - "widespread_languages": "ru", - "suggest_list": [ - 213, - 2, - 65, - 54, - 47, - 43, - 66, - 51, - 56, - 172, - 39, - 62 - ], - "is_eu": false, - "services_names": [ - "bs", - "yaca", - "ad" - ], - "ename": "russia", - "bounds": [ - [ - 13.683785499999999, - 35.290400699917846 - ], - [ - -174.6729755, - 75.99052769991785 - ] - ], - "names": { - "ablative": "", - "accusative": "Россию", - "dative": "России", - "directional": "", - "genitive": "России", - "instrumental": "Россией", - "locative": "", - "nominative": "Россия", - "preposition": "в", - "prepositional": "России" - } - } - } - }, - "Transport": [ - { - "lineId": "2036925416", - "name": "194", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "2036927196", - "EssentialStops": [ - { - "id": "stop__9711780", - "name": "Метро Петровско-Разумовская" - }, - { - "id": "stop__9648742", - "name": "Коровино" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659860", - "tzOffset": 10800, - "text": "21:51" - } - }, - { - "Scheduled": { - "value": "1568660760", - "tzOffset": 10800, - "text": "22:06" - } - }, - { - "Scheduled": { - "value": "1568661840", - "tzOffset": 10800, - "text": "22:24" - } - } - ], - "departureTime": "21:51" - } - } - ], - "threadId": "2036927196", - "EssentialStops": [ - { - "id": "stop__9711780", - "name": "Метро Петровско-Разумовская" - }, - { - "id": "stop__9648742", - "name": "Коровино" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659860", - "tzOffset": 10800, - "text": "21:51" - } - }, - { - "Scheduled": { - "value": "1568660760", - "tzOffset": 10800, - "text": "22:06" - } - }, - { - "Scheduled": { - "value": "1568661840", - "tzOffset": 10800, - "text": "22:24" - } - } - ], - "departureTime": "21:51" - } - }, - { - "lineId": "213_114_bus_mosgortrans", - "name": "114", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213B_114_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9647199", - "name": "Метро Войковская" - }, - { - "id": "stop__9639588", - "name": "Коровинское шоссе" - } - ], - "BriefSchedule": { - "Events": [], - "Frequency": { - "text": "15 мин", - "value": 900, - "begin": { - "value": "1568603405", - "tzOffset": 10800, - "text": "6:10" - }, - "end": { - "value": "1568672165", - "tzOffset": 10800, - "text": "1:16" - } - } - } - } - ], - "threadId": "213B_114_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9647199", - "name": "Метро Войковская" - }, - { - "id": "stop__9639588", - "name": "Коровинское шоссе" - } - ], - "BriefSchedule": { - "Events": [], - "Frequency": { - "text": "15 мин", - "value": 900, - "begin": { - "value": "1568603405", - "tzOffset": 10800, - "text": "6:10" - }, - "end": { - "value": "1568672165", - "tzOffset": 10800, - "text": "1:16" - } - } - } - }, - { - "lineId": "213_154_bus_mosgortrans", - "name": "154", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213B_154_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9642548", - "name": "ВДНХ (южная)" - }, - { - "id": "stop__9711744", - "name": "Станция Ховрино" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659260", - "tzOffset": 10800, - "text": "21:41" - }, - "Estimated": { - "value": "1568659252", - "tzOffset": 10800, - "text": "21:40" - }, - "vehicleId": "codd%5Fnew|1054764%5F191500" - }, - { - "Scheduled": { - "value": "1568660580", - "tzOffset": 10800, - "text": "22:03" - } - }, - { - "Scheduled": { - "value": "1568661900", - "tzOffset": 10800, - "text": "22:25" - } - } - ], - "departureTime": "21:41" - } - } - ], - "threadId": "213B_154_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9642548", - "name": "ВДНХ (южная)" - }, - { - "id": "stop__9711744", - "name": "Станция Ховрино" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659260", - "tzOffset": 10800, - "text": "21:41" - }, - "Estimated": { - "value": "1568659252", - "tzOffset": 10800, - "text": "21:40" - }, - "vehicleId": "codd%5Fnew|1054764%5F191500" - }, - { - "Scheduled": { - "value": "1568660580", - "tzOffset": 10800, - "text": "22:03" - } - }, - { - "Scheduled": { - "value": "1568661900", - "tzOffset": 10800, - "text": "22:25" - } - } - ], - "departureTime": "21:41" - } - }, - { - "lineId": "213_179_bus_mosgortrans", - "name": "179", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213B_179_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9647199", - "name": "Метро Войковская" - }, - { - "id": "stop__9639480", - "name": "Платформа Лианозово" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659920", - "tzOffset": 10800, - "text": "21:52" - }, - "Estimated": { - "value": "1568659351", - "tzOffset": 10800, - "text": "21:42" - }, - "vehicleId": "codd%5Fnew|59832%5F31359" - }, - { - "Scheduled": { - "value": "1568660760", - "tzOffset": 10800, - "text": "22:06" - } - }, - { - "Scheduled": { - "value": "1568661660", - "tzOffset": 10800, - "text": "22:21" - } - } - ], - "departureTime": "21:52" - } - } - ], - "threadId": "213B_179_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9647199", - "name": "Метро Войковская" - }, - { - "id": "stop__9639480", - "name": "Платформа Лианозово" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659920", - "tzOffset": 10800, - "text": "21:52" - }, - "Estimated": { - "value": "1568659351", - "tzOffset": 10800, - "text": "21:42" - }, - "vehicleId": "codd%5Fnew|59832%5F31359" - }, - { - "Scheduled": { - "value": "1568660760", - "tzOffset": 10800, - "text": "22:06" - } - }, - { - "Scheduled": { - "value": "1568661660", - "tzOffset": 10800, - "text": "22:21" - } - } - ], - "departureTime": "21:52" - } - }, - { - "lineId": "213_191m_minibus_default", - "name": "591", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213A_191m_minibus_default", - "EssentialStops": [ - { - "id": "stop__9647199", - "name": "Метро Войковская" - }, - { - "id": "stop__9711744", - "name": "Станция Ховрино" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568660525", - "tzOffset": 10800, - "text": "22:02" - }, - "vehicleId": "codd%5Fnew|38278%5F9345312" - } - ], - "Frequency": { - "text": "22 мин", - "value": 1320, - "begin": { - "value": "1568602033", - "tzOffset": 10800, - "text": "5:47" - }, - "end": { - "value": "1568672233", - "tzOffset": 10800, - "text": "1:17" - } - } - } - } - ], - "threadId": "213A_191m_minibus_default", - "EssentialStops": [ - { - "id": "stop__9647199", - "name": "Метро Войковская" - }, - { - "id": "stop__9711744", - "name": "Станция Ховрино" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568660525", - "tzOffset": 10800, - "text": "22:02" - }, - "vehicleId": "codd%5Fnew|38278%5F9345312" - } - ], - "Frequency": { - "text": "22 мин", - "value": 1320, - "begin": { - "value": "1568602033", - "tzOffset": 10800, - "text": "5:47" - }, - "end": { - "value": "1568672233", - "tzOffset": 10800, - "text": "1:17" - } - } - } - }, - { - "lineId": "213_206m_minibus_default", - "name": "206к", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213A_206m_minibus_default", - "EssentialStops": [ - { - "id": "stop__9640756", - "name": "Метро Петровско-Разумовская" - }, - { - "id": "stop__9640553", - "name": "Лобненская улица" - } - ], - "BriefSchedule": { - "Events": [], - "Frequency": { - "text": "22 мин", - "value": 1320, - "begin": { - "value": "1568601239", - "tzOffset": 10800, - "text": "5:33" - }, - "end": { - "value": "1568671439", - "tzOffset": 10800, - "text": "1:03" - } - } - } - } - ], - "threadId": "213A_206m_minibus_default", - "EssentialStops": [ - { - "id": "stop__9640756", - "name": "Метро Петровско-Разумовская" - }, - { - "id": "stop__9640553", - "name": "Лобненская улица" - } - ], - "BriefSchedule": { - "Events": [], - "Frequency": { - "text": "22 мин", - "value": 1320, - "begin": { - "value": "1568601239", - "tzOffset": 10800, - "text": "5:33" - }, - "end": { - "value": "1568671439", - "tzOffset": 10800, - "text": "1:03" - } - } - } - }, - { - "lineId": "213_215_bus_mosgortrans", - "name": "215", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213B_215_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9711780", - "name": "Метро Петровско-Разумовская" - }, - { - "id": "stop__9711744", - "name": "Станция Ховрино" - } - ], - "BriefSchedule": { - "Events": [], - "Frequency": { - "text": "27 мин", - "value": 1620, - "begin": { - "value": "1568601276", - "tzOffset": 10800, - "text": "5:34" - }, - "end": { - "value": "1568671476", - "tzOffset": 10800, - "text": "1:04" - } - } - } - } - ], - "threadId": "213B_215_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9711780", - "name": "Метро Петровско-Разумовская" - }, - { - "id": "stop__9711744", - "name": "Станция Ховрино" - } - ], - "BriefSchedule": { - "Events": [], - "Frequency": { - "text": "27 мин", - "value": 1620, - "begin": { - "value": "1568601276", - "tzOffset": 10800, - "text": "5:34" - }, - "end": { - "value": "1568671476", - "tzOffset": 10800, - "text": "1:04" - } - } - } - }, - { - "lineId": "213_282_bus_mosgortrans", - "name": "282", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213A_282_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9641102", - "name": "Улица Корнейчука" - }, - { - "id": "2532226085", - "name": "Метро Войковская" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568659888", - "tzOffset": 10800, - "text": "21:51" - }, - "vehicleId": "codd%5Fnew|34874%5F9345408" - } - ], - "Frequency": { - "text": "15 мин", - "value": 900, - "begin": { - "value": "1568602180", - "tzOffset": 10800, - "text": "5:49" - }, - "end": { - "value": "1568673460", - "tzOffset": 10800, - "text": "1:37" - } - } - } - } - ], - "threadId": "213A_282_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9641102", - "name": "Улица Корнейчука" - }, - { - "id": "2532226085", - "name": "Метро Войковская" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568659888", - "tzOffset": 10800, - "text": "21:51" - }, - "vehicleId": "codd%5Fnew|34874%5F9345408" - } - ], - "Frequency": { - "text": "15 мин", - "value": 900, - "begin": { - "value": "1568602180", - "tzOffset": 10800, - "text": "5:49" - }, - "end": { - "value": "1568673460", - "tzOffset": 10800, - "text": "1:37" - } - } - } - }, - { - "lineId": "213_294m_minibus_default", - "name": "994", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213A_294m_minibus_default", - "EssentialStops": [ - { - "id": "stop__9640756", - "name": "Метро Петровско-Разумовская" - }, - { - "id": "stop__9649459", - "name": "Метро Алтуфьево" - } - ], - "BriefSchedule": { - "Events": [], - "Frequency": { - "text": "30 мин", - "value": 1800, - "begin": { - "value": "1568601527", - "tzOffset": 10800, - "text": "5:38" - }, - "end": { - "value": "1568671727", - "tzOffset": 10800, - "text": "1:08" - } - } - } - } - ], - "threadId": "213A_294m_minibus_default", - "EssentialStops": [ - { - "id": "stop__9640756", - "name": "Метро Петровско-Разумовская" - }, - { - "id": "stop__9649459", - "name": "Метро Алтуфьево" - } - ], - "BriefSchedule": { - "Events": [], - "Frequency": { - "text": "30 мин", - "value": 1800, - "begin": { - "value": "1568601527", - "tzOffset": 10800, - "text": "5:38" - }, - "end": { - "value": "1568671727", - "tzOffset": 10800, - "text": "1:08" - } - } - } - }, - { - "lineId": "213_36_trolleybus_mosgortrans", - "name": "т36", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213A_36_trolleybus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9642550", - "name": "ВДНХ (южная)" - }, - { - "id": "stop__9640641", - "name": "Дмитровское шоссе, 155" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659680", - "tzOffset": 10800, - "text": "21:48" - }, - "Estimated": { - "value": "1568659426", - "tzOffset": 10800, - "text": "21:43" - }, - "vehicleId": "codd%5Fnew|1084829%5F430260" - }, - { - "Scheduled": { - "value": "1568660520", - "tzOffset": 10800, - "text": "22:02" - }, - "Estimated": { - "value": "1568659656", - "tzOffset": 10800, - "text": "21:47" - }, - "vehicleId": "codd%5Fnew|1117016%5F430280" - }, - { - "Scheduled": { - "value": "1568661900", - "tzOffset": 10800, - "text": "22:25" - }, - "Estimated": { - "value": "1568660538", - "tzOffset": 10800, - "text": "22:02" - }, - "vehicleId": "codd%5Fnew|1054576%5F430226" - } - ], - "departureTime": "21:48" - } - } - ], - "threadId": "213A_36_trolleybus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9642550", - "name": "ВДНХ (южная)" - }, - { - "id": "stop__9640641", - "name": "Дмитровское шоссе, 155" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659680", - "tzOffset": 10800, - "text": "21:48" - }, - "Estimated": { - "value": "1568659426", - "tzOffset": 10800, - "text": "21:43" - }, - "vehicleId": "codd%5Fnew|1084829%5F430260" - }, - { - "Scheduled": { - "value": "1568660520", - "tzOffset": 10800, - "text": "22:02" - }, - "Estimated": { - "value": "1568659656", - "tzOffset": 10800, - "text": "21:47" - }, - "vehicleId": "codd%5Fnew|1117016%5F430280" - }, - { - "Scheduled": { - "value": "1568661900", - "tzOffset": 10800, - "text": "22:25" - }, - "Estimated": { - "value": "1568660538", - "tzOffset": 10800, - "text": "22:02" - }, - "vehicleId": "codd%5Fnew|1054576%5F430226" - } - ], - "departureTime": "21:48" - } - }, - { - "lineId": "213_47_trolleybus_mosgortrans", - "name": "т47", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213B_47_trolleybus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9639568", - "name": "Бескудниковский переулок" - }, - { - "id": "stop__9641903", - "name": "Бескудниковский переулок" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659980", - "tzOffset": 10800, - "text": "21:53" - }, - "Estimated": { - "value": "1568659253", - "tzOffset": 10800, - "text": "21:40" - }, - "vehicleId": "codd%5Fnew|1112219%5F430329" - }, - { - "Scheduled": { - "value": "1568660940", - "tzOffset": 10800, - "text": "22:09" - }, - "Estimated": { - "value": "1568660519", - "tzOffset": 10800, - "text": "22:01" - }, - "vehicleId": "codd%5Fnew|1139620%5F430382" - }, - { - "Scheduled": { - "value": "1568663580", - "tzOffset": 10800, - "text": "22:53" - } - } - ], - "departureTime": "21:53" - } - } - ], - "threadId": "213B_47_trolleybus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9639568", - "name": "Бескудниковский переулок" - }, - { - "id": "stop__9641903", - "name": "Бескудниковский переулок" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659980", - "tzOffset": 10800, - "text": "21:53" - }, - "Estimated": { - "value": "1568659253", - "tzOffset": 10800, - "text": "21:40" - }, - "vehicleId": "codd%5Fnew|1112219%5F430329" - }, - { - "Scheduled": { - "value": "1568660940", - "tzOffset": 10800, - "text": "22:09" - }, - "Estimated": { - "value": "1568660519", - "tzOffset": 10800, - "text": "22:01" - }, - "vehicleId": "codd%5Fnew|1139620%5F430382" - }, - { - "Scheduled": { - "value": "1568663580", - "tzOffset": 10800, - "text": "22:53" - } - } - ], - "departureTime": "21:53" - } - }, - { - "lineId": "213_56_trolleybus_mosgortrans", - "name": "т56", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213A_56_trolleybus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9639561", - "name": "Коровинское шоссе" - }, - { - "id": "stop__9639588", - "name": "Коровинское шоссе" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568660675", - "tzOffset": 10800, - "text": "22:04" - }, - "vehicleId": "codd%5Fnew|146304%5F31207" - } - ], - "Frequency": { - "text": "8 мин", - "value": 480, - "begin": { - "value": "1568606244", - "tzOffset": 10800, - "text": "6:57" - }, - "end": { - "value": "1568670144", - "tzOffset": 10800, - "text": "0:42" - } - } - } - } - ], - "threadId": "213A_56_trolleybus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9639561", - "name": "Коровинское шоссе" - }, - { - "id": "stop__9639588", - "name": "Коровинское шоссе" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568660675", - "tzOffset": 10800, - "text": "22:04" - }, - "vehicleId": "codd%5Fnew|146304%5F31207" - } - ], - "Frequency": { - "text": "8 мин", - "value": 480, - "begin": { - "value": "1568606244", - "tzOffset": 10800, - "text": "6:57" - }, - "end": { - "value": "1568670144", - "tzOffset": 10800, - "text": "0:42" - } - } - } - }, - { - "lineId": "213_63_bus_mosgortrans", - "name": "63", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213A_63_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9640554", - "name": "Лобненская улица" - }, - { - "id": "stop__9640553", - "name": "Лобненская улица" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568659369", - "tzOffset": 10800, - "text": "21:42" - }, - "vehicleId": "codd%5Fnew|38921%5F9215306" - }, - { - "Estimated": { - "value": "1568660136", - "tzOffset": 10800, - "text": "21:55" - }, - "vehicleId": "codd%5Fnew|38918%5F9215303" - } - ], - "Frequency": { - "text": "17 мин", - "value": 1020, - "begin": { - "value": "1568600987", - "tzOffset": 10800, - "text": "5:29" - }, - "end": { - "value": "1568670227", - "tzOffset": 10800, - "text": "0:43" - } - } - } - } - ], - "threadId": "213A_63_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9640554", - "name": "Лобненская улица" - }, - { - "id": "stop__9640553", - "name": "Лобненская улица" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568659369", - "tzOffset": 10800, - "text": "21:42" - }, - "vehicleId": "codd%5Fnew|38921%5F9215306" - }, - { - "Estimated": { - "value": "1568660136", - "tzOffset": 10800, - "text": "21:55" - }, - "vehicleId": "codd%5Fnew|38918%5F9215303" - } - ], - "Frequency": { - "text": "17 мин", - "value": 1020, - "begin": { - "value": "1568600987", - "tzOffset": 10800, - "text": "5:29" - }, - "end": { - "value": "1568670227", - "tzOffset": 10800, - "text": "0:43" - } - } - } - }, - { - "lineId": "213_677_bus_mosgortrans", - "name": "677", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213B_677_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9639495", - "name": "Метро Петровско-Разумовская" - }, - { - "id": "stop__9639480", - "name": "Платформа Лианозово" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568659369", - "tzOffset": 10800, - "text": "21:42" - }, - "vehicleId": "codd%5Fnew|11731%5F31376" - } - ], - "Frequency": { - "text": "4 мин", - "value": 240, - "begin": { - "value": "1568600940", - "tzOffset": 10800, - "text": "5:29" - }, - "end": { - "value": "1568672640", - "tzOffset": 10800, - "text": "1:24" - } - } - } - } - ], - "threadId": "213B_677_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9639495", - "name": "Метро Петровско-Разумовская" - }, - { - "id": "stop__9639480", - "name": "Платформа Лианозово" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568659369", - "tzOffset": 10800, - "text": "21:42" - }, - "vehicleId": "codd%5Fnew|11731%5F31376" - } - ], - "Frequency": { - "text": "4 мин", - "value": 240, - "begin": { - "value": "1568600940", - "tzOffset": 10800, - "text": "5:29" - }, - "end": { - "value": "1568672640", - "tzOffset": 10800, - "text": "1:24" - } - } - } - }, - { - "lineId": "213_692_bus_mosgortrans", - "name": "692", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "2036928706", - "EssentialStops": [ - { - "id": "3163417967", - "name": "Платформа Дегунино" - }, - { - "id": "3163417967", - "name": "Платформа Дегунино" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568660280", - "tzOffset": 10800, - "text": "21:58" - }, - "Estimated": { - "value": "1568660255", - "tzOffset": 10800, - "text": "21:57" - }, - "vehicleId": "codd%5Fnew|63029%5F31485" - }, - { - "Scheduled": { - "value": "1568693340", - "tzOffset": 10800, - "text": "7:09" - } - }, - { - "Scheduled": { - "value": "1568696940", - "tzOffset": 10800, - "text": "8:09" - } - } - ], - "departureTime": "21:58" - } - } - ], - "threadId": "2036928706", - "EssentialStops": [ - { - "id": "3163417967", - "name": "Платформа Дегунино" - }, - { - "id": "3163417967", - "name": "Платформа Дегунино" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568660280", - "tzOffset": 10800, - "text": "21:58" - }, - "Estimated": { - "value": "1568660255", - "tzOffset": 10800, - "text": "21:57" - }, - "vehicleId": "codd%5Fnew|63029%5F31485" - }, - { - "Scheduled": { - "value": "1568693340", - "tzOffset": 10800, - "text": "7:09" - } - }, - { - "Scheduled": { - "value": "1568696940", - "tzOffset": 10800, - "text": "8:09" - } - } - ], - "departureTime": "21:58" - } - }, - { - "lineId": "213_78_trolleybus_mosgortrans", - "name": "т78", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213A_78_trolleybus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9887464", - "name": "9-я Северная линия" - }, - { - "id": "stop__9887464", - "name": "9-я Северная линия" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659620", - "tzOffset": 10800, - "text": "21:47" - }, - "Estimated": { - "value": "1568659898", - "tzOffset": 10800, - "text": "21:51" - }, - "vehicleId": "codd%5Fnew|147522%5F31184" - }, - { - "Scheduled": { - "value": "1568660760", - "tzOffset": 10800, - "text": "22:06" - } - }, - { - "Scheduled": { - "value": "1568661900", - "tzOffset": 10800, - "text": "22:25" - } - } - ], - "departureTime": "21:47" - } - } - ], - "threadId": "213A_78_trolleybus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9887464", - "name": "9-я Северная линия" - }, - { - "id": "stop__9887464", - "name": "9-я Северная линия" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659620", - "tzOffset": 10800, - "text": "21:47" - }, - "Estimated": { - "value": "1568659898", - "tzOffset": 10800, - "text": "21:51" - }, - "vehicleId": "codd%5Fnew|147522%5F31184" - }, - { - "Scheduled": { - "value": "1568660760", - "tzOffset": 10800, - "text": "22:06" - } - }, - { - "Scheduled": { - "value": "1568661900", - "tzOffset": 10800, - "text": "22:25" - } - } - ], - "departureTime": "21:47" - } - }, - { - "lineId": "213_82_bus_mosgortrans", - "name": "82", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "2036925244", - "EssentialStops": [ - { - "id": "2310890052", - "name": "Метро Верхние Лихоборы" - }, - { - "id": "2310890052", - "name": "Метро Верхние Лихоборы" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659680", - "tzOffset": 10800, - "text": "21:48" - } - }, - { - "Scheduled": { - "value": "1568661780", - "tzOffset": 10800, - "text": "22:23" - } - }, - { - "Scheduled": { - "value": "1568663760", - "tzOffset": 10800, - "text": "22:56" - } - } - ], - "departureTime": "21:48" - } - } - ], - "threadId": "2036925244", - "EssentialStops": [ - { - "id": "2310890052", - "name": "Метро Верхние Лихоборы" - }, - { - "id": "2310890052", - "name": "Метро Верхние Лихоборы" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659680", - "tzOffset": 10800, - "text": "21:48" - } - }, - { - "Scheduled": { - "value": "1568661780", - "tzOffset": 10800, - "text": "22:23" - } - }, - { - "Scheduled": { - "value": "1568663760", - "tzOffset": 10800, - "text": "22:56" - } - } - ], - "departureTime": "21:48" - } - }, - { - "lineId": "2465131598", - "name": "179к", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "2465131758", - "EssentialStops": [ - { - "id": "stop__9640244", - "name": "Платформа Лианозово" - }, - { - "id": "stop__9639480", - "name": "Платформа Лианозово" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659500", - "tzOffset": 10800, - "text": "21:45" - } - }, - { - "Scheduled": { - "value": "1568659980", - "tzOffset": 10800, - "text": "21:53" - } - }, - { - "Scheduled": { - "value": "1568660880", - "tzOffset": 10800, - "text": "22:08" - } - } - ], - "departureTime": "21:45" - } - } - ], - "threadId": "2465131758", - "EssentialStops": [ - { - "id": "stop__9640244", - "name": "Платформа Лианозово" - }, - { - "id": "stop__9639480", - "name": "Платформа Лианозово" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659500", - "tzOffset": 10800, - "text": "21:45" - } - }, - { - "Scheduled": { - "value": "1568659980", - "tzOffset": 10800, - "text": "21:53" - } - }, - { - "Scheduled": { - "value": "1568660880", - "tzOffset": 10800, - "text": "22:08" - } - } - ], - "departureTime": "21:45" - } - }, - { - "lineId": "466_bus_default", - "name": "466", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "466B_bus_default", - "EssentialStops": [ - { - "id": "stop__9640546", - "name": "Станция Бескудниково" - }, - { - "id": "stop__9640545", - "name": "Станция Бескудниково" - } - ], - "BriefSchedule": { - "Events": [], - "Frequency": { - "text": "22 мин", - "value": 1320, - "begin": { - "value": "1568604647", - "tzOffset": 10800, - "text": "6:30" - }, - "end": { - "value": "1568675447", - "tzOffset": 10800, - "text": "2:10" - } - } - } - } - ], - "threadId": "466B_bus_default", - "EssentialStops": [ - { - "id": "stop__9640546", - "name": "Станция Бескудниково" - }, - { - "id": "stop__9640545", - "name": "Станция Бескудниково" - } - ], - "BriefSchedule": { - "Events": [], - "Frequency": { - "text": "22 мин", - "value": 1320, - "begin": { - "value": "1568604647", - "tzOffset": 10800, - "text": "6:30" - }, - "end": { - "value": "1568675447", - "tzOffset": 10800, - "text": "2:10" - } - } - } - }, - { - "lineId": "677k_bus_default", - "name": "677к", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "677kA_bus_default", - "EssentialStops": [ - { - "id": "stop__9640244", - "name": "Платформа Лианозово" - }, - { - "id": "stop__9639480", - "name": "Платформа Лианозово" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659920", - "tzOffset": 10800, - "text": "21:52" - }, - "Estimated": { - "value": "1568660003", - "tzOffset": 10800, - "text": "21:53" - }, - "vehicleId": "codd%5Fnew|130308%5F31319" - }, - { - "Scheduled": { - "value": "1568661240", - "tzOffset": 10800, - "text": "22:14" - } - }, - { - "Scheduled": { - "value": "1568662500", - "tzOffset": 10800, - "text": "22:35" - } - } - ], - "departureTime": "21:52" - } - } - ], - "threadId": "677kA_bus_default", - "EssentialStops": [ - { - "id": "stop__9640244", - "name": "Платформа Лианозово" - }, - { - "id": "stop__9639480", - "name": "Платформа Лианозово" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659920", - "tzOffset": 10800, - "text": "21:52" - }, - "Estimated": { - "value": "1568660003", - "tzOffset": 10800, - "text": "21:53" - }, - "vehicleId": "codd%5Fnew|130308%5F31319" - }, - { - "Scheduled": { - "value": "1568661240", - "tzOffset": 10800, - "text": "22:14" - } - }, - { - "Scheduled": { - "value": "1568662500", - "tzOffset": 10800, - "text": "22:35" - } - } - ], - "departureTime": "21:52" - } - }, - { - "lineId": "m10_bus_default", - "name": "м10", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "2036926048", - "EssentialStops": [ - { - "id": "stop__9640554", - "name": "Лобненская улица" - }, - { - "id": "stop__9640553", - "name": "Лобненская улица" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568659718", - "tzOffset": 10800, - "text": "21:48" - }, - "vehicleId": "codd%5Fnew|146260%5F31212" - }, - { - "Estimated": { - "value": "1568660422", - "tzOffset": 10800, - "text": "22:00" - }, - "vehicleId": "codd%5Fnew|13997%5F31247" - } - ], - "Frequency": { - "text": "15 мин", - "value": 900, - "begin": { - "value": "1568606903", - "tzOffset": 10800, - "text": "7:08" - }, - "end": { - "value": "1568675183", - "tzOffset": 10800, - "text": "2:06" - } - } - } - } - ], - "threadId": "2036926048", - "EssentialStops": [ - { - "id": "stop__9640554", - "name": "Лобненская улица" - }, - { - "id": "stop__9640553", - "name": "Лобненская улица" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568659718", - "tzOffset": 10800, - "text": "21:48" - }, - "vehicleId": "codd%5Fnew|146260%5F31212" - }, - { - "Estimated": { - "value": "1568660422", - "tzOffset": 10800, - "text": "22:00" - }, - "vehicleId": "codd%5Fnew|13997%5F31247" - } - ], - "Frequency": { - "text": "15 мин", - "value": 900, - "begin": { - "value": "1568606903", - "tzOffset": 10800, - "text": "7:08" - }, - "end": { - "value": "1568675183", - "tzOffset": 10800, - "text": "2:06" - } - } - } - } + ], + "names": { + "ablative": "", + "accusative": "Москву", + "dative": "Москве", + "directional": "", + "genitive": "Москвы", + "instrumental": "Москвой", + "locative": "", + "nominative": "Москва", + "preposition": "в", + "prepositional": "Москве" + }, + "parent": { + "id": 1, + "type": 5, + "parent_id": 3, + "capital_id": 213, + "geo_parent_id": 0, + "city_id": 213, + "name": "moscow-and-moscow-oblast", + "native_name": "", + "iso_name": "RU-MOS", + "is_main": true, + "en_name": "Moscow and Moscow Oblast", + "short_en_name": "RU-MOS", + "phone_code": "495 496 498 499", + "phone_code_old": "", + "zip_code": "", + "population": 7503385, + "synonyms": "Московская область, Подмосковье, Podmoskovye", + "latitude": 55.815792, + "longitude": 37.380031, + "latitude_size": 2.705659, + "longitude_size": 5.060749, + "zoom": 8, + "tzname": "Europe/Moscow", + "official_languages": "ru", + "widespread_languages": "ru", + "suggest_list": [ + 213, + 10716, + 10747, + 10758, + 20728, + 10740, + 10738, + 20523, + 10735, + 10734, + 10743, + 21622 + ], + "is_eu": false, + "services_names": [ + "bs", + "yaca", + "ad" + ], + "ename": "moscow-and-moscow-oblast", + "bounds": [ + [ + 34.8496565, + 54.439456064325434 + ], + [ + 39.9104055, + 57.14511506432543 + ] + ], + "names": { + "ablative": "", + "accusative": "Москву и Московскую область", + "dative": "Москве и Московской области", + "directional": "", + "genitive": "Москвы и Московской области", + "instrumental": "Москвой и Московской областью", + "locative": "", + "nominative": "Москва и Московская область", + "preposition": "в", + "prepositional": "Москве и Московской области" + }, + "parent": { + "id": 225, + "type": 3, + "parent_id": 10001, + "capital_id": 213, + "geo_parent_id": 0, + "city_id": 213, + "name": "russia", + "native_name": "", + "iso_name": "RU", + "is_main": false, + "en_name": "Russia", + "short_en_name": "RU", + "phone_code": "7", + "phone_code_old": "", + "zip_code": "", + "population": 146880432, + "synonyms": "Russian Federation,Российская Федерация", + "latitude": 61.698653, + "longitude": 99.505405, + "latitude_size": 40.700127, + "longitude_size": 171.643239, + "zoom": 3, + "tzname": "", + "official_languages": "ru", + "widespread_languages": "ru", + "suggest_list": [ + 213, + 2, + 65, + 54, + 47, + 43, + 66, + 51, + 56, + 172, + 39, + 62 + ], + "is_eu": false, + "services_names": [ + "bs", + "yaca", + "ad" + ], + "ename": "russia", + "bounds": [ + [ + 13.683785499999999, + 35.290400699917846 + ], + [ + -174.6729755, + 75.99052769991785 ] + ], + "names": { + "ablative": "", + "accusative": "Россию", + "dative": "России", + "directional": "", + "genitive": "России", + "instrumental": "Россией", + "locative": "", + "nominative": "Россия", + "preposition": "в", + "prepositional": "России" + } } + } }, - "toponymSeoname": "dmitrovskoye_shosse" - } -} + "Transport": [ + { + "lineId": "2036924720", + "name": "692", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "2036928706", + "EssentialStops": [ + { + "id": "3163417967", + "name": "Платформа Дегунино" + }, + { + "id": "3163417967", + "name": "Платформа Дегунино" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1570973441", + "tzOffset": 10800, + "text": "16:30" + }, + "vehicleId": "codd%5Fnew|144020%5F31402" + } + ], + "Frequency": { + "text": "1 ч", + "value": 3600, + "begin": { + "value": "1570938428", + "tzOffset": 10800, + "text": "6:47" + }, + "end": { + "value": "1570990628", + "tzOffset": 10800, + "text": "21:17" + } + } + } + } + ], + "uri": "ymapsbm1://transit/line?id=2036924720&ll=37.577436%2C55.828981&name=692&r=4037&type=bus", + "seoname": "692" + }, + { + "lineId": "2036924968", + "name": "82", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "2036925244", + "EssentialStops": [ + { + "id": "2310890052", + "name": "Метро Верхние Лихоборы" + }, + { + "id": "2310890052", + "name": "Метро Верхние Лихоборы" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "34 мин", + "value": 2040, + "begin": { + "value": "1570944072", + "tzOffset": 10800, + "text": "8:21" + }, + "end": { + "value": "1570997592", + "tzOffset": 10800, + "text": "23:13" + } + } + } + } + ], + "uri": "ymapsbm1://transit/line?id=2036924968&ll=37.571504%2C55.816622&name=82&r=4164&type=bus", + "seoname": "82" + }, + { + "lineId": "2036925416", + "name": "194", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "2036927196", + "EssentialStops": [ + { + "id": "stop__9711780", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9648742", + "name": "Коровино" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "12 мин", + "value": 720, + "begin": { + "value": "1570933976", + "tzOffset": 10800, + "text": "5:32" + }, + "end": { + "value": "1571004356", + "tzOffset": 10800, + "text": "1:05" + } + } + } + } + ], + "uri": "ymapsbm1://transit/line?id=2036925416&ll=37.544800%2C55.865286&name=194&r=3667&type=bus", + "seoname": "194" + }, + { + "lineId": "2036925728", + "name": "282", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_282_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9641102", + "name": "Улица Корнейчука" + }, + { + "id": "2532226085", + "name": "Метро Войковская" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1570971861", + "tzOffset": 10800, + "text": "16:04" + }, + "vehicleId": "codd%5Fnew|34854%5F9345401" + }, + { + "Estimated": { + "value": "1570973231", + "tzOffset": 10800, + "text": "16:27" + }, + "vehicleId": "codd%5Fnew|37913%5F9225419" + } + ], + "Frequency": { + "text": "22 мин", + "value": 1320, + "begin": { + "value": "1570934963", + "tzOffset": 10800, + "text": "5:49" + }, + "end": { + "value": "1571005163", + "tzOffset": 10800, + "text": "1:19" + } + } + } + } + ], + "uri": "ymapsbm1://transit/line?id=2036925728&ll=37.553526%2C55.860385&name=282&r=5779&type=bus", + "seoname": "282" + }, + { + "lineId": "2036926781", + "name": "154", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213B_154_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9642548", + "name": "ВДНХ (южная)" + }, + { + "id": "stop__9711744", + "name": "Станция Ховрино" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1570972424", + "tzOffset": 10800, + "text": "16:13" + }, + "vehicleId": "codd%5Fnew|1161539%5F191543" + }, + { + "Estimated": { + "value": "1570973620", + "tzOffset": 10800, + "text": "16:33" + }, + "vehicleId": "codd%5Fnew|58773%5F190599" + } + ], + "Frequency": { + "text": "20 мин", + "value": 1200, + "begin": { + "value": "1570938166", + "tzOffset": 10800, + "text": "6:42" + }, + "end": { + "value": "1571006446", + "tzOffset": 10800, + "text": "1:40" + } + } + } + } + ], + "uri": "ymapsbm1://transit/line?id=2036926781&ll=37.576158%2C55.846301&name=154&r=4917&type=bus", + "seoname": "154" + }, + { + "lineId": "2036926818", + "name": "994", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_294m_minibus_default", + "EssentialStops": [ + { + "id": "stop__9640756", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9649459", + "name": "Метро Алтуфьево" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "30 мин", + "value": 1800, + "begin": { + "value": "1570934327", + "tzOffset": 10800, + "text": "5:38" + }, + "end": { + "value": "1571004527", + "tzOffset": 10800, + "text": "1:08" + } + } + } + } + ], + "uri": "ymapsbm1://transit/line?id=2036926818&ll=37.560060%2C55.868431&name=994&r=3637&type=bus", + "seoname": "994" + }, + { + "lineId": "2036926890", + "name": "466", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "466B_bus_default", + "EssentialStops": [ + { + "id": "stop__9640546", + "name": "Станция Бескудниково" + }, + { + "id": "stop__9640545", + "name": "Станция Бескудниково" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "22 мин", + "value": 1320, + "begin": { + "value": "1570937447", + "tzOffset": 10800, + "text": "6:30" + }, + "end": { + "value": "1571008247", + "tzOffset": 10800, + "text": "2:10" + } + } + } + } + ], + "uri": "ymapsbm1://transit/line?id=2036926890&ll=37.564238%2C55.845050&name=466&r=4163&type=bus", + "seoname": "466" + }, + { + "lineId": "213_114_bus_mosgortrans", + "name": "114", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213B_114_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9647199", + "name": "Метро Войковская" + }, + { + "id": "stop__9639588", + "name": "Коровинское шоссе" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1570972913", + "tzOffset": 10800, + "text": "16:21" + }, + "vehicleId": "codd%5Fnew|1092230%5F191422" + } + ], + "Frequency": { + "text": "15 мин", + "value": 900, + "begin": { + "value": "1570936205", + "tzOffset": 10800, + "text": "6:10" + }, + "end": { + "value": "1571004965", + "tzOffset": 10800, + "text": "1:16" + } + } + } + } + ], + "uri": "ymapsbm1://transit/line?id=213_114_bus_mosgortrans&ll=37.508487%2C55.852137&name=114&r=3544&type=bus", + "seoname": "114" + }, + { + "lineId": "213_179_bus_mosgortrans", + "name": "179", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213B_179_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9647199", + "name": "Метро Войковская" + }, + { + "id": "stop__9639480", + "name": "Платформа Лианозово" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1570971963", + "tzOffset": 10800, + "text": "16:06" + }, + "vehicleId": "codd%5Fnew|194519%5F31367" + }, + { + "Estimated": { + "value": "1570973105", + "tzOffset": 10800, + "text": "16:25" + }, + "vehicleId": "codd%5Fnew|56358%5F31365" + } + ], + "Frequency": { + "text": "15 мин", + "value": 900, + "begin": { + "value": "1570936823", + "tzOffset": 10800, + "text": "6:20" + }, + "end": { + "value": "1571005583", + "tzOffset": 10800, + "text": "1:26" + } + } + } + } + ], + "uri": "ymapsbm1://transit/line?id=213_179_bus_mosgortrans&ll=37.526151%2C55.858031&name=179&r=4634&type=bus", + "seoname": "179" + }, + { + "lineId": "213_191m_minibus_default", + "name": "591", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_191m_minibus_default", + "EssentialStops": [ + { + "id": "stop__9647199", + "name": "Метро Войковская" + }, + { + "id": "stop__9711744", + "name": "Станция Ховрино" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1570972150", + "tzOffset": 10800, + "text": "16:09" + }, + "vehicleId": "codd%5Fnew|35595%5F9345307" + } + ], + "Frequency": { + "text": "22 мин", + "value": 1320, + "begin": { + "value": "1570934833", + "tzOffset": 10800, + "text": "5:47" + }, + "end": { + "value": "1571005033", + "tzOffset": 10800, + "text": "1:17" + } + } + } + } + ], + "uri": "ymapsbm1://transit/line?id=213_191m_minibus_default&ll=37.510906%2C55.848214&name=591&r=3384&type=bus", + "seoname": "591" + }, + { + "lineId": "213_206m_minibus_default", + "name": "206к", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_206m_minibus_default", + "EssentialStops": [ + { + "id": "stop__9640756", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9640553", + "name": "Лобненская улица" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "22 мин", + "value": 1320, + "begin": { + "value": "1570934039", + "tzOffset": 10800, + "text": "5:33" + }, + "end": { + "value": "1571004239", + "tzOffset": 10800, + "text": "1:03" + } + } + } + } + ], + "uri": "ymapsbm1://transit/line?id=213_206m_minibus_default&ll=37.548997%2C55.864997&name=206%D0%BA&r=3515&type=bus", + "seoname": "206k" + }, + { + "lineId": "213_215_bus_mosgortrans", + "name": "215", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213B_215_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9711780", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9711744", + "name": "Станция Ховрино" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "27 мин", + "value": 1620, + "begin": { + "value": "1570934076", + "tzOffset": 10800, + "text": "5:34" + }, + "end": { + "value": "1571004276", + "tzOffset": 10800, + "text": "1:04" + } + } + } + } + ], + "uri": "ymapsbm1://transit/line?id=213_215_bus_mosgortrans&ll=37.543701%2C55.854527&name=215&r=2763&type=bus", + "seoname": "215" + }, + { + "lineId": "213_36_trolleybus_mosgortrans", + "name": "т36", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_36_trolleybus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9642550", + "name": "ВДНХ (южная)" + }, + { + "id": "stop__9640641", + "name": "Дмитровское шоссе, 155" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1570972236", + "tzOffset": 10800, + "text": "16:10" + }, + "vehicleId": "codd%5Fnew|1084830%5F430261" + }, + { + "Estimated": { + "value": "1570972641", + "tzOffset": 10800, + "text": "16:17" + }, + "vehicleId": "codd%5Fnew|1084829%5F430260" + }, + { + "Estimated": { + "value": "1570973178", + "tzOffset": 10800, + "text": "16:26" + }, + "vehicleId": "codd%5Fnew|1084827%5F430255" + } + ], + "Frequency": { + "text": "12 мин", + "value": 720, + "begin": { + "value": "1570932741", + "tzOffset": 10800, + "text": "5:12" + }, + "end": { + "value": "1571003121", + "tzOffset": 10800, + "text": "0:45" + } + } + } + } + ], + "uri": "ymapsbm1://transit/line?id=213_36_trolleybus_mosgortrans&ll=37.588604%2C55.859705&name=%D1%8236&r=5104&type=bus", + "seoname": "t36" + }, + { + "lineId": "213_47_trolleybus_mosgortrans", + "name": "т47", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213B_47_trolleybus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9639568", + "name": "Бескудниковский переулок" + }, + { + "id": "stop__9641903", + "name": "Бескудниковский переулок" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1570972080", + "tzOffset": 10800, + "text": "16:08" + }, + "Estimated": { + "value": "1570972183", + "tzOffset": 10800, + "text": "16:09" + }, + "vehicleId": "codd%5Fnew|1132404%5F430361" + }, + { + "Scheduled": { + "value": "1570972980", + "tzOffset": 10800, + "text": "16:23" + }, + "Estimated": { + "value": "1570972219", + "tzOffset": 10800, + "text": "16:10" + }, + "vehicleId": "codd%5Fnew|1136132%5F430358" + }, + { + "Scheduled": { + "value": "1570973940", + "tzOffset": 10800, + "text": "16:39" + } + } + ], + "departureTime": "16:08" + } + } + ], + "uri": "ymapsbm1://transit/line?id=213_47_trolleybus_mosgortrans&ll=37.588308%2C55.818685&name=%D1%8247&r=5359&type=bus", + "seoname": "t47" + }, + { + "lineId": "213_56_trolleybus_mosgortrans", + "name": "т56", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_56_trolleybus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9639561", + "name": "Коровинское шоссе" + }, + { + "id": "stop__9639588", + "name": "Коровинское шоссе" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1570971900", + "tzOffset": 10800, + "text": "16:05" + }, + "Estimated": { + "value": "1570972560", + "tzOffset": 10800, + "text": "16:16" + }, + "vehicleId": "codd%5Fnew|1117148%5F430351" + }, + { + "Scheduled": { + "value": "1570972680", + "tzOffset": 10800, + "text": "16:18" + }, + "Estimated": { + "value": "1570973442", + "tzOffset": 10800, + "text": "16:30" + }, + "vehicleId": "codd%5Fnew|1080552%5F430302" + }, + { + "Scheduled": { + "value": "1570973400", + "tzOffset": 10800, + "text": "16:30" + } + } + ], + "departureTime": "16:05" + } + } + ], + "uri": "ymapsbm1://transit/line?id=213_56_trolleybus_mosgortrans&ll=37.551454%2C55.830147&name=%D1%8256&r=6304&type=bus", + "seoname": "t56" + }, + { + "lineId": "213_63_bus_mosgortrans", + "name": "63", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_63_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9640554", + "name": "Лобненская улица" + }, + { + "id": "stop__9640553", + "name": "Лобненская улица" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1570972434", + "tzOffset": 10800, + "text": "16:13" + }, + "vehicleId": "codd%5Fnew|38700%5F9215301" + } + ], + "Frequency": { + "text": "17 мин", + "value": 1020, + "begin": { + "value": "1570934207", + "tzOffset": 10800, + "text": "5:36" + }, + "end": { + "value": "1571003507", + "tzOffset": 10800, + "text": "0:51" + } + } + } + } + ], + "uri": "ymapsbm1://transit/line?id=213_63_bus_mosgortrans&ll=37.550792%2C55.872690&name=63&r=3057&type=bus", + "seoname": "63" + }, + { + "lineId": "213_677_bus_mosgortrans", + "name": "677", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213B_677_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9639495", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9639480", + "name": "Платформа Лианозово" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1570972200", + "tzOffset": 10800, + "text": "16:10" + }, + "Estimated": { + "value": "1570971838", + "tzOffset": 10800, + "text": "16:03" + }, + "vehicleId": "codd%5Fnew|58581%5F31321" + }, + { + "Scheduled": { + "value": "1570972560", + "tzOffset": 10800, + "text": "16:16" + } + }, + { + "Scheduled": { + "value": "1570972920", + "tzOffset": 10800, + "text": "16:22" + } + } + ], + "departureTime": "16:10" + } + } + ], + "uri": "ymapsbm1://transit/line?id=213_677_bus_mosgortrans&ll=37.564191%2C55.866620&name=677&r=3386&type=bus", + "seoname": "677" + }, + { + "lineId": "213_78_trolleybus_mosgortrans", + "name": "т78", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_78_trolleybus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9887464", + "name": "9-я Северная линия" + }, + { + "id": "stop__9887464", + "name": "9-я Северная линия" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1570971984", + "tzOffset": 10800, + "text": "16:06" + }, + "vehicleId": "codd%5Fnew|59694%5F31155" + }, + { + "Estimated": { + "value": "1570972003", + "tzOffset": 10800, + "text": "16:06" + }, + "vehicleId": "codd%5Fnew|55041%5F31116" + }, + { + "Estimated": { + "value": "1570972550", + "tzOffset": 10800, + "text": "16:15" + }, + "vehicleId": "codd%5Fnew|62710%5F31142" + }, + { + "Estimated": { + "value": "1570973307", + "tzOffset": 10800, + "text": "16:28" + }, + "vehicleId": "codd%5Fnew|1037437%5F31144" + }, + { + "Estimated": { + "value": "1570973456", + "tzOffset": 10800, + "text": "16:30" + }, + "vehicleId": "codd%5Fnew|318517%5F31136" + } + ], + "Frequency": { + "text": "11 мин", + "value": 660, + "begin": { + "value": "1570937045", + "tzOffset": 10800, + "text": "6:24" + }, + "end": { + "value": "1571002385", + "tzOffset": 10800, + "text": "0:33" + } + } + } + } + ], + "uri": "ymapsbm1://transit/line?id=213_78_trolleybus_mosgortrans&ll=37.569453%2C55.855402&name=%D1%8278&r=8810&type=bus", + "seoname": "t78" + }, + { + "lineId": "2465131598", + "name": "179к", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "2465131758", + "EssentialStops": [ + { + "id": "stop__9640244", + "name": "Платформа Лианозово" + }, + { + "id": "stop__9639480", + "name": "Платформа Лианозово" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "15 мин", + "value": 900, + "begin": { + "value": "1570935230", + "tzOffset": 10800, + "text": "5:53" + }, + "end": { + "value": "1571003030", + "tzOffset": 10800, + "text": "0:43" + } + } + } + } + ], + "uri": "ymapsbm1://transit/line?id=2465131598&ll=37.561423%2C55.871807&name=179%D0%BA&r=2787&type=bus", + "seoname": "179k" + }, + { + "lineId": "677k_bus_default", + "name": "677к", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "677kA_bus_default", + "EssentialStops": [ + { + "id": "stop__9640244", + "name": "Платформа Лианозово" + }, + { + "id": "stop__9639480", + "name": "Платформа Лианозово" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1570972560", + "tzOffset": 10800, + "text": "16:16" + }, + "Estimated": { + "value": "1570971986", + "tzOffset": 10800, + "text": "16:06" + }, + "vehicleId": "codd%5Fnew|1038096%5F31398" + }, + { + "Scheduled": { + "value": "1570973280", + "tzOffset": 10800, + "text": "16:28" + }, + "Estimated": { + "value": "1570972342", + "tzOffset": 10800, + "text": "16:12" + }, + "vehicleId": "codd%5Fnew|58590%5F31348" + }, + { + "Scheduled": { + "value": "1570974000", + "tzOffset": 10800, + "text": "16:40" + }, + "Estimated": { + "value": "1570973387", + "tzOffset": 10800, + "text": "16:29" + }, + "vehicleId": "codd%5Fnew|58902%5F31316" + } + ], + "departureTime": "16:16" + } + } + ], + "uri": "ymapsbm1://transit/line?id=677k_bus_default&ll=37.565257%2C55.870397&name=677%D0%BA&r=2987&type=bus", + "seoname": "677k" + }, + { + "lineId": "m10_bus_default", + "name": "м10", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "2036926048", + "EssentialStops": [ + { + "id": "stop__9640554", + "name": "Лобненская улица" + }, + { + "id": "stop__9640553", + "name": "Лобненская улица" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1570972343", + "tzOffset": 10800, + "text": "16:12" + }, + "vehicleId": "codd%5Fnew|62922%5F31434" + }, + { + "Estimated": { + "value": "1570972813", + "tzOffset": 10800, + "text": "16:20" + }, + "vehicleId": "codd%5Fnew|57281%5F31242" + } + ], + "Frequency": { + "text": "15 мин", + "value": 900, + "begin": { + "value": "1570939772", + "tzOffset": 10800, + "text": "7:09" + }, + "end": { + "value": "1571008052", + "tzOffset": 10800, + "text": "2:07" + } + } + } + } + ], + "uri": "ymapsbm1://transit/line?id=m10_bus_default&ll=37.579221%2C55.823763&name=%D0%BC10&r=8474&type=bus", + "seoname": "m10" + } + ] + } + }, + "searchResult": { + "requestId": "1570971868582853-530182592-man1-6817", + "title": "7-й автобусный парк", + "description": "Россия, Москва, Дмитровское шоссе", + "address": "Россия, Москва, Дмитровское шоссе", + "coordinates": [ + 37.56528, + 55.85196 + ], + "bounds": [ + [ + 37.543123, + 55.77889866 + ], + [ + 37.587437, + 55.92488366 + ] + ], + "displayCoordinates": [ + 37.56528, + 55.85196 + ], + "metro": [ + { + "id": "2244536395", + "name": "Верхние Лихоборы", + "distance": "510 м", + "distanceValue": 509.265, + "coordinates": [ + 37.56121218, + 55.854501501 + ], + "type": "metro", + "color": "#99cc33" + }, + { + "id": "1727539211", + "name": "Окружная", + "distance": "640 м", + "distanceValue": 641.333, + "coordinates": [ + 37.572849014, + 55.848814359 + ], + "type": "metro", + "color": "#ffa8af" + }, + { + "id": "2244535785", + "name": "Окружная", + "distance": "1,3 км", + "distanceValue": 1263.44, + "coordinates": [ + 37.575977155, + 55.844377845 + ], + "type": "metro", + "color": "#99cc33" + } + ], + "stops": [ + { + "id": "stop__9639579", + "name": "7-й автобусный парк", + "distance": "0 м", + "distanceValue": 0.0383997, + "coordinates": [ + 37.565280044, + 55.851959656 + ], + "type": "common" + }, + { + "id": "2310890052", + "name": "Метро Верхние Лихоборы", + "distance": "420 м", + "distanceValue": 424.274, + "coordinates": [ + 37.563047501, + 55.853727589 + ], + "type": "common" + }, + { + "id": "stop__9639678", + "name": "Метро Верхние Лихоборы (северный вестибюль)", + "distance": "630 м", + "distanceValue": 629.689, + "coordinates": [ + 37.562346735, + 55.857147019 + ], + "type": "common" + }, + { + "id": "station__lh_9601830", + "name": "Окружная", + "distance": "860 м", + "distanceValue": 857.487, + "coordinates": [ + 37.574303, + 55.847684 + ], + "type": "common" + }, + { + "id": "stop__9639906", + "name": "Платформа Окружная", + "distance": "930 м", + "distanceValue": 926.144, + "coordinates": [ + 37.576123886, + 55.847913668 + ], + "type": "common" + } + ], + "logId": "dHlwZT1iaXpmaW5kZXI7aWQ9MjM5MzY2OTUwNjU4", + "type": "business", + "id": "239366950658", + "shortTitle": "7-й автобусный парк", + "additionalAddress": "", + "fullAddress": "Россия, Москва, Дмитровское шоссе", + "postalCode": "", + "addressDetails": { + "locality": "Москва", + "street": "Дмитровское шоссе" + }, + "categories": [ + { + "name": "Остановка общественного транспорта", + "class": "bus stop", + "seoname": "public_transport_stop", + "pluralName": "Остановки общественного транспорта", + "id": "223677355200" + } + ], + "status": "open", + "businessLinks": [], + "businessProperties": { + "geoproduct_poi_color": "#ABAEB3", + "snippet_show_title": "short_title", + "snippet_show_rating": "five_star_rating", + "snippet_show_photo": "single_photo", + "snippet_show_eta": "show_eta", + "snippet_show_category": "single_category", + "snippet_show_subline": [ + "no_subline" + ], + "snippet_show_geoproduct_offer": "show_geoproduct_offer", + "snippet_show_bookmark": "show_bookmark", + "detailview_show_claim_organization": "not_show_claim_organization", + "detailview_show_reviews": "show_reviews", + "detailview_show_add_photo_button": "show_add_photo_button", + "detailview_show_taxi_button": "show_taxi_button", + "sensitive": "1" + }, + "seoname": "7_y_avtobusny_park", + "geoId": 117015, + "uri": "ymapsbm1://org?oid=239366950658", + "uriList": [ + "ymapsbm1://org?oid=239366950658", + "ymapsbm1://transit/stop?id=stop__9639579" + ], + "references": [ + { + "id": "2036929560", + "scope": "nyak" + } + ], + "ratingData": { + "ratingCount": 0, + "ratingValue": 0, + "reviewCount": 0 + }, + "sources": [ + { + "id": "yandex", + "name": "Яндекс", + "href": "https://www.yandex.ru" + } + ], + "analyticsId": "1" + }, + "toponymSeoname": "dmitrovskoye_shosse" + } +} \ No newline at end of file diff --git a/tests/helpers/test_config_entry_oauth2_flow.py b/tests/helpers/test_config_entry_oauth2_flow.py new file mode 100644 index 00000000000..e47dd834bf7 --- /dev/null +++ b/tests/helpers/test_config_entry_oauth2_flow.py @@ -0,0 +1,266 @@ +"""Tests for the Somfy config flow.""" +import asyncio +import logging +from unittest.mock import patch +import time + +import pytest + +from homeassistant import data_entry_flow, setup, config_entries +from homeassistant.helpers import config_entry_oauth2_flow + +from tests.common import mock_platform, MockConfigEntry + +TEST_DOMAIN = "oauth2_test" +CLIENT_SECRET = "5678" +CLIENT_ID = "1234" +REFRESH_TOKEN = "mock-refresh-token" +ACCESS_TOKEN_1 = "mock-access-token-1" +ACCESS_TOKEN_2 = "mock-access-token-2" +AUTHORIZE_URL = "https://example.como/auth/authorize" +TOKEN_URL = "https://example.como/auth/token" + + +@pytest.fixture +async def local_impl(hass): + """Local implementation.""" + assert await setup.async_setup_component(hass, "http", {}) + return config_entry_oauth2_flow.LocalOAuth2Implementation( + hass, TEST_DOMAIN, CLIENT_ID, CLIENT_SECRET, AUTHORIZE_URL, TOKEN_URL + ) + + +@pytest.fixture +def flow_handler(hass): + """Return a registered config flow.""" + + mock_platform(hass, f"{TEST_DOMAIN}.config_flow") + + class TestFlowHandler(config_entry_oauth2_flow.AbstractOAuth2FlowHandler): + """Test flow handler.""" + + DOMAIN = TEST_DOMAIN + + @property + def logger(self) -> logging.Logger: + """Return logger.""" + return logging.getLogger(__name__) + + @property + def extra_authorize_data(self) -> dict: + """Extra data that needs to be appended to the authorize url.""" + return {"scope": "read write"} + + with patch.dict(config_entries.HANDLERS, {TEST_DOMAIN: TestFlowHandler}): + yield TestFlowHandler + + +class MockOAuth2Implementation(config_entry_oauth2_flow.AbstractOAuth2Implementation): + """Mock implementation for testing.""" + + @property + def name(self) -> str: + """Name of the implementation.""" + return "Mock" + + @property + def domain(self) -> str: + """Domain that is providing the implementation.""" + return "test" + + async def async_generate_authorize_url(self, flow_id: str) -> str: + """Generate a url for the user to authorize.""" + return "http://example.com/auth" + + async def async_resolve_external_data(self, external_data) -> dict: + """Resolve external data to tokens.""" + return external_data + + async def _async_refresh_token(self, token: dict) -> dict: + """Refresh a token.""" + raise NotImplementedError() + + +def test_inherit_enforces_domain_set(): + """Test we enforce setting DOMAIN.""" + + class TestFlowHandler(config_entry_oauth2_flow.AbstractOAuth2FlowHandler): + """Test flow handler.""" + + @property + def logger(self) -> logging.Logger: + """Return logger.""" + return logging.getLogger(__name__) + + with patch.dict(config_entries.HANDLERS, {TEST_DOMAIN: TestFlowHandler}): + with pytest.raises(TypeError): + TestFlowHandler() + + +async def test_abort_if_no_implementation(hass, flow_handler): + """Check flow abort when no implementations.""" + flow = flow_handler() + flow.hass = hass + result = await flow.async_step_user() + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "missing_configuration" + + +async def test_abort_if_authorization_timeout(hass, flow_handler, local_impl): + """Check timeout generating authorization url.""" + flow_handler.async_register_implementation(hass, local_impl) + + flow = flow_handler() + flow.hass = hass + + with patch.object( + local_impl, "async_generate_authorize_url", side_effect=asyncio.TimeoutError + ): + result = await flow.async_step_user() + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "authorize_url_timeout" + + +async def test_full_flow( + hass, flow_handler, local_impl, aiohttp_client, aioclient_mock +): + """Check full flow.""" + hass.config.api.base_url = "https://example.com" + flow_handler.async_register_implementation(hass, local_impl) + config_entry_oauth2_flow.async_register_implementation( + hass, TEST_DOMAIN, MockOAuth2Implementation() + ) + + result = await hass.config_entries.flow.async_init( + TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "pick_implementation" + + # Pick implementation + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"implementation": TEST_DOMAIN} + ) + + 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"{AUTHORIZE_URL}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}&scope=read+write" + ) + + client = await aiohttp_client(hass.http.app) + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.post( + TOKEN_URL, + json={ + "refresh_token": REFRESH_TOKEN, + "access_token": ACCESS_TOKEN_1, + "type": "bearer", + "expires_in": 60, + }, + ) + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["data"]["auth_implementation"] == TEST_DOMAIN + + result["data"]["token"].pop("expires_at") + assert result["data"]["token"] == { + "refresh_token": REFRESH_TOKEN, + "access_token": ACCESS_TOKEN_1, + "type": "bearer", + "expires_in": 60, + } + + entry = hass.config_entries.async_entries(TEST_DOMAIN)[0] + + assert ( + await config_entry_oauth2_flow.async_get_config_entry_implementation( + hass, entry + ) + is local_impl + ) + + +async def test_local_refresh_token(hass, local_impl, aioclient_mock): + """Test we can refresh token.""" + aioclient_mock.post( + TOKEN_URL, json={"access_token": ACCESS_TOKEN_2, "expires_in": 100} + ) + + new_tokens = await local_impl.async_refresh_token( + { + "refresh_token": REFRESH_TOKEN, + "access_token": ACCESS_TOKEN_1, + "type": "bearer", + "expires_in": 60, + } + ) + new_tokens.pop("expires_at") + + assert new_tokens == { + "refresh_token": REFRESH_TOKEN, + "access_token": ACCESS_TOKEN_2, + "type": "bearer", + "expires_in": 100, + } + + assert len(aioclient_mock.mock_calls) == 1 + assert aioclient_mock.mock_calls[0][2] == { + "client_id": CLIENT_ID, + "client_secret": CLIENT_SECRET, + "grant_type": "refresh_token", + "refresh_token": REFRESH_TOKEN, + } + + +async def test_oauth_session(hass, flow_handler, local_impl, aioclient_mock): + """Test the OAuth2 session helper.""" + flow_handler.async_register_implementation(hass, local_impl) + + aioclient_mock.post( + TOKEN_URL, json={"access_token": ACCESS_TOKEN_2, "expires_in": 100} + ) + + aioclient_mock.post("https://example.com", status=201) + + config_entry = MockConfigEntry( + domain=TEST_DOMAIN, + data={ + "auth_implementation": TEST_DOMAIN, + "token": { + "refresh_token": REFRESH_TOKEN, + "access_token": ACCESS_TOKEN_1, + "expires_in": 10, + "expires_at": 0, # Forces a refresh, + "token_type": "bearer", + "random_other_data": "should_stay", + }, + }, + ) + + now = time.time() + session = config_entry_oauth2_flow.OAuth2Session(hass, config_entry, local_impl) + resp = await session.async_request("post", "https://example.com") + assert resp.status == 201 + + # Refresh token, make request + assert len(aioclient_mock.mock_calls) == 2 + + assert ( + aioclient_mock.mock_calls[1][3]["authorization"] == f"Bearer {ACCESS_TOKEN_2}" + ) + + assert config_entry.data["token"]["refresh_token"] == REFRESH_TOKEN + assert config_entry.data["token"]["access_token"] == ACCESS_TOKEN_2 + assert config_entry.data["token"]["expires_in"] == 100 + assert config_entry.data["token"]["random_other_data"] == "should_stay" + assert round(config_entry.data["token"]["expires_at"] - now) == 100 diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index e09f8cf57aa..1f5d6ddfc40 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -494,7 +494,10 @@ def test_deprecated_with_no_optionals(caplog, schema): test_data = {"mars": True} output = deprecated_schema(test_data.copy()) assert len(caplog.records) == 1 - assert caplog.records[0].name == __name__ + assert caplog.records[0].name in [ + __name__, + "homeassistant.helpers.config_validation", + ] assert ( "The 'mars' option (with value 'True') is deprecated, " "please remove it from your configuration" diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 18cedf1c46a..9d05920f78b 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -231,6 +231,88 @@ def test_async_schedule_update_ha_state(hass): assert update_call is True +async def test_async_async_request_call_without_lock(hass): + """Test for async_requests_call works without a lock.""" + updates = [] + + class AsyncEntity(entity.Entity): + def __init__(self, entity_id): + """Initialize Async test entity.""" + self.entity_id = entity_id + self.hass = hass + + async def testhelper(self, count): + """Helper function.""" + updates.append(count) + + ent_1 = AsyncEntity("light.test_1") + ent_2 = AsyncEntity("light.test_2") + try: + job1 = ent_1.async_request_call(ent_1.testhelper(1)) + job2 = ent_2.async_request_call(ent_2.testhelper(2)) + + await asyncio.wait([job1, job2]) + while True: + if len(updates) >= 2: + break + await asyncio.sleep(0) + finally: + pass + + assert len(updates) == 2 + updates.sort() + assert updates == [1, 2] + + +async def test_async_async_request_call_with_lock(hass): + """Test for async_requests_call works with a semaphore.""" + updates = [] + + test_semaphore = asyncio.Semaphore(1) + + class AsyncEntity(entity.Entity): + def __init__(self, entity_id, lock): + """Initialize Async test entity.""" + self.entity_id = entity_id + self.hass = hass + self.parallel_updates = lock + + async def testhelper(self, count): + """Helper function.""" + updates.append(count) + + ent_1 = AsyncEntity("light.test_1", test_semaphore) + ent_2 = AsyncEntity("light.test_2", test_semaphore) + + try: + assert test_semaphore.locked() is False + await test_semaphore.acquire() + assert test_semaphore.locked() + + job1 = ent_1.async_request_call(ent_1.testhelper(1)) + job2 = ent_2.async_request_call(ent_2.testhelper(2)) + + hass.async_create_task(job1) + hass.async_create_task(job2) + + assert len(updates) == 0 + assert updates == [] + assert test_semaphore._value == 0 + + test_semaphore.release() + + while True: + if len(updates) >= 2: + break + await asyncio.sleep(0) + finally: + test_semaphore.release() + + assert len(updates) == 2 + updates.sort() + assert updates == [1, 2] + + async def test_async_parallel_updates_with_zero(hass): """Test parallel updates with 0 (disabled).""" updates = [] diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index ebc56c111ee..4b8be715f37 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -9,7 +9,9 @@ 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 # Otherwise can't test just this file (import order issue) @@ -120,6 +122,31 @@ async def test_calling_service(hass): assert calls[0].data.get("hello") == "world" +async def test_activating_scene(hass): + """Test the activation of a scene.""" + calls = [] + context = Context() + + @callback + def record_call(service): + """Add recorded event to set.""" + calls.append(service) + + hass.services.async_register(scene.DOMAIN, SERVICE_TURN_ON, record_call) + + hass.async_add_job( + ft.partial( + script.call_from_config, hass, {"scene": "scene.hello"}, context=context + ) + ) + + await hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0].context is context + assert calls[0].data.get(ATTR_ENTITY_ID) == "scene.hello" + + async def test_calling_service_template(hass): """Test the calling of a service.""" calls = [] diff --git a/tests/helpers/test_state.py b/tests/helpers/test_state.py index 7f428c0833d..14bcbde5094 100644 --- a/tests/helpers/test_state.py +++ b/tests/helpers/test_state.py @@ -129,7 +129,7 @@ async def test_reproduce_turn_on(hass): last_call = calls[-1] assert last_call.domain == "light" assert SERVICE_TURN_ON == last_call.service - assert ["light.test"] == last_call.data.get("entity_id") + assert "light.test" == last_call.data.get("entity_id") async def test_reproduce_turn_off(hass): @@ -146,7 +146,7 @@ async def test_reproduce_turn_off(hass): last_call = calls[-1] assert last_call.domain == "light" assert SERVICE_TURN_OFF == last_call.service - assert ["light.test"] == last_call.data.get("entity_id") + assert "light.test" == last_call.data.get("entity_id") async def test_reproduce_complex_data(hass): @@ -155,10 +155,10 @@ async def test_reproduce_complex_data(hass): hass.states.async_set("light.test", "off") - complex_data = ["hello", {"11": "22"}] + complex_data = [255, 100, 100] await state.async_reproduce_state( - hass, ha.State("light.test", "on", {"complex": complex_data}) + hass, ha.State("light.test", "on", {"rgb_color": complex_data}) ) await hass.async_block_till_done() @@ -167,7 +167,7 @@ async def test_reproduce_complex_data(hass): last_call = calls[-1] assert last_call.domain == "light" assert SERVICE_TURN_ON == last_call.service - assert complex_data == last_call.data.get("complex") + assert complex_data == last_call.data.get("rgb_color") async def test_reproduce_bad_state(hass): diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index cc1f7707df6..b69fdb17e35 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -501,6 +501,30 @@ def test_timestamp_local(hass): ) +def test_to_json(hass): + """Test the object to JSON string filter.""" + + # Note that we're not testing the actual json.loads and json.dumps methods, + # only the filters, so we don't need to be exhaustive with our sample JSON. + expected_result = '{"Foo": "Bar"}' + actual_result = template.Template( + "{{ {'Foo': 'Bar'} | to_json }}", hass + ).async_render() + assert actual_result == expected_result + + +def test_from_json(hass): + """Test the JSON string to object filter.""" + + # Note that we're not testing the actual json.loads and json.dumps methods, + # only the filters, so we don't need to be exhaustive with our sample JSON. + expected_result = "Bar" + actual_result = template.Template( + '{{ (\'{"Foo": "Bar"}\' | from_json).Foo }}', hass + ).async_render() + assert actual_result == expected_result + + def test_min(hass): """Test the min filter.""" assert template.Template("{{ [1, 2, 3] | min }}", hass).async_render() == "1" diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index 18143c088be..5199f01807f 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -92,8 +92,8 @@ def test_secrets(isfile_patch, loop): files = { get_test_config_dir(YAML_CONFIG_FILE): BASE_CONFIG - + ("http:\n" " api_password: !secret http_pw"), - secrets_path: ("logger: debug\n" "http_pw: abc123"), + + ("http:\n" " cors_allowed_origins: !secret http_pw"), + secrets_path: ("logger: debug\n" "http_pw: http://google.com"), } with patch_yaml_files(files): @@ -103,17 +103,15 @@ def test_secrets(isfile_patch, loop): assert res["except"] == {} assert res["components"].keys() == {"homeassistant", "http"} assert res["components"]["http"] == { - "api_password": "abc123", - "cors_allowed_origins": ["https://cast.home-assistant.io"], + "cors_allowed_origins": ["http://google.com"], "ip_ban_enabled": True, "login_attempts_threshold": -1, "server_host": "0.0.0.0", "server_port": 8123, - "trusted_networks": [], "ssl_profile": "modern", } - assert res["secret_cache"] == {secrets_path: {"http_pw": "abc123"}} - assert res["secrets"] == {"http_pw": "abc123"} + assert res["secret_cache"] == {secrets_path: {"http_pw": "http://google.com"}} + assert res["secrets"] == {"http_pw": "http://google.com"} assert normalize_yaml_files(res) == [ ".../configuration.yaml", ".../secrets.yaml", diff --git a/tests/test_config.py b/tests/test_config.py index a67cd345797..dab51f59176 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -5,7 +5,6 @@ import copy import os import unittest.mock as mock from collections import OrderedDict -from ipaddress import ip_network import asynctest import pytest @@ -34,11 +33,6 @@ from homeassistant.const import ( from homeassistant.util import dt as dt_util from homeassistant.util.yaml import SECRET_YAML from homeassistant.helpers.entity import Entity -from homeassistant.components.config.group import CONFIG_PATH as GROUP_CONFIG_PATH -from homeassistant.components.config.automation import ( - CONFIG_PATH as AUTOMATIONS_CONFIG_PATH, -) -from homeassistant.components.config.script import CONFIG_PATH as SCRIPTS_CONFIG_PATH import homeassistant.helpers.check_config as check_config from tests.common import get_test_config_dir, patch_yaml_files @@ -47,9 +41,9 @@ CONFIG_DIR = get_test_config_dir() YAML_PATH = os.path.join(CONFIG_DIR, config_util.YAML_CONFIG_FILE) SECRET_PATH = os.path.join(CONFIG_DIR, SECRET_YAML) VERSION_PATH = os.path.join(CONFIG_DIR, config_util.VERSION_FILE) -GROUP_PATH = os.path.join(CONFIG_DIR, GROUP_CONFIG_PATH) -AUTOMATIONS_PATH = os.path.join(CONFIG_DIR, AUTOMATIONS_CONFIG_PATH) -SCRIPTS_PATH = os.path.join(CONFIG_DIR, SCRIPTS_CONFIG_PATH) +GROUP_PATH = os.path.join(CONFIG_DIR, config_util.GROUP_CONFIG_PATH) +AUTOMATIONS_PATH = os.path.join(CONFIG_DIR, config_util.AUTOMATION_CONFIG_PATH) +SCRIPTS_PATH = os.path.join(CONFIG_DIR, config_util.SCRIPT_CONFIG_PATH) ORIG_TIMEZONE = dt_util.DEFAULT_TIME_ZONE @@ -346,62 +340,6 @@ def test_config_upgrade_no_file(hass): assert opened_file.write.call_args == mock.call(__version__) -@mock.patch("homeassistant.config.shutil") -@mock.patch("homeassistant.config.os") -@mock.patch("homeassistant.config.find_config_file", mock.Mock()) -def test_migrate_file_on_upgrade(mock_os, mock_shutil, hass): - """Test migrate of config files on upgrade.""" - ha_version = "0.7.0" - - mock_os.path.isdir = mock.Mock(return_value=True) - - mock_open = mock.mock_open() - - def _mock_isfile(filename): - return True - - with mock.patch("homeassistant.config.open", mock_open, create=True), mock.patch( - "homeassistant.config.os.path.isfile", _mock_isfile - ): - opened_file = mock_open.return_value - # pylint: disable=no-member - opened_file.readline.return_value = ha_version - - hass.config.path = mock.Mock() - - config_util.process_ha_config_upgrade(hass) - - assert mock_os.rename.call_count == 1 - - -@mock.patch("homeassistant.config.shutil") -@mock.patch("homeassistant.config.os") -@mock.patch("homeassistant.config.find_config_file", mock.Mock()) -def test_migrate_no_file_on_upgrade(mock_os, mock_shutil, hass): - """Test not migrating config files on upgrade.""" - ha_version = "0.7.0" - - mock_os.path.isdir = mock.Mock(return_value=True) - - mock_open = mock.mock_open() - - def _mock_isfile(filename): - return False - - with mock.patch("homeassistant.config.open", mock_open, create=True), mock.patch( - "homeassistant.config.os.path.isfile", _mock_isfile - ): - opened_file = mock_open.return_value - # pylint: disable=no-member - opened_file.readline.return_value = ha_version - - hass.config.path = mock.Mock() - - config_util.process_ha_config_upgrade(hass) - - assert mock_os.rename.call_count == 0 - - async def test_loading_configuration_from_storage(hass, hass_storage): """Test loading core config onto hass object.""" hass_storage["core.config"] = { @@ -876,48 +814,6 @@ async def test_auth_provider_config_default(hass): assert hass.auth.auth_mfa_modules[0].id == "totp" -async def test_auth_provider_config_default_api_password(hass): - """Test loading default auth provider config with api password.""" - core_config = { - "latitude": 60, - "longitude": 50, - "elevation": 25, - "name": "Huis", - CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL, - "time_zone": "GMT", - } - if hasattr(hass, "auth"): - del hass.auth - await config_util.async_process_ha_core_config(hass, core_config, "pass") - - assert len(hass.auth.auth_providers) == 2 - assert hass.auth.auth_providers[0].type == "homeassistant" - assert hass.auth.auth_providers[1].type == "legacy_api_password" - assert hass.auth.auth_providers[1].api_password == "pass" - - -async def test_auth_provider_config_default_trusted_networks(hass): - """Test loading default auth provider config with trusted networks.""" - core_config = { - "latitude": 60, - "longitude": 50, - "elevation": 25, - "name": "Huis", - CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL, - "time_zone": "GMT", - } - if hasattr(hass, "auth"): - del hass.auth - await config_util.async_process_ha_core_config( - hass, core_config, trusted_networks=["192.168.0.1"] - ) - - assert len(hass.auth.auth_providers) == 2 - assert hass.auth.auth_providers[0].type == "homeassistant" - assert hass.auth.auth_providers[1].type == "trusted_networks" - assert hass.auth.auth_providers[1].trusted_networks[0] == ip_network("192.168.0.1") - - async def test_disallowed_auth_provider_config(hass): """Test loading insecure example auth provider is disallowed.""" core_config = { diff --git a/tests/test_main.py b/tests/test_main.py index 509425ce418..29454d269af 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -2,6 +2,7 @@ from unittest.mock import patch, PropertyMock from homeassistant import __main__ as main +from homeassistant.const import REQUIRED_PYTHON_VER @patch("sys.exit") @@ -31,6 +32,32 @@ def test_validate_python(mock_exit): mock_exit.reset_mock() - with patch("sys.version_info", new_callable=PropertyMock(return_value=(3, 6, 0))): + with patch( + "sys.version_info", + new_callable=PropertyMock( + return_value=(REQUIRED_PYTHON_VER[0] - 1,) + REQUIRED_PYTHON_VER[1:] + ), + ): + main.validate_python() + assert mock_exit.called is True + + mock_exit.reset_mock() + + with patch( + "sys.version_info", new_callable=PropertyMock(return_value=REQUIRED_PYTHON_VER) + ): main.validate_python() assert mock_exit.called is False + + mock_exit.reset_mock() + + with patch( + "sys.version_info", + new_callable=PropertyMock( + return_value=(REQUIRED_PYTHON_VER[:2]) + (REQUIRED_PYTHON_VER[2] + 1,) + ), + ): + main.validate_python() + assert mock_exit.called is False + + mock_exit.reset_mock() diff --git a/tests/test_requirements.py b/tests/test_requirements.py index b5574fe96fd..780b175778e 100644 --- a/tests/test_requirements.py +++ b/tests/test_requirements.py @@ -17,6 +17,13 @@ from homeassistant.requirements import ( from tests.common import get_test_home_assistant, MockModule, mock_integration +def env_without_wheel_links(): + """Return env without wheel links.""" + env = dict(os.environ) + env.pop("WHEEL_LINKS", None) + return env + + class TestRequirements: """Test the requirements module.""" @@ -36,6 +43,7 @@ class TestRequirements: @patch("homeassistant.util.package.is_virtual_env", return_value=True) @patch("homeassistant.util.package.is_docker_env", return_value=False) @patch("homeassistant.util.package.install_package", return_value=True) + @patch.dict(os.environ, env_without_wheel_links(), clear=True) def test_requirement_installed_in_venv( self, mock_install, mock_denv, mock_venv, mock_dirname ): @@ -55,6 +63,7 @@ class TestRequirements: @patch("homeassistant.util.package.is_virtual_env", return_value=False) @patch("homeassistant.util.package.is_docker_env", return_value=False) @patch("homeassistant.util.package.install_package", return_value=True) + @patch.dict(os.environ, env_without_wheel_links(), clear=True) def test_requirement_installed_in_deps( self, mock_install, mock_denv, mock_venv, mock_dirname ): @@ -136,7 +145,7 @@ async def test_install_with_wheels_index(hass): mock_dir.return_value = "ha_package_path" assert await setup.async_setup_component(hass, "comp", {}) assert "comp" in hass.config.components - print(mock_inst.call_args) + assert mock_inst.call_args == call( "hello==1.0.0", find_links="https://wheels.hass.io/test", @@ -154,11 +163,13 @@ async def test_install_on_docker(hass): "homeassistant.util.package.is_docker_env", return_value=True ), patch("homeassistant.util.package.install_package") as mock_inst, patch( "os.path.dirname" - ) as mock_dir: + ) as mock_dir, patch.dict( + os.environ, env_without_wheel_links(), clear=True + ): mock_dir.return_value = "ha_package_path" assert await setup.async_setup_component(hass, "comp", {}) assert "comp" in hass.config.components - print(mock_inst.call_args) + assert mock_inst.call_args == call( "hello==1.0.0", constraints=os.path.join("ha_package_path", CONSTRAINT_FILE), diff --git a/tests/testing_config/custom_components/test/alarm_control_panel.py b/tests/testing_config/custom_components/test/alarm_control_panel.py new file mode 100644 index 00000000000..0e2842f8695 --- /dev/null +++ b/tests/testing_config/custom_components/test/alarm_control_panel.py @@ -0,0 +1,91 @@ +""" +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.const import ( + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, + STATE_ALARM_TRIGGERED, +) +from tests.common import MockEntity + +ENTITIES = {} + + +def init(empty=False): + """Initialize the platform with entities.""" + global ENTITIES + + ENTITIES = ( + {} + if empty + else { + "arm_code": MockAlarm( + name=f"Alarm arm code", + code_arm_required=True, + unique_id="unique_arm_code", + ), + "no_arm_code": MockAlarm( + name=f"Alarm no arm code", + code_arm_required=False, + unique_id="unique_no_arm_code", + ), + } + ) + + +async def async_setup_platform( + hass, config, async_add_entities_callback, discovery_info=None +): + """Return mock entities.""" + async_add_entities_callback(list(ENTITIES.values())) + + +class MockAlarm(MockEntity, AlarmControlPanel): + """Mock Alarm control panel class.""" + + def __init__(self, **values): + """Init the Mock Alarm Control Panel.""" + self._state = None + + MockEntity.__init__(self, **values) + + @property + def code_arm_required(self): + """Whether the code is required for arm actions.""" + return self._handle("code_arm_required") + + @property + def state(self): + """Return the state of the device.""" + return self._state + + def alarm_arm_away(self, code=None): + """Send arm away command.""" + self._state = STATE_ALARM_ARMED_AWAY + self.async_write_ha_state() + + def alarm_arm_home(self, code=None): + """Send arm home command.""" + self._state = STATE_ALARM_ARMED_HOME + self.async_write_ha_state() + + def alarm_arm_night(self, code=None): + """Send arm night command.""" + self._state = STATE_ALARM_ARMED_NIGHT + self.async_write_ha_state() + + def alarm_disarm(self, code=None): + """Send disarm command.""" + if code == "1234": + self._state = STATE_ALARM_DISARMED + self.async_write_ha_state() + + def alarm_trigger(self, code=None): + """Send alarm trigger command.""" + self._state = STATE_ALARM_TRIGGERED + self.async_write_ha_state() diff --git a/tests/testing_config/custom_components/test/lock.py b/tests/testing_config/custom_components/test/lock.py new file mode 100644 index 00000000000..db6ce38b097 --- /dev/null +++ b/tests/testing_config/custom_components/test/lock.py @@ -0,0 +1,54 @@ +""" +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 tests.common import MockEntity + +ENTITIES = {} + + +def init(empty=False): + """Initialize the platform with entities.""" + global ENTITIES + + ENTITIES = ( + {} + if empty + else { + "support_open": MockLock( + name=f"Support open Lock", + is_locked=True, + supported_features=SUPPORT_OPEN, + unique_id="unique_support_open", + ), + "no_support_open": MockLock( + name=f"No support open Lock", + is_locked=True, + supported_features=0, + unique_id="unique_no_support_open", + ), + } + ) + + +async def async_setup_platform( + hass, config, async_add_entities_callback, discovery_info=None +): + """Return mock entities.""" + async_add_entities_callback(list(ENTITIES.values())) + + +class MockLock(MockEntity, LockDevice): + """Mock Lock class.""" + + @property + def is_locked(self): + """Return true if the lock is locked.""" + return self._handle("is_locked") + + @property + def supported_features(self): + """Return the class of this sensor.""" + return self._handle("supported_features") diff --git a/tests/util/test_logging.py b/tests/util/test_logging.py index 414b246466c..d5f8eb4a2c7 100644 --- a/tests/util/test_logging.py +++ b/tests/util/test_logging.py @@ -72,12 +72,8 @@ async def test_async_create_catching_coro(hass, caplog): async def job(): raise Exception("This is a bad coroutine") - pass hass.async_create_task(logging_util.async_create_catching_coro(job())) await hass.async_block_till_done() assert "This is a bad coroutine" in caplog.text - assert ( - "hass.async_create_task(" - "logging_util.async_create_catching_coro(job()))" in caplog.text - ) + assert "in test_async_create_catching_coro" in caplog.text diff --git a/tox.ini b/tox.ini index 2d4cf7c54ba..f6d12fe30f5 100644 --- a/tox.ini +++ b/tox.ini @@ -26,7 +26,7 @@ deps = -r{toxinidir}/requirements_test.txt -c{toxinidir}/homeassistant/package_constraints.txt commands = - pylint {posargs} homeassistant + pylint {env:PYLINT_ARGS} {posargs} homeassistant [testenv:lint] deps = @@ -34,12 +34,11 @@ deps = commands = python -m script.gen_requirements_all validate python -m script.hassfest validate - flake8 {posargs: homeassistant tests script} + pre-commit run flake8 {posargs: --all-files} [testenv:typing] -whitelist_externals=/bin/bash deps = -r{toxinidir}/requirements_test.txt -c{toxinidir}/homeassistant/package_constraints.txt commands = - /bin/bash -c 'TYPING_FILES=$(cat mypyrc); mypy $TYPING_FILES' + pre-commit run mypy {posargs: --all-files}